動的プログラミングとは何ですか?[閉まっている]


276

動的プログラミングとは何ですか?

再帰、メモ化などとどう違うのですか?

ウィキペディアの記事を読んだのですが、まだよくわかりません。


1
これは、CMUのMichael A. Trickによるチュートリアルの1つです。これは、私が特に役に立ったと感じたものです。そしてクラインバーグ、タードスはとても良いです!)私がこのチュートリアルを気に入っているのは、高度な概念がかなり段階的に導入されているためです。少し古い資料ですが、ここに示すリソースのリストに追加するのに適しています。また、動的プログラミングにスティーブン・スキイーナのページや講演会をチェックアウト:cs.sunysb.edu/~algorith/video-lecturesます。http:
エドモン

11
「ダイナミックプログラミング」は紛らわしい用語であることに気づきました。「ダイナミック」は静的ではないことを示唆していますが、「スタティックプログラミング」とは何ですか?そして「...プログラミング」は「オブジェクト指向プログラミング」と「関数型プログラミング」を思い起こさせ、DPがプログラミングのパラダイムであることを示唆しています。本当に良い名前はありませんが(おそらく「ダイナミックアルゴリズム」でしょうか)、この名前にこだわっているのは残念です。
dimo414

3
@ dimo414ここでの「プログラミング」は、数学の最適化手法のクラスに該当する「線形計画法」に関連しています。他の数理計画法のリストについては、数学的最適化の記事を参照してください。
syockit 2016年

1
@ dimo414この文脈での「プログラミング」とは、コンピュータコードを記述することではなく、表形式の方法を指します。- Coreman
user2618142

cs.stackexchange.com/questions/59797/…で説明されているバスチケットのコスト最小化の問題は、動的プログラミングで最もよく解決されます。
トゥルーアジャスター

回答:


210

動的プログラミングとは、過去の知識を使用して将来の問題を簡単に解決できるようにすることです。

良い例は、n = 1,000,002のフィボナッチ数列を解くことです。

これは非常に長いプロセスになりますが、n = 1,000,000およびn = 1,000,001の結果を提供するとどうなりますか?突然、問題はより扱いやすくなりました。

動的プログラミングは、文字列編集の問題など、文字列の問題でよく使用されます。問題のサブセットを解決し、その情報を使用して、より困難な元の問題を解決します。

動的プログラミングでは、一般的に結果をある種のテーブルに格納します。問題の答えが必要な場合は、表を参照して、それが何であるかをすでに知っているかどうかを確認します。そうでない場合は、テーブル内のデータを使用して、答えへの足掛かりを自分に与えます。

コーメンアルゴリズムの本には、動的プログラミングに関する素晴らしい章があります。また、Googleブックスでは無料です。こちらでチェックしてください。


50
ただ、メモについて説明しただけではありませんか?
ドレッドウェイル2009年

31
メモ化は、メモ化された関数/メソッドが再帰的なものである場合、動的プログラミングの一種と言えます。
Daniel Huckstep、2009年

6
良い答えは、最適なサブ構造に関する言及のみを追加します(たとえば、AからBへの最短パスに沿ったパスのすべてのサブセットは、それ自体が、三角形の不等式を観察する距離メトリックを想定した2つのエンドポイント間の最短パスです)。
Shea

5
「もっと簡単」とは言いませんが、もっと速いです。よくある誤解は、dpがナイーブアルゴリズムでは解決できない問題を解決するということですが、そうではありません。機能ではなく、パフォーマンスの問題です。
andandandand 2009年

6
メモ化を使用すると、動的プログラミングの問題をトップダウン方式で解決できます。つまり、関数を呼び出して最終値を計算し、その関数が自己再帰的に呼び出してサブ問題を解決します。それがなければ、動的プログラミングの問題はボトムアップの方法でしか解決できません。
プラナフ2009

175

動的計画法は、再帰アルゴリズムで同じ部分問題を何度も計算することを避けるために使用される技法です。

フィボナッチ数列の簡単な例を見てみましょう:n 番目を見つける定義されるフィボナッチ数列を

F n = F n-1 + F n-2およびF 0 = 0、F 1 = 1

再帰

これを行う明白な方法は再帰的です:

def fibonacci(n):
    if n == 0:
        return 0
    if n == 1:
        return 1

    return fibonacci(n - 1) + fibonacci(n - 2)

動的プログラミング

  • トップダウン-メモ化

