ボトムアップとトップダウンの違いは何ですか?


177

ボトムアップ(動的プログラミング)のアプローチは、「小さい」部分問題を見て最初に構成され、そしてより小さい問題に対する解決策を使用して、より大きな部分問題を解決します。

トップダウンはあなたが前部分問題の解を計算している場合は、「自然な形」とチェックで問題を解決することにあります。

私は少し混乱しています。これら2つの違いは何ですか?


回答:


247

rev4:ユーザーSammaronによる非常に雄弁なコメントは、おそらく、この回答は以前はトップダウンとボトムアップを混同していたと述べています。元々この回答(rev3)と他の回答では「ボトムアップはメモ化」(「サブ問題を想定」)でしたが、逆の場合もあります(つまり、「トップダウン」は「サブ問題を想定」と「ボトムアップ」は「サブ問題を構成する」かもしれません)。以前、私はメモ化について、動的プログラミングのサブタイプとは異なる種類の動的プログラミングであることを読みました。購読していないにもかかわらず、私はその視点を引用していた。文献に適切な参照が見つかるまで、この回答を用語にとらわれないように書き直しました。また、この回答をコミュニティーWikiに変換しました。学術的な情報源を優先してください。参考文献一覧:} {文献:5 }

要約

動的プログラミングとは、重複する作業の再計算を回避する方法で計算を順序付けることです。主な問題(サブ問題のツリーのルート)、およびサブ問題(サブツリー)があります。サブ問題は通常繰り返され、重複します。

たとえば、お気に入りのフィボナッチの例を考えてみましょう。単純な再帰呼び出しを行った場合、これはサブ問題の完全なツリーです。

TOP of the tree
fib(4)
 fib(3)...................... + fib(2)
  fib(2)......... + fib(1)       fib(1)........... + fib(0)
   fib(1) + fib(0)   fib(1)       fib(1)              fib(0)
    fib(1)   fib(0)
BOTTOM of the tree

(他のいくつかのまれな問題では、このツリーはいくつかのブランチで無限であり、終了しないことを表している可能性があります。したがって、ツリーの下部が無限に大きくなることがあります。したがって、明らかにするサブ問題を決定するための戦略/アルゴリズムが必要になる場合があります。)


メモ化、集計

動的プログラミングには、相​​互に排他的ではない少なくとも2つの主要な技法があります。

  • メモ化-これは自由放任のアプローチです。すでにすべての部分問題を計算していて、最適な評価順序が何であるかわからないと仮定します。通常は、ルートから再帰呼び出し(または同等の反復)を実行し、最適な評価順序に近づくことを期待するか、最適な評価順序に到達するのに役立つ証拠を取得します。結果をキャッシュするため、再帰呼び出しで副問題が再計算されないようにし、重複するサブツリーが再計算されないようにします。

    • 例:あなたはフィボナッチ数列を計算している場合fib(100)、あなたはこれを呼ぶだろう、それは呼ぶだろうfib(100)=fib(99)+fib(98)呼び出すことになる、fib(99)=fib(98)+fib(97)、...等...、呼び出すことになりますfib(2)=fib(1)+fib(0)=1+0=1。その後、最終的に解決しますがfib(3)=fib(2)+fib(1)fib(2)キャッシュしているため、再計算する必要はありません。
    • これはツリーの最上部から始まり、葉/サブツリーからルートに向かって戻るサブ問題を評価します。
  • 集計-動的プログラミングを「テーブル充填」アルゴリズムと考えることもできます(通常、多次元ですが、この「テーブル」には非常にまれなケースで非ユークリッドジオメトリがある場合があります*)。これはメモ化に似ていますが、よりアクティブで、1つの追加ステップが含まれます。事前に、計算を行う正確な順序を選択する必要があります。これは、順序が静的である必要があることを意味するのではなく、メモ化よりもはるかに柔軟性があることを意味します。

    • 例:あなたはフィボナッチを実行している場合、あなたがこの順に数字を計算することを選択するかもしれません:fib(2)fib(3)fib(4)...あなたがより簡単に次のものを計算することができるようにすべての値をキャッシュします。テーブルを埋める(別の形式のキャッシング)と考えることもできます。
    • 個人的には「集計」という言葉はあまり聞きませんが、とてもまともな言葉です。一部の人々は、この「動的プログラミング」を考えています。
    • アルゴリズムを実行する前に、プログラマーはツリー全体を検討し、次に、ルートに向かって特定の順序でサブ問題を評価するアルゴリズムを記述します。
    • *脚注:「テーブル」は、それ自体がグリッドのような接続性を持つ長方形のテーブルではない場合があります。むしろ、それはツリーなどのより複雑な構造、または問題の領域に固有の構造(たとえば、マップ上の飛行距離内の都市)、または格子状であるが、上下左右の接続構造など。たとえば、user3290797 は、ツリー内の空白を埋めることに対応する、ツリー内最大独立セットを見つける動的プログラミングの例をリンクしました。

