18

たとえば、n 番目のフィボナッチ数を計算するコードを見てください。

fib(int n)
{
    if(n==0 || n==1)
        return 1;
    return fib(n-1) + fib(n-2);
}

このコードの問題は、(ほとんどのコンピューターで) 15 を超える数値に対してスタック オーバーフロー エラーが発生することです。

fib(10) を計算しているとします。このプロセスでは、 fib(5) が何度も計算されるとします。これをメモリに保存して高速に取得し、再帰の速度を上げる方法はありますか?

ほとんどすべての問題で使用できる一般的な手法を探しています。

4

18 に答える 18

17

はい、あなたの洞察は正しいです。これは動的計画法と呼ばれます。これは通常、一般的なメモリ ランタイムのトレードオフです。

fibo の場合、すべてをキャッシュする必要さえありません。

[編集] 質問の著者は、フィボナッチを計算する方法ではなく、キャッシュする一般的な方法を探しているようです。この答えを得るには、ウィキペディアを検索するか、他のポスターのコードを見てください。これらの答えは、時間と記憶において直線的です。

**これは線形時間アルゴリズム O(n)、メモリ内定数 **

in OCaml:

let rec fibo n = 
    let rec aux = fun
        | 0 -> (1,1)
        | n -> let (cur, prec) = aux (n-1) in (cur+prec, cur)
    let (cur,prec) = aux n in prec;;



in C++:

int fibo(int n) {
    if (n == 0 ) return 1;
    if (n == 1 ) return 1;
    int p = fibo(0);
    int c = fibo(1);
    int buff = 0;
    for (int i=1; i < n; ++i) {
      buff = c;
      c = p+c;
      p = buff;
    };
    return c;
};

これは線形時間で実行されます。しかし、ログは実際に可能です!!! Roo のプログラムも線形ですが、かなり遅く、メモリを消費します。

これが対数アルゴリズム O(log(n)) です

対数時間アルゴリズム (はるかに高速) については、次の方法があります。u(n)、u(n-1) がわかっている場合、u(n+1)、u(n) の計算は、マトリックスの適用:

| u(n+1) |  = | 1 1 | | u(n)   |
| u(n)   |    | 1 0 | | u(n-1) |    

あなたが持っているように:

| u(n)    |  = | 1 1 |^(n-1) | u(1) | = | 1 1 |^(n-1) | 1 |
| u(n-1)  |    | 1 0 |       | u(0) |   | 1 0 |       | 1 |

行列の指数の計算は対数的に複雑です。アイデアを再帰的に実装するだけです:

M^(0)    = Id
M^(2p+1) = (M^2p) * M
M^(2p)   = (M^p) * (M^p)  // of course don't compute M^p twice here.

それを対角化することもできます (それほど難しくありません)。黄金数とその固有値の共役が見つかり、その結果から u(n) の正確な数式が得られます。これらの固有値の累乗が含まれているため、複雑さは依然として対数になります。

Fibo は、動的計画法を説明するための例としてよく取り上げられますが、ご覧のとおり、実際には関係ありません。

@ジョン:ハッシュとは何の関係もないと思います。

@John2: マップは少し一般的だと思いませんか? フィボナッチの場合、すべてのキーが連続しているため、ベクトルが適切です。ここでも、フィボ シーケンスを計算するはるかに高速な方法があります。そこにある私のコード サンプルを参照してください。

于 2008-08-23T04:24:57.807 に答える
7

これはメモ化と呼ばれ、最近Matthew Podwysockiが投稿したメモ化に関する非常に優れた記事があります。フィボナッチを使用してそれを例証します。また、C# のコードも示します。ここでそれを読んでください

于 2008-08-23T04:35:51.040 に答える
5

C# を使用していて、PostSharpを使用できる場合は、コードの簡単なメモ化の側面を次に示します。

[Serializable]
public class MemoizeAttribute : PostSharp.Laos.OnMethodBoundaryAspect, IEqualityComparer<Object[]>
{
    private Dictionary<Object[], Object> _Cache;

    public MemoizeAttribute()
    {
        _Cache = new Dictionary<object[], object>(this);
    }