与えられたフィボナッチ数は複数回計算されるため、再帰は多くの不必要な計算を行います。これを改善する簡単な方法は、結果をキャッシュすることです:

cache = {}

def fibonacci(n):
    if n == 0:
        return 0
    if n == 1:
        return 1
    if n in cache:
        return cache[n]

    cache[n] = fibonacci(n - 1) + fibonacci(n - 2)

    return cache[n]
  • 一気飲み

これを行うためのより良い方法は、正しい順序で結果を評価することにより、再帰をすべて取り除くことです。

cache = {}

def fibonacci(n):
    cache[0] = 0
    cache[1] = 1

    for i in range(2, n + 1):
        cache[i] = cache[i - 1] +  cache[i - 2]

    return cache[n]

一定のスペースを使用して、途中で必要な部分的な結果のみを保存することもできます。

def fibonacci(n):
  fi_minus_2 = 0
  fi_minus_1 = 1

  for i in range(2, n + 1):
      fi = fi_minus_1 + fi_minus_2
      fi_minus_1, fi_minus_2 = fi, fi_minus_1

  return fi
  • 動的プログラミングをどのように適用しますか?

    1. 問題の再帰を見つけます。
    2. トップダウン:各副問題の答えを表に格納して、再計算する必要をなくします。
    3. ボトムアップ:必要なときに部分的な結果を利用できるように、結果を評価する正しい順序を見つけます。

動的プログラミングは通常、文字列、ツリー、整数シーケンスなど、固有の左から右の順序を持​​つ問題に対して機能します。単純な再帰アルゴリズムが同じ部分問題を複数回計算しない場合、動的プログラミングは役に立ちません。

ロジックを理解するのに役立つ問題のコレクションを作成しました:https : //github.com/tristanguigue/dynamic-programing


3
これは素晴らしい答えであり、Githubの問題集も非常に役に立ちます。ありがとう!
p4sh4

物事を明確にするための好奇心からちょうど-あなたの意見では、再帰関係とメモ化を使用した再帰的な実装は動的プログラミングですか?
コドール2017

説明ありがとう。ボトムアップから欠落している状態はありif n in cacheますか?
DavidC、

各反復で計算された値が後続の反復で使用されるループは動的プログラミングの例であることを正しく理解していますか?
Alexey

トップダウンやボトムアップの特殊なケースを含め、あなたが与えた解釈の参考にしてください。
Alexey

37

メモ化は、関数呼び出しの以前の結果を保存するときです(実際の関数は、同じ入力を与えられると、常に同じものを返します)。結果が保存される前のアルゴリズムの複雑さには影響しません。

再帰は、それ自体を呼び出す関数のメソッドであり、通常はデータセットが小さくなります。ほとんどの再帰関数は同様の反復関数に変換できるため、アルゴリズムの複雑さにも影響しません。

動的プログラミングは、解決が容易な副問題を解決し、そこから答えを構築するプロセスです。ほとんどのDPアルゴリズムは、貪欲アルゴリズム(存在する場合)と指数関数(すべての可能性を列挙して最適なアルゴリズムを見つける)アルゴリズムの間の実行時間内にあります。

  • DPアルゴリズムは再帰で実装できますが、そうである必要はありません。
  • 各副問題は一度しか解決されない(または「ソルブ」関数が呼び出される)ため、DPアルゴリズムをメモ化によって高速化することはできません。

非常に明確に置く。アルゴリズムのインストラクターがこれをうまく説明してくれることを願っています。
ケリーS.フランス語

21

実行時間を短縮するのは、アルゴリズムの最適化です。

貪欲アルゴリズムは通常同じデータセットで複数回実行される可能性があるため、単純アルゴリズムと呼ばれますが、ダイナミックプログラミングでは、最終的なソリューションを構築するために保存する必要がある部分的な結果をより深く理解することにより、この落とし穴を回避します。

簡単な例は、ソリューションで寄与するノードのみを介してツリーまたはグラフをトラバースするか、同じノードを何度もトラバースしないように、これまでに見つけたソリューションをテーブルに置くことです。

UVAのオンラインジャッジ、Edit Steps Ladderによる、動的プログラミングに適した問題の例を以下に示します。

この問題の分析の重要な部分について簡単に説明します。これは、 『プログラミングの挑戦』の本から引用したものですので、ぜひご覧ください。