(最も一般的には、「動的プログラミング」パラダイムでは、プログラマーはツリー全体を検討し、その後、必要なプロパティを最適化できるサブ問題を評価するための戦略を実装するアルゴリズムを記述します(通常、時間の複雑さとスペースの複雑さ)戦略は、特定のサブ問題からどこかに開始する必要があり、おそらくそれらの評価の結果に基づいて適応する可能性があります。「動的プログラミング」の一般的な意味では、これらのサブ問題をキャッシュしようとする可能性があります。 、おそらくさまざまなデータ構造のグラフの場合など、微妙な区別を付けてサブ問題を再検討しないようにしてください。多くの場合、これらのデータ構造は配列やテーブルのようにコアにあります。サブ問題の解決策は、もう必要ありません。)

[以前は、この回答はトップダウンとボトムアップの用語について述べていました。完全にではありませんが、これらの用語と全単射の可能性があるメモ化と集計と呼ばれる2つの主要なアプローチが明らかにあります。ほとんどの人が使用する一般的な用語はまだ「動的プログラミング」であり、「動的プログラミング」の特定のサブタイプを指すために「メモ化」と言う人もいます。この答えは、コミュニティが学術論文で適切な参照を見つけることができるまで、どちらがトップダウンかボトムアップかを述べることを拒否します。結局のところ、用語ではなく区別を理解することが重要です。]


長所と短所

コーディングのしやすさ

メモ化はコーディングが非常に簡単であり(通常、*自動的に行う「メモライザ」注釈またはラッパー関数を記述できます)、これが最初のアプローチになるはずです。集計の欠点は、注文を考え出す必要があることです。

*(これは実際に関数を自分で記述している場合、および/または純粋でない/機能しないプログラミング言語でコーディングしている場合にのみ簡単です...たとえば、誰かがすでにコンパイル済みのfib関数をすでに記述している場合、それは必ずそれ自体に再帰呼び出しを行います。これらの再帰呼び出しが新しいメモ化された関数(元のメモ化されていない関数ではない)を呼び出すことを保証せずに、関数を魔法のようにメモ化することはできません。

再帰性

当然ではないかもしれませんが、トップダウンとボトムアップの両方が再帰または反復的なテーブル充填で実装できることに注意してください。

実用上の懸念

メモ化を使用すると、ツリーが非常に深い場合(例:)、fib(10^6)遅延計算をスタックに配置する必要があり、10 ^ 6になるため、スタック領域が不足します。

最適性

サブ問題を訪問する(または試行する)順序が最適でない場合、特にサブ問題を計算する方法が複数ある場合(通常はキャッシングでこれを解決できますが、理論的にはキャッシングで可能性がある場合)一部のエキゾチックなケースではありません)。メモ化は通常、時間の複雑さを空間の複雑さに追加します(たとえば、集計では、Fibで集計を使用するとO(1)スペースを使用できますが、Fibでメモを作成するとO(N)を使用できます。スタックスペース)。

高度な最適化

非常に複雑な問題も行っている場合は、表を作成する以外に方法がない可能性があります(または、メモを目的の場所に導くために、少なくともより積極的な役割を果たします)。また、最適化が絶対的に重要であり、最適化する必要がある状況にある場合、表を作成することで、他の方法では正常化できないメモ化では行われない最適化を実行できます。私の控えめな意見では、通常のソフトウェアエンジニアリングでは、これらの2つのケースはどちらも発生しません。そのため、何か(スタックスペースなど)が集計を必要としない限り、メモ(「回答をキャッシュする関数」)を使用します...技術的には、スタックブローアウトを回避するために、1)許可する言語でスタックサイズの制限を増やすか、2)一定の係数の追加作業を実行してスタックを仮想化します(ick)、