    public override void OnEntry(PostSharp.Laos.MethodExecutionEventArgs eventArgs)
    {
        Object[] arguments = eventArgs.GetReadOnlyArgumentArray();
        if (_Cache.ContainsKey(arguments))
        {
            eventArgs.ReturnValue = _Cache[arguments];
            eventArgs.FlowBehavior = FlowBehavior.Return;
        }
    }

    public override void OnExit(MethodExecutionEventArgs eventArgs)
    {
        if (eventArgs.Exception != null)
            return;

        _Cache[eventArgs.GetReadOnlyArgumentArray()] = eventArgs.ReturnValue;
    }

    #region IEqualityComparer<object[]> Members

    public bool Equals(object[] x, object[] y)
    {
        if (Object.ReferenceEquals(x, y))
            return true;

        if (x == null || y == null)
            return false;

        if (x.Length != y.Length)
            return false;

        for (Int32 index = 0, len = x.Length; index < len; index++)
            if (Comparer.Default.Compare(x[index], y[index]) != 0)
                return false;

        return true;
    }

    public int GetHashCode(object[] obj)
    {
        Int32 hash = 23;

        foreach (Object o in obj)
        {
            hash *= 37;
            if (o != null)
                hash += o.GetHashCode();
        }

        return hash;
    }

    #endregion
}

これを使用したフィボナッチ実装のサンプルを次に示します。

[Memoize]
private Int32 Fibonacci(Int32 n)
{
    if (n <= 1)
        return 1;
    else
        return Fibonacci(n - 2) + Fibonacci(n - 1);
}
于 2008-08-23T12:54:54.603 に答える
4

C++ でのすばやく汚いメモ化:

再帰メソッドtype1 foo(type2 bar) { ... }は、 で簡単にメモ化できmap<type2, type1> Mます。

// your original method
int fib(int n)
{
    if(n==0 || n==1)
        return 1;
    return fib(n-1) + fib(n-2);
}

// with memoization
map<int, int> M = map<int, int>();
int fib(int n)
{
    if(n==0 || n==1)
        return 1;

    // only compute the value for fib(n) if we haven't before
    if(M.count(n) == 0)
        M[n] = fib(n-1) + fib(n-2);

    return M[n];
}

編集: @Konrad Rudolph
Konrad は、 std::map はここで使用できる最速のデータ構造ではないことを指摘しています。確かに、avector<something>は a よりも高速であるmap<int, something>必要があります (ただし、関数の再帰呼び出しへの入力がこの場合のように連続する整数でない場合は、より多くのメモリが必要になる可能性があります) が、マップは一般的に使用すると便利です。

于 2008-08-23T12:07:18.773 に答える
2

ウィキペディアによると、Fib(0) は 0 である必要がありますが、問題ではありません。

for サイクルを使用した単純な C# ソリューションを次に示します。

ulong Fib(int n)
{
  ulong fib = 1;  // value of fib(i)
  ulong fib1 = 1; // value of fib(i-1)
  ulong fib2 = 0; // value of fib(i-2)

  for (int i = 0; i < n; i++)
  {
    fib = fib1 + fib2;
    fib2 = fib1;
    fib1 = fib;
  }

  return fib;
}

再帰を末尾再帰に変換してからループするのはかなり一般的なトリックです。詳細については、たとえばこの講義(ppt) を参照してください。

于 2008-08-23T09:18:38.163 に答える
1

再帰、パーシャル、カリー化、メモ化などの C# プログラマ向けのもう 1 つの優れたリソースは、しばらく投稿していませんが、Wes Dyer のブログです。彼はメモ化をよく説明しており、具体的なコード例をここに示しています: http://blogs.msdn.com/wesdyer/archive/2007/01/26/function-memoization.aspx

于 2008-11-19T00:44:25.440 に答える
1

他の人があなたの質問に適切かつ正確に答えています - あなたはメモ化を探しています.

末尾呼び出しの最適化を備えたプログラミング言語(主に関数型言語) は、スタック オーバーフローなしで再帰の特定のケースを実行できます。トリックはありますが、フィボナッチの定義に直接適用されるわけではありません..