その問題をよく見てください。2つの文字列がどれだけ離れているかを示すコスト関数を定義する場合、3つの自然なタイプの変更を2つ検討します。

置換-「ショット」を「スポット」に変更するなど、単一の文字をパターン「s」からテキスト「t」の別の文字に変更します。

挿入-パターン「s」に単一の文字を挿入して、「ago」を「agog」に変更するなど、テキスト「t」に一致させるのに役立ちます。

削除-パターン「s」から1文字を削除して、「hour」を「our」に変更するなど、テキスト「t」との一致を支援します。

この各操作を1ステップのコストに設定すると、2つの文字列間の編集距離が定義されます。では、それをどのように計算するのでしょうか?

文字列の最後の文字を一致、置換、挿入、または削除する必要があるという観察を使用して、再帰アルゴリズムを定義できます。最後の編集操作で文字を切り落とすと、ペア操作が残り、ペアの小さな文字列が残ります。iとjを、それぞれ関連するプレフィックスの最後の文字とします。最後の操作の後に、一致/置換、挿入、または削除後の文字列に対応する3つの短い文字列のペアがあります。小さい文字列の3つのペアを編集するコストがわかっている場合、どのオプションが最適なソリューションにつながるかを判断し、それに応じてそのオプションを選択できます。再帰というすばらしいことを通して、このコストを知ることができます。

#define MATCH 0 /* enumerated type symbol for match */
#define INSERT 1 /* enumerated type symbol for insert */
#define DELETE 2 /* enumerated type symbol for delete */


int string_compare(char *s, char *t, int i, int j)

{

    int k; /* counter */
    int opt[3]; /* cost of the three options */
    int lowest_cost; /* lowest cost */
    if (i == 0) return(j * indel(’ ’));
    if (j == 0) return(i * indel(’ ’));
    opt[MATCH] = string_compare(s,t,i-1,j-1) +
      match(s[i],t[j]);
    opt[INSERT] = string_compare(s,t,i,j-1) +
      indel(t[j]);
    opt[DELETE] = string_compare(s,t,i-1,j) +
      indel(s[i]);
    lowest_cost = opt[MATCH];
    for (k=INSERT; k<=DELETE; k++)
    if (opt[k] < lowest_cost) lowest_cost = opt[k];
    return( lowest_cost );

}

このアルゴリズムは正しいですが、非常に遅いです。

私たちのコンピューターで実行すると、2つの11文字の文字列を比較するのに数秒かかり、計算が消えて、それ以上長くなることはありません。

なぜアルゴリズムがとても遅いのですか?値を何度も再計算するため、指数関数的な時間がかかります。文字列のすべての位置で、再帰は3つの方法で分岐します。つまり、少なくとも3 ^ nの割合で成長します。実際、ほとんどの呼び出しは2つのインデックスの両方ではなく、1つだけを減らすため、さらに高速です。

それでは、アルゴリズムを実用的にするにはどうすればよいでしょうか。重要な観察は、これらの再帰呼び出しのほとんどが、以前にすでに計算されたものを計算しているということです。どうやって知るの?まあ、| s |しかありません ・| t | 再帰呼び出しのパラメーターとして機能するのは、多くの異なる(i、j)ペアだけなので、可能な一意の再帰呼び出しです。

これらの(i、j)の各ペアの値をテーブルに格納することにより、それらを再計算することを回避し、必要に応じてそれらを検索できます。

テーブルは2次元行列mであり、| s |・| t |のそれぞれが セルには、このサブ問題の最適解のコストと、この場所に到達した方法を説明する親ポインターが含まれています。

typedef struct {
int cost; /* cost of reaching this cell */
int parent; /* parent cell */
} cell;

cell m[MAXLEN+1][MAXLEN+1]; /* dynamic programming table */

動的プログラミングバージョンには、再帰バージョンと3つの違いがあります。

まず、再帰呼び出しの代わりにテーブル検索を使用して中間値を取得します。

** 2番目に、**各セルの親フィールドを更新します。これにより、後で編集シーケンスを再構築できます。

** 3番目、** 3番目、cell()m [| s |] [| t |] .costを返すだけでなく、より一般的なゴール関数を使用して計測されます。これにより、このルーチンをより幅広いクラスの問題に適用できるようになります。

ここでは、最適な部分的な結果を収集するために必要なことの非常に特定の分析が、ソリューションを「動的」なものにします。

