自然数に対してのみ次のアルゴリズムを持つ: rounds(n)={1, if n=1; 1+rounds(ceil(n/2)), else} なのでプログラミング言語で書くとこうなる
int rounds(int n){
if(n==1)
return 1;
return 1+rounds(ceil(n/2));
}
これには時間の複雑さ O(log n) があると思います
より良い複雑さはありますか?
自然数に対してのみ次のアルゴリズムを持つ: rounds(n)={1, if n=1; 1+rounds(ceil(n/2)), else} なのでプログラミング言語で書くとこうなる
int rounds(int n){
if(n==1)
return 1;
return 1+rounds(ceil(n/2));
}
これには時間の複雑さ O(log n) があると思います
より良い複雑さはありますか?
アルゴリズムが反復的で数値が 2 進数であると考える場合、この関数は最下位ビットをシフト アウトし、シフト アウトされたのが 1 の場合は数値を 1 増やします。したがって、増分を除いて、数値のビット数 (つまり、最大の 1 の位置) をカウントします。数値が 1000... の形式の場合を除いて、インクリメントは最終的に結果を 1 増やします。したがって、ビット数 + 1、または数値が 2 の累乗の場合はビット数が得られます。マシンのモデルによっては、O(log n) よりも高速に計算できる場合があります。
1から上に結果をリストすることから始めて、
rounds(1) = 1
rounds(2) = 1 + rounds(2/2) = 1 + 1 = 2
次に、ceil(n/2)
が 2 の場合はrounds(n)
3 になりn = 3
ますn = 4
。
rounds(3) = rounds(4) = 3
ceil(n/2)
が 3 または 4 の場合、結果は 4 になり3 <= ceil(n/2) <= 4
ます2*3-1 <= n <= 2*4
。
round(5) = ... = rounds(8) = 4
続けてみるとわかるように
rounds(n) = k+2 if 2^k < n <= 2^(k+1)
誘導によって。
それを次のように書き換えることができます
rounds(n) = 2 + floor(log_2(n-1)) if n > 1 [and rounds(1) = 1]
n = 1
数学的には、次のように書き換えることで一様に扱うこともできます
rounds(n) = 1 + floor(log_2(2*n-1))
ただし、固定幅型を使用している場合、最後の式はオーバーフローの可能性があります。
だから質問は
固定幅の型、つまり境界のある範囲の場合、もちろんこれらはすべて O(1) 操作ですが、計算の複雑さは問題になりませんが、可能な限り効率的にすることにおそらく関心があるでしょう。
ネイティブ マシン タイプのint
場合 (long
通常はそうです)、整数の比較と減算は非常に高速なマシン命令であるため、問題になる可能性があるのは底 2 の対数だけです。
多くのプロセッサには、マシン タイプの値の先頭の 0 ビットをカウントするためのマシン命令があり、コンパイラがアクセスできるようにすると、基数 2 の対数を非常に高速に実装できます。そうでない場合は、古典的なビットハックの 1 つを使用して、再帰よりも高速なバージョンを取得できます。
たとえば、gcc と clang の十分に最近のバージョンには、命令がプロセッサ上に存在する場合はその命令にマップする__builtin_clz
( __builtin_clzl
64 ビット タイプの場合はそれぞれ)bsr*
があり、提供されていない場合は、おそらく何らかのビット操作を使用する適切な実装があります。プロセッサによって。
バージョン
unsigned rounds(unsigned long n) {
if (n <= 1) return n;
return sizeof n * CHAR_BIT + 1 - __builtin_clzl(n-1);
}
命令を使用すると、 (私のボックスでは) 1 から 100,000,000 までのビットハックbsrq
を計算するのに 0.165 秒かかりますrounds
unsigned rounds(unsigned n) {
if (n <= 1) return n;
--n;
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
n |= n >> 8;
n |= n >> 16;
n -= (n >> 1) & 0x55555555;
n = (n & 0x33333333) + ((n >> 2) & 0x33333333);
n = (n & 0x0F0F0F0F) + ((n >> 4) & 0x0F0F0F0F);
return ((n * 0x01010101) >> 24)+1;
}
0.626 秒かかり、単純なループ
unsigned rounds(unsigned n) {
unsigned r = 1;
while(n > 1) {
++r;
n = (n+1)/2;
}
return r;
}
1.865 秒かかります。
固定幅型を使用せず、任意精度の整数を使用すると、状況が少し変わります。単純なループ (または再帰) はまだステップを使用しΘ(log n)
ますが、ステップにΘ(log n)
は平均して時間がかかる (またはさらに悪い) ため、全体としてΘ(log² n)
アルゴリズム (またはさらに悪い) が得られます。次に、上記の式を使用すると、定数係数が低くなるだけでなく、アルゴリズムの複雑さも低くなります。
O(log n)
、妥当な表現では最悪のケースです。O(log n)
と、合理的な表現が得られます。O(log n)
他の合理的な表現では [2 のべき乗の底を使用する場合、私は半分慣れているすべての任意精度ライブラリを使用します。と; 彼らが10のべき乗ベースを使用した場合、それは異なるでしょう].