より複雑な例

ここでは、DPの一般的な問題だけでなく、メモと表を興味深いことに区別する、特に関心のある例を示します。たとえば、1つの定式化が他の定式化よりもはるかに簡単な場合や、基本的には集計が必要な最適化がある場合があります。

  • edit-distance [ 4 ] を計算するアルゴリズム。2次元のテーブル充填アルゴリズムの重要な例として興味深い

3
@ coder000001:Pythonの例の場合、グーグルで検索できpython memoization decoratorます。一部の言語では、メモ化パターンをカプセル化するマクロまたはコードを記述できます。メモ化パターンは、「関数を呼び出すのではなく、キャッシュから値を検索する(値がそこにない場合は、最初に計算してキャッシュに追加する)」にすぎません。
ninjagecko

16
これについて言及している人はいないようですが、トップダウンのもう1つの利点は、ルックアップテーブル/キャッシュをまばらにしか作成できないことです。(つまり、実際に必要な場所に値を入力します)。だから、これは簡単なコーディングに加えて長所かもしれません。言い換えると、トップダウンを使用すると、すべてを計算するわけではないため、実際の実行時間を節約できる場合があります(ただし、実行時間は非常に優れていますが、漸近的な実行時間は同じです)。ただし、追加のスタックフレームを保持するために追加のメモリが必要です(ここでも、メモリ使用量は2倍になります(5
InformedA

2
サブソリューションの重複に対するソリューションをキャッシュするトップダウンアプローチは、メモ化と呼ばれる手法であるという印象を受けています。また、テーブルを充填し、ボトムアップ手法は、重複部分問題を再計算することと呼ばれることを回避する集計。これらの手法は、動的プログラミングを使用するときに使用できます。動的プログラミングは、より大きな問題を解決するために副問題を解決することを指します。これはこの回答と矛盾しているようです。この回答では、多くの場所で集計ではなく動的プログラミングを使用しています。誰が正しいのですか?
Sammaron、2015

1
@サマロン:うーん、あなたは良い点を作る。ウィキペディアでソースを確認したはずですが、見つかりません。cstheory.stackexchangeを少しチェックすると、「ボトムアップ」はボトムが事前にわかっていることを意味することに同意します(集計)。「トップダウン」は、サブ問題/サブツリーの解決策であると想定します。あいまいな用語を見つけたとき、私は二重の見方でフレーズを解釈しました(「ボトムアップ」では、サブ問題の解決策を想定して記憶し、「トップダウン」では、どのサブ問題について把握しており、表にまとめることができます)。編集でこれに対処しようとします。
ninjagecko

1
@mgiuffrida:スタック領域は、プログラミング言語によって異なる方法で処理されることがあります。たとえば、Pythonでは、メモ化された再帰的なfibを実行しようとすると、失敗しますfib(513)。ここで、用語が多すぎて邪魔になっていると感じています。1)不要になったサブ問題はいつでも破棄できます。2)不要な部分問題の計算を常に回避できます。3)1と2は、サブ問題を格納する明示的なデータ構造がないと、コーディングがはるかに難しくなる可能性があります。または、関数呼び出し間で制御フローを織り込む必要がある場合は、さらに困難になります(状態または継続が必要になる場合があります)。
ninjagecko 2015

76

トップダウンとボトムアップDPは、同じ問題を解決する2つの異なる方法です。フィボナッチ数を計算するためのメモ化(トップダウン)と動的(ボトムアップ)のプログラミングソリューションについて考えてみましょう。

fib_cache = {}

def memo_fib(n):
  global fib_cache
  if n == 0 or n == 1:
     return 1
  if n in fib_cache:
     return fib_cache[n]
  ret = memo_fib(n - 1) + memo_fib(n - 2)
  fib_cache[n] = ret
  return ret

def dp_fib(n):
   partial_answers = [1, 1]
   while len(partial_answers) <= n:
     partial_answers.append(partial_answers[-1] + partial_answers[-2])
   return partial_answers[n]

print memo_fib(5), dp_fib(5)

個人的には、メモ化はずっと自然だと思います。再帰的な関数を取り、それを機械的なプロセスでメモすることができます(最初の回答をキャッシュで検索して、可能であればそれを返します。それ以外の場合は再帰的に計算してから、戻る前に、将来の使用のために計算をキャッシュに保存します)。動的プログラミングでは、ソリューションが計算される順序をエンコードする必要があります。これにより、依存する小さな問題の前に「大きな問題」が計算されなくなります。