これは、同じ問題に対する代替の完全な解決策です。実行方法は異なりますが、「動的」なものでもあります。UVAのオンラインジャッジに提出することで、ソリューションの効率性を確認することをお勧めします。そのような重い問題がいかに効率的に対処されたかは驚くべきものです。


ストレージは本当に動的プログラミングである必要がありますか?スキップする作業量が多いと、アルゴリズムは動的であると見なされませんか?
Nthalk 2011年

あなたは持っている、最適な収集するために、ステップバイステップのアルゴリズム「ダイナミック」を作るために結果を。動的プログラミングはORでのベルマンの仕事に端を発しています。「単語をいくらかスキップしても動的プログラミングである」と言った場合、検索ヒューリスティックは動的プログラミングであるため、用語の価値を下げています。en.wikipedia.org/wiki/Dynamic_programming
andandandand 2012年

12

動的プログラミングの主要なビットは、「重複するサブ問題」と「最適なサブ構造」です。問題のこれらの特性は、最適解がその副問題の最適解で構成されることを意味します。たとえば、最短経路の問題は最適な部分構造を示します。AからCへの最短パスは、AからノードBへの最短パスであり、その後にそのノードBからCへの最短パスが続きます。

より詳細には、最短経路問題を解決するには、次のようにします。

  • 開始ノードからそれに接するすべてのノードまでの距離を見つける(たとえば、AからBおよびCへ)
  • それらのノードからそれらに接するノードまでの距離を求めます(BからDとE、およびCからEとF)
  • これで、AからEへの最短経路がわかりました。これは、訪問したノードxのAxとxEの最短合計です(BまたはC)。
  • 最終的な宛先ノードに到達するまでこのプロセスを繰り返します

私たちはボトムアップで作業しているため、サブ問題をメモすることで、それらを使用するときの解決策をすでに持っています。

動的プログラミングの問題には、重複するサブ問題と最適なサブ構造の両方が必要であることに注意してください。フィボナッチ数列の生成は動的プログラミングの問題ではありません。重複するサブ問題があるためメモ化を利用しますが、最適化のサブ構造はありません(最適化の問題がないため)。


1
私見、これは動的プログラミングに関して意味のある唯一の答えです。人々がフィボナッチ数(ほとんど関係ない)を使用してDPを説明し始めたときから、私は興味があります。
Terry Li

@TerryLi、それは「理にかなっている」かもしれませんが、理解するのは簡単ではありません。フィボナッチ数の問題は既知であり、簡単に理解できます。
Ajay

5

動的プログラミング

定義

動的計画法(DP)は、副問題が重複する問題を解決するための一般的なアルゴリズム設計手法です。この手法は、1950年代にアメリカの数学者「リチャードベルマン」によって発明されました。

重要なアイデア

重要なアイデアは、重複する小さなサブ問題の回答を保存して、再計算を回避することです。

動的プログラミングのプロパティ

  • インスタンスは、小さいインスタンスのソリューションを使用して解決されます。
  • 小さいインスタンスのソリューションは複数回必要になる可能性があるため、それらの結果をテーブルに保存します。
  • したがって、小さいインスタンスはそれぞれ1回だけ解決されます。
  • 時間を節約するために、追加のスペースが使用されます。

4

また、私はダイナミックプログラミング(特定の種類の問題のための強力なアルゴリズム)を非常に初めて使用しています。

最も単純な言葉で言えば、動的プログラミングは以前の知識を使用した再帰的なアプローチと考えてください。

以前の知識は、ここで最も重要なものです。すでに持っている副問題の解決策を追跡してください。

ウィキペディアのdpの最も基本的な例を考えてみましょう

フィボナッチ数列を見つける

function fib(n)   // naive implementation
    if n <=1 return n
    return fib(n − 1) + fib(n − 2)

例えばn = 5で関数呼び出しを分解してみましょう

