web-dev-qa-db-ja.com

フィボナッチの高速化

私はフィボナッチのアルゴリズムの簡単な実装を記述し、それから高速化にする必要がありました。

これが私の最初の実装です

public class Fibonacci {

    public static long getFibonacciOf(long n) {
        if (n== 0) {
            return 0;
        } else if (n == 1) {
            return 1;
        } else {
            return getFibonacciOf(n-2) + getFibonacciOf(n-1);
        }
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner (System.in);
        while (true) {
            System.out.println("Enter n :");
            long n = scanner.nextLong();
            if (n >= 0) {
                long beginTime = System.currentTimeMillis();
                long fibo = getFibonacciOf(n);
                long endTime = System.currentTimeMillis();

                long delta = endTime - beginTime;

                System.out.println("F(" + n + ") = " + fibo + " ... computed     in " + delta + " milliseconds");
            } else {
                break;

            }
        }

    }

}

ご覧のとおり、私はSystem.currentTimeMillis()を使用して、フィボナッチの計算中に経過した時間の簡単な測定値を取得しています。

この実装は急速に指数関数的に遅くなります次の図に示すように

simple version of fibonacci's algorithm

単純な最適化のアイデアがあります。以前の値をHashMapに配置し、毎回再計算する代わりに、HashMapが存在する場合はそれらを単純に戻す。存在しない場合は、HashMapに配置します。

これが新しいバージョンのコードです

public class FasterFibonacci {

    private static Map<Long, Long> previousValuesHolder;
    static {
        previousValuesHolder = new HashMap<Long, Long>();
        previousValuesHolder.put(Long.valueOf(0), Long.valueOf(0));
        previousValuesHolder.put(Long.valueOf(1), Long.valueOf(1));
    }
    public static long getFibonacciOf(long n) {
        if (n== 0) {

            return 0;
        } else if (n == 1) {
            return 1;
        } else {
            if (previousValuesHolder.containsKey(Long.valueOf(n))) {
                return previousValuesHolder.get(n);
            } {

                long newValue = getFibonacciOf(n-2) + getFibonacciOf(n-1);
                previousValuesHolder.put(Long.valueOf(n),     Long.valueOf(newValue));
                return newValue;
            }

        }
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner (System.in);
        while (true) {
            System.out.println("Enter n :");
            long n = scanner.nextLong();
            if (n >= 0) {
                long beginTime = System.currentTimeMillis();
                long fibo = getFibonacciOf(n);
                long endTime = System.currentTimeMillis();

                long delta = endTime - beginTime;

                System.out.println("F(" + n + ") = " + fibo + " ... computed     in " + delta + " milliseconds");
            } else {
                break;

            }
        }

    }

この変更により、コンピューティングが非常に高速になります。 2から103までのすべての値をすぐに計算し、F(104)でlongオーバーフローを取得します(Gives me F(104) = -7076989329685730859、これは間違っています)。私はそれが非常に速いので、**コードに誤りがあるのではないかと思います(チェックしてくれてありがとう、教えてください)**。 2番目の写真をご覧ください。

Faster Fibonacci

私の高速フィボナッチのアルゴリズムの実装は正しいですか(最初のバージョンと同じ値を取得しているようですが、最初のバージョンが遅すぎたため、F(75)などの大きな値を計算できませんでした)?より高速にするために他にどのような方法を使用できますか?それとも、それをより速くするためのより良い方法はありますか? ** longオーバーフロー**を取得せずに、より大きな値(150、200など)のフィボナッチを計算するにはどうすればよいですか?速いように見えますが、限界までプッシュしたいと思います。 Abrash氏が「最高のオプティマイザーは両耳の間にあります」と言ったのを覚えているので、改善できると信じています。手伝ってくれてありがとう

[Edition Note:]けれども this の質問は私の質問の主要なポイントの1つを扱っていますが、上から追加の問題があることがわかります。

30
alainlompo

動的プログラミング

アイデア:同じ値を複数回再計算する代わりに、計算された値を保存し、それを使用しながら使用します。

f(n)=f(n-1)+f(n-2) with f(0)= 0、f(1)= 1。したがって、f(n-1)を計算した時点で、f(n)の値を格納すると、f(n)を簡単に計算できます。およびf(n-1)。

最初にBignumの配列を取得しましょう。 A [1..200]。 -1に初期化します。

擬似コード

_fact(n)
{
if(A[n]!=-1) return A[n];
A[0]=0;
A[1]=1;
for i=2 to n
  A[i]= addition of A[i],A[i-1];
return A[n]
}
_

これはO(n)時間で実行されます。自分でチェックしてください。

この手法は、memoizationとも呼ばれます。

アイデア

動的プログラミング(通常DPと呼ばれる)は、特定のクラスの問題を解決するための非常に強力な手法です。アプローチとシンプルな思考の非常にエレガントな定式化が要求され、コーディングの部分は非常に簡単です。アイデアは非常にシンプルです。与えられた入力で問題を解決した場合、今後の参照のために結果を保存して、同じ問題を再度解決しないようにします。まもなく「過去を記憶する」。

与えられた問題がより小さな副問題に分割でき、これらのより小さな副問題がさらに小さなものに分割され、このプロセスで_over-lappping subproblems_を観察する場合、その大きなヒントDP。また、サブ問題の最適な解決策は、特定の問題の最適な解決策に貢献します(_Optimal Substructure Property_と呼ばれます)。

これを行うには2つの方法があります。

1.)トップダウン:与えられた問題を分解することで解決を開始します。問題がすでに解決されていることがわかった場合は、保存された回答を返します。解決されていない場合は、解決して回答を保存します。これは通常、簡単に考えられ、非常に直感的です。これはメモ化と呼ばれます。 (このアイデアを使用しました)。

