web-dev-qa-db-ja.com

ユークリッドアルゴリズムの時間の複雑さ

ユークリッドの最大公約数アルゴリズムの時間の複雑さを判断するのは困難です。擬似コードのこのアルゴリズムは次のとおりです。

function gcd(a, b)
    while b ≠ 0
       t := b
       b := a mod b
       a := t
    return a

aおよびbに依存しているようです。私の考えでは、時間の複雑さはO(a%b)です。あれは正しいですか?それを書くより良い方法はありますか?

82
Donald Taylor

ユークリッドアルゴリズムの時間の複雑さを分析するための1つのコツは、2回の反復で何が起こるかを追跡することです。

_a', b' := a % b, b % (a % b)
_

これで、aとbの両方が1つではなく減少し、分析が容易になります。あなたはそれをケースに分けることができます:

  • 小さなA:_2a <= b_
  • 小さなB:_2b <= a_
  • 小A:_2a > b_が_a < b_
  • スモールB:_2b > a_が_b < a_
  • 等しい:_a == b_

ここで、すべてのケースが合計_a+b_を少なくとも4分の1ずつ減らすことを示します。

  • 小さなA:b % (a % b) < aおよび_2a <= b_、したがってbは少なくとも半分に減少するため、_a+b_は少なくとも_25%_減少します
  • Tiny B:_a % b < b_および_2b <= a_、したがってaは少なくとも半分に減少するため、_a+b_は少なくとも_25%_減少します
  • 小さいA:bは_b-a_になり、_b/2_より小さく、_a+b_を少なくとも_25%_減らします。
  • Small B:aは_a-b_になります。これは_a/2_より小さく、_a+b_を少なくとも_25%_減らします。
  • 等しい:_a+b_は_0_に下がります。これは明らかに_a+b_を少なくとも_25%_だけ減少させます。

したがって、ケース分析により、すべてのダブルステップで_a+b_が少なくとも_25%_減少します。 _a+b_が_1_を下回らない限り、これが発生する可能性のある最大回数があります。 0に達するまでの合計ステップ数(S)は、_(4/3)^S <= A+B_を満たす必要があります。今すぐそれを動作させます:

_(4/3)^S <= A+B
S <= lg[4/3](A+B)
S is O(lg[4/3](A+B))
S is O(lg(A+B))
S is O(lg(A*B)) //because A*B asymptotically greater than A+B
S is O(lg(A)+lg(B))
//Input size N is lg(A) + lg(B)
S is O(N)
_

したがって、反復の数は入力桁数で線形になります。 CPUレジスターに収まる数値の場合、一定の時間がかかるように反復をモデル化し、gcdのtotal実行時間が線形であると仮定するのが妥当です。

もちろん、大きな整数を扱う場合、各反復内のモジュラス演算には一定のコストがないという事実を考慮する必要があります。大まかに言うと、漸近ランタイムの合計は、多対数因子のn ^ 2倍になります。 のようなものn^2 lg(n) 2^O(log* n)。代わりに binary gcd を使用することにより、多対数因子を回避できます。

65
Craig Gidney

アルゴリズムを分析する適切な方法は、最悪のシナリオを決定することです。ユークリッドGCDの最悪のケースは、フィボナッチペアが関係している場合に発生します。 void EGCD(fib[i], fib[i - 1])、ここでi> 0。

たとえば、配当が55で除数が34の場合を選択しましょう(まだフィボナッチ数を扱っていることを思い出してください)。

enter image description here

お気づきかもしれませんが、この操作には8回の反復(または再帰呼び出し)がかかりました。

より大きなフィボナッチ数、つまり121393と75025を試してみましょう。ここでも、24回の反復(または再帰呼び出し)が必要であることがわかります。

enter image description here

また、各反復がフィボナッチ数を生成することにも気付くことができます。それが私たちが非常に多くのオペレーションを持っている理由です。実際、フィボナッチ数だけでは同様の結果を得ることができません。

したがって、時間の複雑さは、今回は小さなOh(上限)で表されます。下限は直観的にOmega(1)です。たとえば、500を2で割った場合です。

再帰関係を解こう:

enter image description here

ユークリッドGCDはlog(xy)演算最大を作成できると言えます。

wikipediaの記事 でこれをよく見ることができます。

値のペアの複雑さの素敵なプロットもあります。

O(a%b)ではありません。

小さい数字の桁数の5倍以上のステップを踏むことは決してないことが知られています(記事を参照)。したがって、ステップの最大数は、桁数_(ln b)_とともに増加します。各ステップのコストも桁数に応じて増加するため、複雑さはO(ln^2 b)によって制限されます(bは小さい数です)。これは上限であり、実際の時間は通常は短くなります。

17
JoshD

here を参照してください。

特にこの部分:

ラメは、nより小さい2つの数値の最大公約数に到達するために必要なステップ数は

alt text

したがって、O(log min(a, b))は適切な上限です。

11
IVlad

ユークリッドのアルゴリズムの実行時の複雑さを直感的に理解できます。正式な証明は、Introduction to AlgorithmsやTAOCP Vol 2などのさまざまなテキストで説明されています。

最初に、2つのフィボナッチ数F(k + 1)とF(k)のgcdを取得しようとした場合について考えます。ユークリッドのアルゴリズムがF(k)およびF(k-1)で反復することをすぐに確認できます。つまり、各反復でフィボナッチ数列の1つの数値を下に移動します。 O(Phi ^ k)ここで、Phiは黄金比であり、GCDの実行時間はO(log n)であり、n = max(a、b)であり、logはPhiのベースを持っていることがわかります。最悪の場合、フィボナッチ数は、各反復で剰余が十分な大きさのままであり、系列の先頭に到達するまでゼロにならないペアを一貫して生成することを観察することによって。