あなたの質問の言い回しは、私に興味深いアイデアを考えさせました..スタックフレームのサブセットのみを保存し、必要に応じて再構築することにより、純粋な再帰関数のスタックオーバーフローを回避します..本当に役立つのは、いくつかのケースだけです. アルゴリズムがリターンではなくコンテキストにのみ条件付きで依存している場合、および/または速度ではなくメモリを最適化している場合。

于 2008-08-24T02:34:04.757 に答える
1

@ESRogs:

std::mapルックアップはO (log n ) であり、ここでは遅くなります。ベクトルを使用することをお勧めします。

vector<unsigned int> fib_cache;
fib_cache.push_back(1);
fib_cache.push_back(1);

unsigned int fib(unsigned int n) {
    if (fib_cache.size() <= n)
        fib_cache.push_back(fib(n - 1) + fib(n - 2));

    return fib_cache[n];
}
于 2008-08-23T12:16:51.260 に答える
1

マップを使用してみてください。n がキーで、対応するフィボナッチ数が値です。

@ポール

情報をありがとう。私はそれを知りませんでした。あなたが言及したウィキペディアのリンクから:

すでに計算された値を保存するこの手法は、メモ化と呼ばれます

ええ、私はすでにコードを見ました(+1)。:)

于 2008-08-23T04:15:39.780 に答える
1

これは何語ですか?Cでは何もオーバーフローしません...また、ヒープにルックアップテーブルを作成するか、マップを使用できます

于 2008-08-23T04:19:28.990 に答える
1

キャッシングは一般に、この種のことには良い考えです。フィボナッチ数は一定であるため、計算後に結果をキャッシュできます。簡単な c/pseudocode の例

class fibstorage {


    bool has-result(int n) { return fibresults.contains(n); }
    int get-result(int n) { return fibresult.find(n).value; }
    void add-result(int n, int v) { fibresults.add(n,v); }

    map<int, int>   fibresults;

}


fib(int n ) {
    if(n==0 || n==1)
            return 1;

    if (fibstorage.has-result(n)) {
        return fibstorage.get-result(n-1);
    }

    return ( (fibstorage.has-result(n-1) ? fibstorage.get-result(n-1) : fib(n-1) ) +
             (fibstorage.has-result(n-2) ? fibstorage.get-result(n-2) : fib(n-2) )
           );
}


calcfib(n) {
    v = fib(n);
    fibstorage.add-result(n,v);
}

すべての再帰が 3 回のルックアップになるため、これは非常に遅くなりますが、これは一般的な考え方を示しているはずです。

于 2008-08-23T04:23:28.280 に答える
1

これは意図的に選ばれた例ですか?(例: テストしたい極端なケース)

現在は O(1.6^n) であるため、この問題の一般的なケース (値のキャッシュなど) の処理に関する回答を探しているだけで、誤って貧弱なコードを書いているだけではないことを確認したいだけです:D

この特定のケースを見ると、次のようになります。

var cache = [];
function fib(n) {
    if (n < 2) return 1;
    if (cache.length > n) return cache[n];
    var result = fib(n - 2) + fib(n - 1);
    cache[n] = result;
    return result;
}

最悪の場合、O(n) に退化します:D

[編集: * は + :D と等しくない]

[さらに別の編集: Haskell バージョン (私はマゾヒストか何かだから)

fibs = 1:1:(zipWith (+) fibs (tail fibs))
fib n = fibs !! n

]

于 2008-08-23T04:35:53.897 に答える
1

Mathematica には、ハッシュと関数呼び出しが同じ構文を使用するという事実に依存して、メモ化を行うための特に洗練された方法があります。

fib[0] = 1;
fib[1] = 1;
fib[n_] := fib[n] = fib[n-1] + fib[n-2]

それでおしまい。fib[0] と fib[1] をすぐにキャッシュ (メモ化) し、必要に応じて残りをキャッシュします。パターン マッチング関数呼び出しのルールは、より一般的な定義の前に、より具体的な定義を常に使用するようになっています。

于 2008-10-04T07:12:22.370 に答える
0

@lassevk:

これはすごいです。HigherOrderPerlでのメモ化について読んだ後、頭の中で考えていたのとまったく同じです。私が役立つと思う2つの追加:

  1. キャッシュへのキーを生成するために使用される静的メソッドまたはメンバーメソッドを指定するためのオプションのパラメーター。
  2. ディスクまたはデータベースでバックアップされたキャッシュを使用できるように、キャッシュオブジェクトを変更するオプションの方法。