fib(5)
fib(4) + fib(3)
(fib(3) + fib(2)) + (fib(2) + fib(1))
((fib(2) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
(((fib(1) + fib(0)) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))

特に、fib(2)は最初から3回計算されました。大きな例では、fibまたはサブ問題のより多くの値が再計算され、指数時間アルゴリズムが発生します。

今、言って私たちはすでにデータ構造内で見つかった値を格納することで、それを試すことができます地図

var m := map(0 → 0, 1 → 1)
function fib(n)
    if key n is not in map m 
        m[n] := fib(n − 1) + fib(n − 2)
    return m[n]

ここでは、サブ問題の解をマップに保存します(まだない場合)。すでに計算した値を保存するこの手法は、メモ化と呼ばれます。

最後に、問題については、最初に状態(可能なサブ問題)を見つけて、より良い再帰アプローチを考えて、以前のサブ問題の解をさらに別の解に使用できるようにしてみてください。


ウィキペディアからのぼったくり。反対投票!
solidak 2018年

3

動的プログラミングは、サブ問題が重複する問題を解決するための手法です。動的プログラミングアルゴリズムは、すべてのサブ問題を一度だけ解決し、その答えをテーブル(配列)に保存します。サブ問題が発生するたびに回答を再計算する作業を回避します。動的プログラミングの基本的な考え方は、次のとおりです。通常、副問題の既知の結果の表を保持することにより、同じものを2回計算することを避けます。

動的プログラミングアルゴリズムの開発における7つのステップは次のとおりです。

  1. 問題のインスタンスの解決策を提供する再帰的なプロパティを確立します。
  2. 再帰的なプロパティに従って再帰的なアルゴリズムを開発する
  3. 問題の同じインスタンスが再帰呼び出しでもう一度解決されているかどうかを確認します
  4. メモ化された再帰アルゴリズムを開発する
  5. メモリにデータを保存するパターンを確認する
  6. メモ化された再帰アルゴリズムを反復アルゴリズムに変換します
  7. 必要に応じてストレージを使用して反復アルゴリズムを最適化する(ストレージの最適化)

ある6. Convert the memoized recursive algorithm into iterative algorithm必須のステップは?これは、その最終的な形式が非再帰的であることを意味しますか?
トゥルーアジャスター

必須ではなく、オプション
Adnan Qureshi

反復ソリューションは、行われる再帰呼び出しごとに関数スタックの作成を保存するため、目的は、データをメモリに格納するために使用される再帰アルゴリズムを、格納された値に対する反復で置き換えることです。
デビッドC.ランキン

1

つまり、再帰メモ化と動的プログラミングの違い

名前が示唆する動的プログラミングは、前の計算値を使用して次の新しいソリューションを動的に構築します

動的プログラミングを適用する場所:ソリューションが最適な部分構造と重なり合う部分問題に基づいている場合、その場合は以前に計算された値を使用すると便利なので、再計算する必要はありません。ボトムアップアプローチです。その場合、fib(n)を計算する必要があるとしましょう。必要なのは、前に計算されたfib(n-1)とfib(n-2)の値を加算することだけです。

再帰:基本的に、問題を小さな部分に細分して簡単に解決しますが、以前に他の再帰呼び出しで同じ値を計算した場合、再計算は避けられないことに注意してください。

メモ化:基本的に、古い計算された再帰値をテーブルに格納することはメモ化と呼ばれ、以前の呼び出しによって既に計算されている場合は再計算を回避するため、値は1回だけ計算されます。したがって、計算する前に、この値がすでに計算されているかどうかを確認します。計算済みの場合は、再計算するのではなく、テーブルから同じ値を返します。トップダウンのアプローチでもあります


-2

ここでの簡単なPythonのコード例でありRecursiveTop-downBottom-upフィボナッチ数列のためのアプローチは:

再帰的:O(2 n

def fib_recursive(n):
    if n == 1 or n == 2:
        return 1
    else:
        return fib_recursive(n-1) + fib_recursive(n-2)


print(fib_recursive(40))

トップダウン:O(n)より大きな入力に効率的

def fib_memoize_or_top_down(n, mem):
    if mem[n] is not 0:
        return mem[n]
    else:
        mem[n] = fib_memoize_or_top_down(n-1, mem) + fib_memoize_or_top_down(n-2, mem)
        return mem[n]


n = 40
mem = [0] * (n+1)
mem[1] = 1
mem[2] = 1
print(fib_memoize_or_top_down(n, mem))

ボトムアップ:O(n)シンプルで入力サイズが小さい

def fib_bottom_up(n):
    mem = [0] * (n+1)
    mem[1] = 1
    mem[2] = 1
    if n == 1 or n == 2:
        return 1

    for i in range(3, n+1):
        mem[i] = mem[i-1] + mem[i-2]

    return mem[n]


print(fib_bottom_up(40))

最初のケースの実行時間はn ^ 2ではなく、その時間の複雑さはO(2 ^ n)です。stackoverflow.com
Sam

感謝を更新しました。@サム
0xAliHn
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.