2.)ボトムアップ:問題を分析し、サブ問題が解決される順序を確認し、些細なサブ問題から特定の問題に向かって解決を開始します。このプロセスでは、問題を解決する前にサブ問題が解決されることが保証されます。これはDynamic Programmingと呼ばれます。 (MinecraftShamrockはこのアイデアを使用しました)


さらにあります!

(これを行う他の方法)

より良い解決策を得るための私たちの探求がここで終わるわけではありません。別のアプローチが表示されます

_recurrence relation_の解決方法を知っている場合、この関係の解決策が見つかります

f(n)=f(n-1)+f(n-2) given f(0)=0,f(1)=1

あなたはそれを解いた後、式に到達します-

f(n)= (1/sqrt(5))((1+sqrt(5))/2)^n - (1/sqrt(5))((1-sqrt(5))/2)^n

よりコンパクトな形式で記述できます

f(n)=floor((((1+sqrt(5))/2)^n) /sqrt(5) + 1/2)

複雑

O(logn)演算でパワーの数値を取得できます。 二乗によるべき乗 を学ぶ必要があります。

[〜#〜] edit [〜#〜]:これは必ずしもフィボナッチ数が見つかることを意味するわけではないことを指摘するのは良いことですO(logn)で。実際に、私たちが線形に計算する必要がある桁数。おそらく、数の階乗はO(logn)の時間で計算できるという間違った考えを主張していると私が述べた位置のために。 [バクルイ、MinecraftShamrockはこれについてコメントしました]

29
user2736738

n番目のフィボナッチ数を非常に頻繁に計算する必要がある場合は、amalsomの答えを使用することをお勧めします。

しかし、非常に大きなフィボナッチ数を計算したい場合、allより小さなフィボナッチ数を保存しているため、メモリが不足します。次の擬似コードは、最後の2つのフィボナッチ数のみをメモリに保持します。つまり、必要なメモリがはるかに少なくなります。

fibonacci(n) {
    if n = 0: return 0;
    if n = 1: return 1;
    a = 0;
    b = 1;
    for i from 2 to n: {
        sum = a + b;
        a = b;
        b = sum;
    }
    return b;
}

分析
これにより、非常に高いメモリ消費で非常に高いフィボナッチ数を計算できます。ループが繰り返されるときに、O(n)timeがありますn-1回。スペースの複雑さも興味深い:n番目のフィボナッチ数の長さはO(n)であり、簡単に表示できます。
Fn <= 2 * Fn-1
これは、n番目のフィボナッチ数が最大で前任者の2倍であることを意味します。バイナリで数値を2倍にすることは、必要なビット数を1つ増やす1つの左シフトと同等です。したがって、n番目のフィボナッチ数を表すには、最大でO(n)スペースが必要です。メモリには最大3つの連続したフィボナッチ数があり、O(n) + O(n-1) + O(n-2) = O(n )合計スペース消費量。これとは対照的に、メモ化アルゴリズムは常にメモリ内の最初のn個のフィボナッチ数を保持するため、O(n) + O(n-1) + O(n-2) + ... + O(1) = O(n ^ 2)スペース消費。

だからどちらの方法を使うべきか?
すべての下位フィボナッチ数をメモリに保持する唯一の理由は、フィボナッチ数が非常に頻繁に必要な場合です。これは、時間とメモリ消費のバランスをとるという問題です。

15

フィボナッチ再帰から逃げて、アイデンティティを使用する

(F(2n), F(2n-1)) = (F(n)^2 + 2 F(n) F(n-1), F(n)^2+F(n-1)^2)
(F(2n+1), F(2n)) = (F(n+1)^2+F(n)^2, 2 F(n+1) F(n) - F(n)^2)

これにより、mの半分のサイズのkに対して(F(k + 1)、F(m))で(F(m + 1)、F(k))を計算できます。 2で除算するためのビットシフトを反復して記述します。これにより、整数演算内に完全にとどまりながら、2乗によるべき乗の理論上のO(log n)速度が得られます。 (まあ、O(log n)算術演算。おおよそnビットの数値を扱うので、大きな整数ライブラリへの切り替えを強制されると、O(log n)時間にはなりません。 )、整数データ型をオーバーフローさせますが、最大2 ^(31)になります。)