1
ああ、「トップダウン」と「ボトムアップ」の意味がわかりました。実際、それは単にメモ化対DPを指しているだけです。そして、私がタイトルでDPを言及するために質問を編集した人だと思うために...
ninjagecko

メモ化されたfib対通常の再帰的なfibのランタイムは何ですか?
シッダールタ

通常のcozの指数(2 ^ n)は、私が考える再帰ツリーです。
シッダールタ

1
ええ、それは線形です!私は再帰ツリーを引き出し、どの呼び出しを回避できるかを確認し、最初の呼び出しの後にmemo_fib(n-2)呼び出しがすべて回避されるため、再帰ツリーのすべての正しいブランチが切り捨てられ、線形に還元します。
Siddhartha、2012年

1
DPは本質的に各結果が最大で1回計算される結果テーブルを構築することを伴うため、DPアルゴリズムのランタイムを視覚化する1つの簡単な方法は、テーブルの大きさを確認することです。この場合、サイズはn(入力値ごとに1つの結果)なので、O(n)になります。他の場合には、それはOで、その結果、N ^ 2行列とすることができる(N ^ 2)、等
ジョンソン・ウォン

22

動的プログラミングの主要な機能は、重複するサブ問題の存在です。つまり、解決しようとしている問題はサブ問題に分割でき、それらのサブ問題の多くはサブサブ問題を共有します。それは「分断して征服」のようなものですが、同じことを何度も繰り返すことになります。2003年以降、これらの問題を指導または説明するときに使用した例:フィボナッチ数を再帰的に計算できます。

def fib(n):
  if n < 2:
    return n
  return fib(n-1) + fib(n-2)

お好きな言語を使用して、で実行してみてくださいfib(50)。非常に長い時間がかかります。だいたいfib(50)それ自体と同じくらいの時間!しかし、多くの不要な作業が行われています。fib(50)fib(49)and を呼び出しますが、値が同じであっても、fib(48)両方がを呼び出しfib(47)てしまいます。実際、fib(47)は3回計算されます。からの直接呼び出し、からfib(49)の直接呼び出しfib(48)、および別のからの直接呼び出しfib(48)によって、の計算によって生成されたものですfib(49)...したがって、重複するサブ問題があります

素晴らしいニュース:同じ値を何度も計算する必要はありません。一度計算したら、結果をキャッシュし、次にキャッシュされた値を使用します!これが動的プログラミングの本質です。「トップダウン」、「メモ化」など、好きな名前を付けることができます。このアプローチは非常に直感的で、実装が非常に簡単です。最初に再帰的なソリューションを記述し、小さなテストでテストし、メモ(すでに計算された値のキャッシュ)を追加し、そして---ビンゴ!---完了です。

通常、再帰なしでボトムアップで機能する同等の反復プログラムを作成することもできます。この場合、これはより自然なアプローチです。1から50までループして、すべてのフィボナッチ数を計算します。

fib[0] = 0
fib[1] = 1
for i in range(48):
  fib[i+2] = fib[i] + fib[i+1]

興味深いシナリオでは、ボトムアップソリューションは通常、理解するのがより困難です。ただし、一度理解すれば、通常、アルゴリズムがどのように機能するかをより明確に把握できます。実際には、重要な問題を解決するときは、最初にトップダウンアプローチを記述し、小さな例でテストすることをお勧めします。次に、ボトムアップソリューションを記述し、2つを比較して、同じ結果が得られることを確認します。理想的には、2つのソリューションを自動的に比較します。理想的には、多くのテストを生成する小さなルーチンを作成します- すべて特定のサイズまでの小規模なテスト---両方のソリューションが同じ結果をもたらすことを検証します。その後、本番環境でボトムアップソリューションを使用しますが、トップボトムコードは保持し、コメント化します。これにより、他の開発者があなたが何をしているのかを簡単に理解できるようになります。ボトムアップコードは、たとえ自分で書いても、何をしているのかを正確に理解しているとしても、非常にわかりにくい場合があります。

