動的計画法: コンピューティングの問題を解決するためのアプローチ

コンピューター サイエンスでは、問題に遭遇することがあります。 これらをサブ問題に分割し、サブ問題をさらに小さなサブ問題に分割できます。 小さい部分問題が重複する場合は、後で参照できるように結果をメモリに保存できます。 この方法では、同じ結果を何度も計算する必要がないため、プログラムの効率が大幅に向上します。 これらの問題を解決するこの方法は、動的計画法と呼ばれます。

動的プログラミング – アルゴリズムの問​​題とコーディングの課題を解決する方法を学びます。 | | ビデオ: freeCodeCamp.org

この記事では、動的計画法とは何かを学びます。 また、動的計画法で解決できる単純な問題であるフィボナッチ数を計算する方法も示します。 動的計画法のソリューションと、再帰を使用する素朴なソリューションを比較します。 これらの例は、 パイソン 構文。 最後に、動的計画法を使用して問題を解決しようとするときに留意すべき一般的な指針も示します。

動的計画法

動的計画法は、後で参照できるように解をメモリに保存することにより、コンピューティングの問題を解決するための効率的な方法です。 サブ問題が重複している場合は、動的計画法を適用して時間を節約し、プログラムの効率を高めることができます。

Artturi Jali のその他の投稿: Python チート シート: Python の便利なガイド

動的計画法で解決できる問題の種類

動的計画法は通常、再帰を使用する特定の問題のソリューションを最適化する方法です。 問題の再帰的解法で、同じ入力を使用して部分問題の解を繰り返し計算する必要がある場合は、動的計画法を使用して最適化できます。 前述のように、この場合は、後で必要になったときに使用できるように、計算結果を保存するだけです。 この最適化により、アルゴリズムの時間の複雑さを指数時間から多項式時間に減らすことができます。 つまり計算回数は いいえ 指数式のようにスケーリングするのではなく、多項式のようにスケーリングします。 いいえ 増加します。 一般に、多項式は指数式よりもはるかに遅くなります。

動的計画法を使用するには、次の 2 つの条件を満たす必要があります。

  1. 部分問題の重複
  2. 最適部分構造特性

重複部分問題とは

部分問題の重複状態については前に触れました。 これは単に、問題を解決するときに、同じ部分問題の解決策が繰り返し必要になることを意味します。 この場合、これらの部分問題の解を保存して、後で再計算をスキップするために使用できます。

この状態について考える別の方法は、逆さまにすることです。 重複する部分問題がない場合、部分問題の解を保存しても意味がなく、動的計画法は使用できません。

重複部分問題の解を格納するには、次の 2 つの方法があります。

  1. メモ化(トップダウン)
  2. タブ (ボトムアップ)

メモ化とは

動的計画法へのメモ化アプローチは、単純な再帰アプローチと非常によく似ていますが、わずかな変更しかありません。 違いは、ルックアップ テーブルを使用して部分問題の解を格納し、このテーブルを使用してその解が既に存在するかどうかを確認することです。

特定の部分問題の解がルックアップ テーブルに既に存在する場合、その解はルックアップ テーブルから返されます。 そうでない場合は、(再帰によって) 計算し、ルックアップ テーブルに追加する必要があります。