(JavaをJavaで実装するのに十分覚えていないことをおologiesび申し上げます。誰でも自由に編集できます。)

13
David E Speyer
  • フィボナッチ(0)= 0
  • フィボナッチ(1)= 1
  • フィボナッチ(n)=フィボナッチ(n-1)+フィボナッチ(n-2)、n> = 2の場合

通常、フィボナッチ数を計算するには2つの方法があります。

  1. 再帰

    _public long getFibonacci(long n) {
      if(n <= 1) {
        return n;
      } else {
        return getFibonacci(n - 1) + getFibonacci(n - 2);
      }
    }
    _

    この方法は直感的で理解しやすく、計算されたフィボナッチ数を再利用しないため、時間の複雑さはO(2^n)程度ですが、計算結果を保存しないため、スペースを大幅に節約できます。複雑さはO(1)です。

  2. 動的プログラミング

    _public long getFibonacci(long n) {
      long[] f = new long[(int)(n + 1)];
      f[0] = 0;
      f[1] = 1;
      for(int i=2;i<=n;i++) {
        f[i] = f[i - 1] + f[i - 2];
      }
      return f[(int)n];
    }
    _

    この メモ化 はフィボナッチ数を計算し、次の数を計算するときに再利用します。時間の複雑さはO(n)ですが、スペースの複雑さはO(n)です。スペースの複雑さを最適化できるかどうかを調べてみましょう... f(i)f(i - 1)f(i - 2)のみを必要とするため、計算されたすべてのフィボナッチ数を保存する必要はありません。

    より効率的な実装は

    _public long getFibonacci(long n) {
      if(n <= 1) {
        return n;
      }
      long x = 0, y = 1;
      long ans;
      for(int i=2;i<=n;i++) {
        ans = x + y;
        x = y;
        y = ans;
      }
      return ans;
    }
    _

    時間の複雑さO(n)、およびスペースの複雑さO(1)

追加:フィボナッチ数が驚くほど急速に増加するため、longは以下よりも小さい値しか処理できません100フィボナッチ数。 Javaでは、BigIntegerを使用して、より多くのフィボナッチ数を保存できます。

8
coderz

多数のfib(n)結果を事前計算し、それらをアルゴリズム内のルックアップテーブルとして保存します。バム、無料の「スピード」

fib(101)を計算する必要があり、すでにfibs 0〜100が保存されている場合、これはfib(1)を計算するのと同じです。

これはこの宿題が探しているものではない可能性がありますが、それは完全に合法な戦略であり、基本的にアルゴリズムの実行からさらに離れてキャッシングのアイデアが抽出されます。最初の100 fibsを頻繁に計算する可能性が高く、本当に高速に実行する必要があることがわかっている場合、O(1)ほど高速ではありません。したがって、これらの値を完全に帯域外で計算して保存し、後で検索できるようにします。

もちろん、値を計算するときに値もキャッシュしてください:)重複した計算は無駄です。

7
MushinNoShin

ここでは、再帰ではなく反復アプローチを使用したコードを抜粋しています。

出力例

Enter n: 5
F(5) = 5 ... computed in 1 milliseconds
Enter n: 50
F(50) = 12586269025 ... computed in 0 milliseconds
Enter n: 500
F(500) = ...4125 ... computed in 2 milliseconds
Enter n: 500
F(500) = ...4125 ... computed in 0 milliseconds
Enter n: 500000
F(500000) = ...453125 ... computed in 5,718 milliseconds
Enter n: 500000
F(500000) = ...453125 ... computed in 0 milliseconds

...では、見やすくするために結果の一部が省略されています。

コードスニペット

public class CachedFibonacci {
    private static Map<BigDecimal, BigDecimal> previousValuesHolder;
    static {
        previousValuesHolder = new HashMap<>();
        previousValuesHolder.put(BigDecimal.ZERO, BigDecimal.ZERO);
        previousValuesHolder.put(BigDecimal.ONE, BigDecimal.ONE);
    }