多くのアプリケーションでは、再帰呼び出しのオーバーヘッドのため、ボトムアップアプローチはわずかに高速です。スタックオーバーフローは、特定の問題で問題になることもあります。これは、入力データに大きく依存する可能性があることに注意してください。場合によっては、動的プログラミングを十分に理解していないと、スタックオーバーフローを引き起こすテストを記述できない場合がありますが、いつかはこれがまだ発生する可能性があります。

現在、問題の空間が非常に大きく、すべてのサブ問題を解決することができないため、トップダウンアプローチが唯一の実行可能な解決策である問題があります。ただし、入力が解決される部分問題のほんの一部しか必要としないため、「キャッシュ」は依然として妥当な時間で機能します---しかし、明確に定義するのは難しいので、どの部分問題を解決する必要があるか、つまりボトムを書く必要があります-アップソリューション。一方、すべての副問題を解決する必要があることがわかっている状況もあります。この場合、続けてボトムアップを使用します。

個人的には、段落の最適化、つまりワードラップの最適化問題にトップボトムを使用します(Knuth-Plassの改行アルゴリズムを調べます。少なくともTeXが使用し、Adobe Systemsの一部のソフトウェアも同様のアプローチを使用しています)。高速フーリエ変換にはボトムアップを使用します。


こんにちは!!!以下の命題が正しいかどうかを判断したいと思います。-ダイナミックプログラミングアルゴリズムの場合、ボトムアップでのすべての値の計算は、再帰とメモ化の使用よりも漸近的に速くなります。-動的アルゴリズムの時間は常にΟ(Ρ)です。ここで、Ρは副問題の数です。-NPの各問題は指数関数的に解決できます。
メアリースター

上記の命題について私は何を言うことができますか?アイデアはありますか?@osa
メアリースター