属性を使用してこの種のことを行う方法はわかりませんが(または、この種の実装で可能かどうか)、私は試してみて理解する予定です。

(オフトピック:これをコメントとして投稿しようとしましたが、コメントの許容長が非常に短いことに気づかなかったため、これは「回答」として実際には適合しません)

于 2008-12-13T09:46:58.790 に答える
0

他の投稿者が示しているように、メモ化は速度と引き換えにメモリを交換する標準的な方法です。任意の関数にメモ化を実装するための擬似コードを次に示します (関数に副作用がない場合)。

初期機能コード:

 function (parameters)
      body (with recursive calls to calculate result)
      return result

これは次のように変換する必要があります

 function (parameters)
      key = serialized parameters to string
      if (cache[key] does not exist)  {
           body (with recursive calls to calculate result)
           cache[key] = result
      }
      return cache[key]
于 2008-08-24T22:47:05.513 に答える
0

ところで、Perl にはmemoizeモジュールがあり、指定したコード内の任意の関数に対してこれを行います。

# Compute Fibonacci numbers
sub fib {
      my $n = shift;
      return $n if $n < 2;
      fib($n-1) + fib($n-2);
}

この関数をメモ化するには、プログラムを開始するだけです

use Memoize;
memoize('fib');
# Rest of the fib function just like the original version.
# Now fib is automagically much faster ;-)
于 2008-08-24T22:49:49.360 に答える
0

もしあなたがSchemeのような第一級の関数を持つ言語を使っているなら、初期アルゴリズムを変更せずにメモ化を追加することができます:

(define (memoize fn)
  (letrec ((get (lambda (query) '(#f)))
           (set (lambda (query value)
                  (let ((old-get get))
                    (set! get (lambda (q)
                                (if (equal? q query)
                                    (cons #t value)
                                    (old-get q))))))))
    (lambda args
      (let ((val (get args)))
        (if (car val)
            (cdr val)
            (let ((ret (apply fn args)))
              (set args ret)
              ret))))))


(define fib (memoize (lambda (x)
                       (if (< x 2) x
                           (+ (fib (- x 1)) (fib (- x 2)))))))

最初のブロックはメモ化機能を提供し、2 番目のブロックはその機能を使用するフィボナッチ数列です。これには、O(n) ランタイムがあります (メモ化を使用しないアルゴリズムの O(2^n) とは対照的に)。

注: 提供されているメモ化機能は、クロージャーのチェーンを使用して以前の呼び出しを探します。最悪の場合、これは O(n) になる可能性があります。ただし、この場合、目的の値は常にチェーンの最上部にあり、O(1) ルックアップが保証されます。

于 2008-08-23T04:36:09.607 に答える
0

このコードの問題は、(ほとんどのコンピューターで) 15 を超える数値に対してスタック オーバーフロー エラーが発生することです。

本当に?どのコンピュータを使用していますか? 44 で時間がかかっていますが、スタックはオーバーフローしていません。実際、スタックがオーバーフローする前に (Fibbonaci(46))、整数が保持できるよりも大きな値 (符号なしで最大 40 億、符号付きで最大 20 億) を取得しようとしています。

ただし、これはあなたがやりたいことにはうまくいきます(非常に高速に実行されます)

class Program
{
    public static readonly Dictionary<int,int> Items = new Dictionary<int,int>();
    static void Main(string[] args)
    {
        Console.WriteLine(Fibbonacci(46).ToString());
        Console.ReadLine();
    }

    public static int Fibbonacci(int number)
    {
        if (number == 1 || number == 0)
        {
            return 1;
        }

        var minus2 = number - 2;
        var minus1 = number - 1;

        if (!Items.ContainsKey(minus2))
        {
            Items.Add(minus2, Fibbonacci(minus2));
        }

        if (!Items.ContainsKey(minus1))
        {
            Items.Add(minus1, Fibbonacci(minus1));
        }

        return (Items[minus2] + Items[minus1]);
    }
}
于 2008-08-23T06:18:55.740 に答える