O(log n)で、n = max(a、b)の範囲をさらに厳しくすることができます。 b> = aと仮定すると、O(log b)にboundを書き込むことができます。まず、GCD(ka、kb)= GCD(a、b)であることを確認します。 kの最大値はgcd(a、c)であるため、ランタイムでbをb/gcd(a、b)に置き換えて、O(log b/gcd(a、b))の境界をより厳密にできます。

8
Shital Shah

ユークリッドアルゴリズムの最悪のケースは、各ステップで剰余が最大になる場合です。フィボナッチ数列の2つの連続した項に対して。

Nおよびmがaおよびbの桁数である場合、n> = mと仮定すると、アルゴリズムはO(m)除算を使用します。

複雑さは常に入力のサイズの観点から与えられることに注意してください。この場合、桁数です。

4
Alexandre C.

Nとmの両方が連続したフィボナッチ数である場合、最悪のケースが発生します。

gcd(Fn、Fn−1)= gcd(Fn−1、Fn−2)=⋯= gcd(F1、F0)= 1であり、n番目のフィボナッチ数は1.618 ^ nであり、1.618は黄金比です。

したがって、gcd(n、m)を見つけるには、再帰呼び出しの数はΘ(logn)になります。

3
Arnav Attri

ただし、反復アルゴリズムの場合は次のとおりです。

_int iterativeEGCD(long long n, long long m) {
    long long a;
    int numberOfIterations = 0;
    while ( n != 0 ) {
         a = m;
         m = n;
         n = a % n;
        numberOfIterations ++;
    }
    printf("\nIterative GCD iterated %d times.", numberOfIterations);
    return m;
}
_

フィボナッチのペアでは、iterativeEGCD()iterativeEGCDForWorstCase()の間に違いはありません。後者は次のようになります。

_int iterativeEGCDForWorstCase(long long n, long long m) {
    long long a;
    int numberOfIterations = 0;
    while ( n != 0 ) {
         a = m;
         m = n;
         n = a - n;
        numberOfIterations ++;
    }
    printf("\nIterative GCD iterated %d times.", numberOfIterations);
    return m;
}
_

はい、フィボナッチペア、_n = a % n_および_n = a - n_では、まったく同じものです。

また、同じ質問に対する以前の回答では、一般的な減少要因であるfactor = m / (n % m)があることもわかっています。

したがって、ユークリッドGCDの反復バージョンを定義済みの形に形作るために、次のような「シミュレータ」として描くことができます。

_void iterativeGCDSimulator(long long x, long long y) {
    long long i;
    double factor = x / (double)(x % y);
    int numberOfIterations = 0;
    for ( i = x * y ; i >= 1 ; i = i / factor) {
        numberOfIterations ++;
    }
    printf("\nIterative GCD Simulator iterated %d times.", numberOfIterations);
}
_

Dr. Jauhar ALiの work (最後のスライド)に基づくと、上記のループは対数です。

enter image description here

はい、小さいああ、シミュレータは反復回数をせいぜいに伝えるからです。ユークリッドGCDでプローブした場合、非フィボナッチペアはフィボナッチよりも反復回数が少なくなります。

Gabriel Lameの定理は、log(1/sqrt(5)*(a + 1/2))-2でステップ数を制限します。ここで、対数の底は(1 + sqrt(5))/ 2です。これは、アルゴリズムの最悪の場合のためであり、入力が連続したフィバノッチ数である場合に発生します。

もう少し自由な境界は次のとおりです。loga、ログのベースは(sqrt(2))Koblitzによって暗示されます。

暗号化の目的のために、通常、ビットサイズがk = logaで与えられることを考慮して、アルゴリズムのビット単位の複雑さを考慮します。

Euclid Algorithのビット単位の複雑さの詳細な分析を次に示します。

ほとんどの参考文献では、ユークリッドアルゴリズムのビット単位の複雑さはO(loga)^ 3で与えられていますが、O(loga)^ 2というより厳密な境界が存在します。

考慮してください。 r0 = a、r1 = b、r0 = q1.r1 + r2 。 。 、ri-1 = qi.ri + ri + 1、。 。 。 、rm-2 = qm-1.rm-1 + rm rm-1 = qm.rm

a = r0> = b = r1> r2> r3 ...> rm-1> rm> 0 ..........(1)

rmはaとbの最大公約数です。

Koblitzの本(数論と暗号学のコース)の主張により、次のことが証明できます:ri + 1 <(ri-1)/ 2 .................( 2)

再びKoblitzで、kビットの正の整数をlビットの正の整数で除算するのに必要なビット演算の数(k> = lと仮定)は、次のようになります。 .............(3)

(1)および(2)により、分割数はO(loga)であるため、(3)により、総複雑度はO(loga)^ 3です。

これは、Koblitzでの発言により、O(loga)^ 2に削減される可能性があります。

ki = logri +1を検討してください

(1)および(2)により、i = 0,1、...、m-2、m-1のki + 1 <= kiおよびi = 0のki + 2 <=(ki)-1 、1、...、m-2

(3)m個のディビジョンの総コストは、SUM [(ki-1)-((ki)-1))] * kiによって制限されます。i= 0,1,2、..、mの場合

再配置:SUM [(ki-1)-((ki)-1))] * ki <= 4 * k0 ^ 2

したがって、ユークリッドのアルゴリズムのビット単位の複雑さはO(loga)^ 2です。

0
esra