わかりやすくするために、動的計画法の問題の部分問題の解を次のように定義しましょう。 DP[X]。、 と DP[N] 望ましい解決策であり、 DP[0] 基本的な解決策です。 メモ化アプローチでは、プログラムは DP[N] そして、そこからの解決策を求めます DP[N] に到達することができます (これらは低次のサブ問題である必要があります) DP[n. そして、これらの状態から、基本解に到達するまで、同じプロセスが再帰的に繰り返されます。 DP[0].

これが少し抽象的すぎると感じても、心配しないでください。 この記事の後半で紹介する例は、私の言いたいことを明確にするはずです。

メモ化は、プログラムが目的の最後の (トップ) 状態から開始され、再帰的に処理されるため、動的プログラミングへのトップダウン アプローチとして知られています。

タビュレーションとは

動的計画法の集計アプローチは、メモ化アプローチとは逆の方法で機能します。 プログラムは、サブ問題のベース (またはボトム) ソリューションから開始し、目的のソリューションに到達するまで、サブ問題を 1 つずつ解決しながら上に向かって進みます。

部分問題の解決策に関して、集計アプローチは基本的な解決策から始まります DP[0] そして計算する DP[1]、PD[2]、…DP[N] 部分問題の望ましい解に到達するまで DP[N]. ベースソリューションから始めたので DP[0] 望ましい解決策に取り組みました DP[N]、集計アプローチはボトムアップアプローチとしても知られています。

繰り返しますが、以下の例はこれを理解しやすくするはずです。

最適部分構造特性とは

問題の最適解が部分問題の最適解を使用して得られる場合、その問題は次のようになると言われます。 最適部分構造特性.

例として、下のグラフの「開始」ノードと「目標」ノードの間の最短経路を見つける問題を考えてみましょう。 ノードはエッジを介して他のノードに接続され、接続された 2 つのノード間の距離はエッジの横に数字でマークされます。

「開始」ノードと「目標」ノードのグラフ。 | | 画像: Artturi Jalli

開始ノードからゴール ノードへの最短パスは、ノード 3 と 4 を経由します。

この問題には、明らかに最適な部分構造のプロパティがあります。 開始ノードからゴール ノードへの最短パスはノード 4 を通過するため、このパスは、開始ノードからノード 4 への最短パスとノード 4 からゴール ノードへの最短パスの組み合わせであることが明らかになります。

多くの問題には、最適な部分構造のプロパティがありません。 たとえば、上のグラフの Start ノードと Goal ノードの間の最長パス (サイクルなし) を見つける問題は、そうではありません。 これを確認する方法は次のとおりです。

最長パスは、開始 – ノード 3 – ノード 2 – ノード 1 – ノード 4 – ゴールです。 ただし、これは、開始ノードからノード 2 への最長パスが開始 – ノード 3 – ノード 2 であることを意味するものではありません。 開始ノードからノード 2 までの最長パスは、実際には開始 – ノード 1 – ノード 4 – ノード 3 – ノード 2 (および開始 – ノード 1 – ノード 4 – ゴール – ノード 2 で、最初のものと同じ長さ) です。

動的プログラミングの例: フィボナッチ数の計算

動的計画法の最も単純な例の 1 つは、フィボナッチ数列の数値であるフィボナッチ数の計算です。 最初のフィボナッチ数は 0、2 番目は 1 で、その後の数は前の 2 つのフィボナッチ数の合計です。 最初の 10 個のフィボナッチ数は、 0、1、1、2、3、5、8、13、21、および 34。

まず、素朴で再帰的なソリューションから始めましょう。 n 番目のフィボナッチ数を計算する Python 関数を次に示します (インデックスはゼロから始まります)。

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

この例から、関数が以前のフィボナッチ数を複数回 (n > 3 の場合) 計算する必要があることが明らかであるため、この問題が部分問題の重複条件を満たすことが容易にわかります。 この関数が大きな n に対して呼び出されると、最小のフィボナッチ数が最も頻繁に計算されます。

目的の問題の解を計算するために使用される部分問題の解は 1 つしかないため、この問題にも明らかに最適な部分構造があります。

再帰のため、この関数は指数時間で実行されます。

次に、動的計画法を使用してこれを解決する方法を見てみましょう。 メモ化を使用したトップダウン ソリューションから始めましょう。 以下は、メモ化によって動的計画法を実装する n 番目のフィボナッチ数を計算する Python 関数です。

def fib_DP_memoization(n):

	# Define a lookup table
	lookup_table = [None] * (n+1)

	# Define an inner function for the recursive part
	def _inner(n, lookup):
		# If the n:th Fibonacci number has not been
		# calculated previously, calculate it
		if lookup[n] is None:

			if n <= 1:	# Base case
				lookup[n] = n
			else:
				lookup[n] = _inner(n-1, lookup) 
						+ _inner(n-2, lookup)

		return lookup[n]

	_inner(n, lookup_table)

	# Return n:th Fibonacci number
	return lookup_table[n]

このアプローチは、n 番目のフィボナッチ数の計算から開始し、それを計算するために n-1 番目と n-2 番目のフィボナッチ数の計算に進むため、再帰的アプローチと非常に似ています。 違いはルックアップ テーブルにあります。小さいフィボナッチ数は計算時に保存されるため、何度も計算する必要はありません。

これにより、この関数は実際には指数時間ではなく線形時間で実行されます。

例として、表形式を使用した動的計画法で同じ問題をボトムアップ方式で解決する Python 関数も見てみましょう。

def fib_DP_tabulation(n):
	# Define an array and base case(s)
	f = [0] * (n+1)
	if n > 0:
		f[1] = 1

	# Calculate Fibonacci numbers bottom-up
	for i in range(2, n+1):
		f[i] = f[i-1] + f[i-2]

	return f[n]

このソリューションでは、n 番目のフィボナッチ数がボトムアップ方式で計算されます。最初のフィボナッチ数の計算から始まり、2 番目、3 番目と続き、n 番目のフィボナッチ数に到達します。 この関数も線形時間で実行されます。

ソフトウェア工学の詳細: Python の Glob モジュール: 説明

動的計画法を使用して問題を解決する方法

動的計画法を使用して問題を解決するための最初のステップは、それを動的計画法の問題として識別することです。 問題に重複する部分問題があり、最適な部分構造のプロパティを満たしていることを検証できれば、動的計画法で解決できると確信できます。

2 番目のステップは、状態式を決定することです。 この状態は、さまざまなサブ問題を一意に識別するパラメーターのセットです。

フィボナッチ数の例では、状態を識別するパラメーターは、特定のフィボナッチ数のシリアル番号になります。

状態を識別するパラメーターが複数存在する場合があります。 ただし、常にできるだけ少ないパラメーターを使用する必要があります。

動的計画法を使用して問題を解決する際の 3 番目の (おそらく最も困難な) ステップは、さまざまな状態間の関係を定式化することです。

ただし、フィボナッチ数の場合、n 番目のフィボナッチ数は n-1 番目と n-2 番目のフィボナッチ数の合計であるため、これは単純です。 SOF[n] =F[n-1] +F[n-2].

4 番目のステップは、状態間に発見した関係を使用して、決定した状態のメモ化または集計を実装することです。 これは、単に状態 (つまり、特定の部分問題の解) を保存することを意味し、再度必要になったときに再計算せずにメモリから復元できるようにします。 手順 1 ~ 3 を適切に実行した場合、これはかなり単純なはずです。

.

Leave a Comment

Your email address will not be published. Required fields are marked *