    public static BigDecimal getFibonacciOf(long number) {
        if (0 == number) {
            return BigDecimal.ZERO;
        } else if (1 == number) {
            return BigDecimal.ONE;
        } else {
            if (previousValuesHolder.containsKey(BigDecimal.valueOf(number))) {
                return previousValuesHolder.get(BigDecimal.valueOf(number));
            } else {
                BigDecimal olderValue = BigDecimal.ONE,
                        oldValue = BigDecimal.ONE,
                        newValue = BigDecimal.ONE;

                for (int i = 3; i <= number; i++) {
                    newValue = oldValue.add(olderValue);
                    olderValue = oldValue;
                    oldValue = newValue;
                }
                previousValuesHolder.put(BigDecimal.valueOf(number), newValue);
                return newValue;
            }
        }
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.print("Enter n: ");
            long inputNumber = scanner.nextLong();
            if (inputNumber >= 0) {
                long beginTime = System.currentTimeMillis();
                BigDecimal fibo = getFibonacciOf(inputNumber);
                long endTime = System.currentTimeMillis();
                long delta = endTime - beginTime;

                System.out.printf("F(%d) = %.0f ... computed in %,d milliseconds\n", inputNumber, fibo, delta);
            } else {
                System.err.println("You must enter number > 0");
                System.out.println("try, enter number again, please:");
                break;
            }
        }
    }
}

このアプローチは、再帰バージョンよりもはるかに高速に実行されます。

このような状況では、反復的なメソッドは少し速くなる傾向があります。これは、各再帰メソッド呼び出しに一定のプロセッサー時間がかかるためです。原則として、スマートコンパイラは、単純なパターンに従う場合、再帰的なメソッド呼び出しを回避できますが、ほとんどのコンパイラはそうしません。その観点から、反復解が好ましい。

5
nazar_art

しばらく前に同様のアプローチを採用していたので、別の最適化ができることに気付きました。

2つの大きな連続した答えを知っている場合、これを出発点として使用できます。たとえば、F(100)およびF (101)、そしてF(104)の計算はおよそ計算が難しい(*)F(4)に基づいてF(0)およびF(1).

反復計算は、キャッシュ再帰を使用して同じことを行うのと同じくらい効率的な計算ですが、使用するメモリは少なくなります。

いくつかの合計を行った後、私はまた、任意の_z < n_について:

F(n)=F(z) * F(n-z) + F(z-1) * F(n-z-1)

Nが奇数で、z=(n+1)/2を選択した場合、これは

F(n)= F(z)^ 2 + F(z-1)^ 2

私はまだ見つけていない方法でこれを使用できるはずであり、上記の情報を使用して操作数でF(n)を見つけることができるはずですに等しい:

(上記のように)n回の倍増のビット数+ n回の加算の_1_ビットの数。 104の場合、これは(7ビット、3 '1'ビット)= 14乗算(2乗)、10加算です。

(*)2つの数値の加算を想定すると、2つの数値のサイズに関係なく、同じ時間がかかります。

4
AMADANON Inc.

[〜#〜] o [〜#〜](logn)(ループがlog n回):

/* 
 * Fast doubling method
 * F(2n) = F(n) * (2*F(n+1) - F(n)).
 * F(2n+1) = F(n+1)^2 + F(n)^2.
 * Adapted from:
 *    https://www.nayuki.io/page/fast-fibonacci-algorithms
 */
private static long getFibonacci(int n) {
    long a = 0;
    long b = 1;
    for (int i = 31 - Integer.numberOfLeadingZeros(n); i >= 0; i--) {
        long d = a * ((b<<1) - a);
        long e = (a*a) + (b*b);
        a = d;
        b = e;
        if (((n >>> i) & 1) != 0) {
            long c = a+b;
            a = b;
            b = c;
        }
    }
    return a;
}

私はここで(従来どおり)、ビット数に関係なく一定の時間である乗算/加算/任意の操作、つまり固定長のデータ型が使用されると仮定しています。

このページ は、これが最も速いいくつかの方法を説明しています。読みやすくするために、BigIntegerを使用しないように翻訳しました。 BigIntegerバージョンは次のとおりです。

/* 
 * Fast doubling method.
 * F(2n) = F(n) * (2*F(n+1) - F(n)).
 * F(2n+1) = F(n+1)^2 + F(n)^2.
 * Adapted from:
 *    http://www.nayuki.io/page/fast-fibonacci-algorithms
 */
private static BigInteger getFibonacci(int n) {
    BigInteger a = BigInteger.ZERO;
    BigInteger b = BigInteger.ONE;
    for (int i = 31 - Integer.numberOfLeadingZeros(n); i >= 0; i--) {
        BigInteger d = a.multiply(b.shiftLeft(1).subtract(a));
        BigInteger e = a.multiply(a).add(b.multiply(b));
        a = d;
        b = e;
        if (((n >>> i) & 1) != 0) {
            BigInteger c = a.add(b);
            a = b;
            b = c;
        }
    }
    return a;
}
4
abligh