@evinda、(1)は常に間違っています。同じか、漸近的に遅くなります(すべての副問題が必要ない場合は、再帰が速くなります)。(2)は、O(1)のすべての部分問題を解決できる場合にのみ正しいです。(3)は正しいです。NPの各問題は、非決定性マシン(複数のことを同時に実行できる量子コンピューターのような、多項式時間で解くことができます:複数のことを同時に実行できます。したがって、ある意味で、NPの各問題は、通常のコンピューターで指数関数的に解決できます。SIde注:PのすべてはNPにもあります。たとえば、2つの整数を追加する
osa

19

例としてフィボナッチシリーズを見てみましょう

1,1,2,3,5,8,13,21....

first number: 1
Second number: 1
Third Number: 2

別の言い方をすると、

Bottom(first) number: 1
Top (Eighth) number on the given sequence: 21

最初の5つのフィボナッチ数の場合

Bottom(first) number :1
Top (fifth) number: 5 

例として、再帰的なフィボナッチ級数アルゴリズムを見てみましょう

public int rcursive(int n) {
    if ((n == 1) || (n == 2)) {
        return 1;
    } else {
        return rcursive(n - 1) + rcursive(n - 2);
    }
}

次のコマンドでこのプログラムを実行すると

rcursive(5);

アルゴリズムを詳しく調べると、5番目の数を生成するために3番目と4番目の数が必要です。したがって、私の再帰は実際にはtop(5)から始まり、それからすべてのボトム/低い数値に行きます。このアプローチは、実際にはトップダウンアプローチです。

同じ計算を複数回行うことを避けるために、動的プログラミング手法を使用します。以前に計算した値を保存して再利用します。この手法はメモ化と呼ばれます。動的プログラミングには、現在の問題を議論するのに必要のないメモ化以外にもあります。

トップダウン

オリジナルのアルゴリズムを書き直し、メモ化された手法を追加しましょう。

public int memoized(int n, int[] memo) {
    if (n <= 2) {
        return 1;
    } else if (memo[n] != -1) {
        return memo[n];
    } else {
        memo[n] = memoized(n - 1, memo) + memoized(n - 2, memo);
    }
    return memo[n];
}

そして、このメソッドを次のように実行します

   int n = 5;
    int[] memo = new int[n + 1];
    Arrays.fill(memo, -1);
    memoized(n, memo);

アルゴリズムはトップバリューから始まり、各ステップの最下部に移動してトップバリューを取得するため、このソリューションは依然としてトップダウンです。

一気飲み

しかし、問題は、最初のフィボナッチ数列のように、下から始めて、上に向かって歩けるかどうかです。このテクニックを使って書き直してみましょう

public int dp(int n) {
    int[] output = new int[n + 1];
    output[1] = 1;
    output[2] = 1;
    for (int i = 3; i <= n; i++) {
        output[i] = output[i - 1] + output[i - 2];
    }
    return output[n];
}

次に、このアルゴリズムを調べると、実際には低い値から開始されてから、先頭に移動します。5番目のフィボナッチ数が必要な場合、実際には1番目、次に2番目、次に3番目の5番目の数まで計算します。この手法は、実際にはボトムアップ手法と呼ばれています。

最後の2つは、動的プログラミング要件を満たすアルゴリズムです。しかし、1つはトップダウンで、もう1つはボトムアップです。どちらのアルゴリズムも、空間と時間の複雑さが似ています。


ボトムアップアプローチは非再帰的な方法で実装されることが多いと言えますか?
ルイスチャン

いいえ、ループロジックを再帰に変換できます
Ashvin Sharma

3

動的プログラミングはしばしばメモ化と呼ばれます!

1.メモ化はトップダウン手法であり(与えられた問題を分解することで解決します)、動的プログラミングはボトムアップ手法です(些細な副問題から、与えられた問題に向かって解決し始めます)

2.DPは、ベースケースから開始してソリューションを見つけ、上に向かって進みます。DPはボトムアップで実行するため、すべてのサブ問題を解決します

必要なサブ問題のみを解決するメモ化とは異なり

  1. DPには、指数時間ブルートフォースソリューションを多項式時間アルゴリズムに変換する可能性があります。

  2. DPは反復的であるため、はるかに効率的かもしれません

反対に、メモ化は、再帰による(多くの場合、かなりの)オーバーヘッドを支払う必要があります。

より簡単にするために、メモ化はトップダウンアプローチを使用して問題を解決します。つまり、コア(メイン)の問題から始まり、サブ問題に分割して、これらのサブ問題を同様に解決します。このアプローチでは、同じサブ問題が複数回発生し、より多くのCPUサイクルを消費する可能性があるため、時間の複雑さが増します。一方、動的プログラミングでは、同じ副問題が複数回解決されることはありませんが、以前の結果がソリューションの最適化に使用されます。


4
それは真実ではありません。メモ化は、時間の複雑さをDPと同じように節約するのに役立つキャッシュを使用します
InformedA

3

トップダウンアプローチは、Subの問題を何度も呼び出すために再帰を使用するというだけです
が、ボトムアップアプローチは何も呼び出さずにシングルを使用するため、より効率的です。


1

以下は、トップダウンの編集距離問題のDPベースのソリューションです。ダイナミックプログラミングの世界を理解するのにも役立つことを願っています。

public int minDistance(String word1, String word2) {//Standard dynamic programming puzzle.
         int m = word2.length();
            int n = word1.length();


     if(m == 0) // Cannot miss the corner cases !
                return n;
        if(n == 0)
            return m;
        int[][] DP = new int[n + 1][m + 1];

        for(int j =1 ; j <= m; j++) {
            DP[0][j] = j;
        }
        for(int i =1 ; i <= n; i++) {
            DP[i][0] = i;
        }

        for(int i =1 ; i <= n; i++) {
            for(int j =1 ; j <= m; j++) {
                if(word1.charAt(i - 1) == word2.charAt(j - 1))
                    DP[i][j] = DP[i-1][j-1];
                else
                DP[i][j] = Math.min(Math.min(DP[i-1][j], DP[i][j-1]), DP[i-1][j-1]) + 1; // Main idea is this.
            }
        }

        return DP[n][m];
}

あなたはあなたの家でその再帰的な実装を考えることができます。これまでにこのような問題を解決したことがない場合、それは非常に良い課題です。


1

トップダウン:現在までの計算値を追跡し、基本条件が満たされたときに結果を返します。

int n = 5;
fibTopDown(1, 1, 2, n);

private int fibTopDown(int i, int j, int count, int n) {
    if (count > n) return 1;
    if (count == n) return i + j;
    return fibTopDown(j, i + j, count + 1, n);
}

ボトムアップ:現在の結果は、そのサブ問題の結果によって異なります。

int n = 5;
fibBottomUp(n);

private int fibBottomUp(int n) {
    if (n <= 1) return 1;
    return fibBottomUp(n - 1) + fibBottomUp(n - 2);
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.