mirror of
https://github.com/krahets/hello-algo.git
synced 2026-06-17 07:38:23 +08:00
Re-translate the Japanese version (#1871)
* Retranslate Japanese docs with GPT-5.4 * Retranslate Japanese code with GPT-5.4
This commit is contained in:
@@ -1,79 +1,79 @@
|
||||
# 動的プログラミング問題の特徴
|
||||
# 動的計画法の問題特性
|
||||
|
||||
前のセクションでは、動的プログラミングが問題を部分問題に分解することで元の問題を解決する方法を学びました。実際、部分問題の分解は一般的なアルゴリズムアプローチであり、分割統治法、動的プログラミング、バックトラッキングでは異なる重点があります。
|
||||
前節では、動的計画法が部分問題への分解によってどのように元の問題を解くのかを学びました。実際、部分問題への分解は汎用的なアルゴリズムの考え方であり、分割統治法、動的計画法、バックトラッキングでは重視点が異なります。
|
||||
|
||||
- 分割統治法アルゴリズムは元の問題を複数の独立した部分問題に再帰的に分割し、最小の部分問題に到達するまで続け、バックトラッキング時に部分問題の解を組み合わせて最終的に元の問題の解を得ます。
|
||||
- 動的プログラミングも問題を再帰的に分解しますが、分割統治法アルゴリズムとの主な違いは、動的プログラミングの部分問題が相互依存的であり、分解プロセス中に多くの重複する部分問題が現れることです。
|
||||
- バックトラッキングアルゴリズムは試行錯誤によってすべての可能な解を網羅し、枝刈りによって不必要な探索分岐を避けます。元の問題の解は一連の決定ステップから構成され、各決定ステップ前の各部分シーケンスを部分問題として考えることができます。
|
||||
- 分割統治法は、元の問題を再帰的に複数の互いに独立した部分問題へ分割し、最小の部分問題に至るまで分解したうえで、バックトラック時に部分問題の解を統合し、最終的に元の問題の解を得ます。
|
||||
- 動的計画法も問題を再帰的に分解しますが、分割統治法との主な違いは、動的計画法における部分問題が相互依存しており、分解の過程で多数の重複部分問題が現れることです。
|
||||
- バックトラッキング法は、試行と巻き戻しの中ですべての可能な解を列挙し、枝刈りによって不要な探索分岐を避けます。元の問題の解は一連の意思決定ステップから構成されるため、各決定ステップ以前の部分系列を一つの部分問題と見なせます。
|
||||
|
||||
実際、動的プログラミングは最適化問題を解決するためによく使用され、これらは重複する部分問題を含むだけでなく、他に2つの主要な特徴があります:最適部分構造と無記憶性です。
|
||||
実際、動的計画法は最適化問題を解くためによく用いられます。これらは重複部分問題を含むだけでなく、さらに二つの大きな特性、すなわち最適部分構造と無後効性を備えています。
|
||||
|
||||
## 最適部分構造
|
||||
|
||||
階段登り問題を少し修正して、最適部分構造の概念を実証するのにより適したものにします。
|
||||
階段昇り問題を少し変更し、最適部分構造の概念をより示しやすくします。
|
||||
|
||||
!!! question "階段登りの最小コスト"
|
||||
!!! question "階段昇りの最小コスト"
|
||||
|
||||
階段があり、一度に1段または2段上ることができ、階段の各段にはその段で支払う必要があるコストを表す非負の整数があります。非負の整数配列 $cost$ が与えられ、$cost[i]$ は $i$ 段目で支払う必要があるコストを表し、$cost[0]$ は地面(開始点)です。頂上に到達するために必要な最小コストは何ですか?
|
||||
階段が与えられ、各ステップで $1$ 段または $2$ 段上ることができます。各段には非負整数が貼られており、その段に到達するために支払う必要があるコストを表します。非負整数配列 $cost$ が与えられ、$cost[i]$ は第 $i$ 段で支払うコストを表し、$cost[0]$ は地面(開始地点)です。頂上に到達するために必要な最小コストを求めてください。
|
||||
|
||||
下の図に示すように、1段目、2段目、3段目のコストがそれぞれ $1$、$10$、$1$ の場合、地面から3段目に登る最小コストは $2$ です。
|
||||
下図に示すように、第 $1$、$2$、$3$ 段のコストがそれぞれ $1$、$10$、$1$ である場合、地面から第 $3$ 段まで上る最小コストは $2$ です。
|
||||
|
||||

|
||||

|
||||
|
||||
$dp[i]$ を $i$ 段目に登る累積コストとします。$i$ 段目は $i-1$ 段目または $i-2$ 段目からのみ来ることができるため、$dp[i]$ は $dp[i-1] + cost[i]$ または $dp[i-2] + cost[i]$ のいずれかしかありえません。コストを最小化するために、2つのうち小さい方を選択すべきです:
|
||||
$dp[i]$ を第 $i$ 段まで上るのに累積して支払ったコストとします。第 $i$ 段には $i - 1$ 段または $i - 2$ 段からしか到達できないため、$dp[i]$ は $dp[i - 1] + cost[i]$ または $dp[i - 2] + cost[i]$ のいずれかになります。コストをできるだけ小さくするには、この二つのうち小さいほうを選べばよいです。
|
||||
|
||||
$$
|
||||
dp[i] = \min(dp[i-1], dp[i-2]) + cost[i]
|
||||
$$
|
||||
|
||||
これにより最適部分構造の意味がわかります:**元の問題の最適解は部分問題の最適解から構築される**。
|
||||
ここから最適部分構造の意味を導けます。**元の問題の最適解は、部分問題の最適解から構築される**ということです。
|
||||
|
||||
この問題は明らかに最適部分構造を持っています:2つの部分問題 $dp[i-1]$ と $dp[i-2]$ の最適解からより良い方を選択し、それを使用して元の問題 $dp[i]$ の最適解を構築します。
|
||||
この問題が最適部分構造を持つことは明らかです。二つの部分問題の最適解 $dp[i-1]$ と $dp[i-2]$ からより良いほうを選び、それを用いて元の問題 $dp[i]$ の最適解を構築しています。
|
||||
|
||||
では、前のセクションの階段登り問題は最適部分構造を持っているでしょうか?その目標は解の数を求めることで、これは数え上げ問題のようですが、別の方法で尋ねてみましょう:「解の最大数を求める」。驚くことに、**問題が変わったにもかかわらず、最適部分構造が現れた**ことがわかります:$n$ 段目での解の最大数は、$n-1$ 段目と $n-2$ 段目での解の最大数の和に等しいです。したがって、最適部分構造の解釈は非常に柔軟で、異なる問題では異なる意味を持ちます。
|
||||
では、前節の階段昇り問題には最適部分構造があるのでしょうか。その目的は方法数を求めることで、一見すると計数問題です。しかし問い方を変えて「最大の方法数を求める」とすると、意外にも、**問題の変更前後は等価であるにもかかわらず、最適部分構造が現れます**。すなわち、第 $n$ 段の最大方法数は第 $n-1$ 段と第 $n-2$ 段の最大方法数の和に等しいのです。このように、最適部分構造の解釈は比較的柔軟であり、問題によって意味合いが異なります。
|
||||
|
||||
状態遷移方程式と初期状態 $dp[1] = cost[1]$ および $dp[2] = cost[2]$ に従って、動的プログラミングコードを得ることができます:
|
||||
状態遷移方程式と初期状態 $dp[1] = cost[1]$ および $dp[2] = cost[2]$ に基づいて、次の動的計画法コードが得られます。
|
||||
|
||||
```src
|
||||
[file]{min_cost_climbing_stairs_dp}-[class]{}-[func]{min_cost_climbing_stairs_dp}
|
||||
```
|
||||
|
||||
下の図は上記コードの動的プログラミングプロセスを示しています。
|
||||
下図は上記コードの動的計画法の過程を示しています。
|
||||
|
||||

|
||||

|
||||
|
||||
この問題も空間最適化が可能で、1次元を0に圧縮し、空間計算量を $O(n)$ から $O(1)$ に削減できます:
|
||||
この問題では空間最適化も可能であり、一次元をゼロ次元まで圧縮することで、空間計算量を $O(n)$ から $O(1)$ に削減できます。
|
||||
|
||||
```src
|
||||
[file]{min_cost_climbing_stairs_dp}-[class]{}-[func]{min_cost_climbing_stairs_dp_comp}
|
||||
```
|
||||
|
||||
## 無記憶性
|
||||
## 無後効性
|
||||
|
||||
無記憶性は動的プログラミングが問題解決に効果的であることを可能にする重要な特徴の1つです。その定義は:**特定の状態が与えられたとき、その将来の発展は現在の状態のみに関連し、過去に経験したすべての状態とは無関係である**。
|
||||
無後効性は、動的計画法が問題を効率よく解ける重要な特性の一つであり、その定義は次のとおりです。**ある確定した状態が与えられたとき、その後の発展は現在の状態のみに依存し、過去に経たすべての状態には依存しない**。
|
||||
|
||||
階段登り問題を例に取ると、状態 $i$ が与えられたとき、それは状態 $i+1$ と $i+2$ に発展し、それぞれ1段ジャンプと2段ジャンプに対応します。これら2つの選択をするとき、状態 $i$ より前の状態を考慮する必要はありません。なぜなら、それらは状態 $i$ の将来に影響しないからです。
|
||||
階段昇り問題を例にすると、状態 $i$ が与えられたとき、そこから状態 $i+1$ と状態 $i+2$ へ発展し、それぞれ $1$ 段進む場合と $2$ 段進む場合に対応します。この二つの選択を行う際、状態 $i$ より前の状態を考慮する必要はなく、それらは状態 $i$ の将来に影響を与えません。
|
||||
|
||||
しかし、階段登り問題に制約を追加すると、状況が変わります。
|
||||
しかし、階段昇り問題に制約を一つ追加すると、状況は変わります。
|
||||
|
||||
!!! question "制約付き階段登り"
|
||||
!!! question "制約付き階段昇り"
|
||||
|
||||
$n$ 段の階段があり、毎回1段または2段上ることができますが、**1段を2回連続でジャンプすることはできません**。頂上に登る方法は何通りありますか?
|
||||
全部で $n$ 段ある階段が与えられ、各ステップで $1$ 段または $2$ 段上ることができます。**ただし、連続する 2 回で $1$ 段ずつ上ることはできません**。頂上まで上る方法は何通りあるでしょうか。
|
||||
|
||||
下の図に示すように、3段目に登る実行可能な選択肢は2つだけで、1段を3回連続でジャンプする選択肢は制約条件を満たさないため破棄されます。
|
||||
下図に示すように、第 $3$ 段まで上る実行可能な方法は $2$ 通りしか残りません。そのうち、$1$ 段ずつ 3 回連続で上る方法は制約を満たさないため除外されます。
|
||||
|
||||

|
||||

|
||||
|
||||
この問題では、前回が1段ジャンプだった場合、次回は必ず2段ジャンプでなければなりません。これは**次のステップの選択が現在の状態(現在の階段段数)だけでは独立して決定できず、前の状態(前回の階段段数)にも依存する**ことを意味します。
|
||||
この問題では、前回が $1$ 段上りだった場合、次回は必ず $2$ 段上らなければなりません。これは、**次の一手が現在の状態(現在いる階段の段数)だけでは独立に決まらず、一つ前の状態(前回いた段数)にも関係する**ことを意味します。
|
||||
|
||||
この問題がもはや無記憶性を満たさず、状態遷移方程式 $dp[i] = dp[i-1] + dp[i-2]$ も失敗することは容易にわかります。なぜなら $dp[i-1]$ は今回の1段ジャンプを表しますが、多くの「前回が1段ジャンプだった」選択肢を含んでおり、制約を満たすためにはこれらを直接 $dp[i]$ に含めることはできません。
|
||||
容易に分かるように、この問題はもはや無後効性を満たしておらず、状態遷移方程式 $dp[i] = dp[i-1] + dp[i-2]$ も成立しません。というのも、$dp[i-1]$ は今回 $1$ 段上る場合を表しますが、その中には「前回も $1$ 段上ってきた」方法が多数含まれており、制約を満たすためには $dp[i-1]$ をそのまま $dp[i]$ に加えることができないからです。
|
||||
|
||||
このため、状態定義を拡張する必要があります:**状態 $[i, j]$ は $i$ 段目にいて、前回が $j$ 段ジャンプだったことを表す**。ここで $j \in \{1, 2\}$ です。この状態定義は前回が1段ジャンプだったか2段ジャンプだったかを効果的に区別し、現在の状態がどこから来たかを適切に判断できます。
|
||||
このため、状態定義を拡張する必要があります。**状態 $[i, j]$ は、第 $i$ 段にいて前回に $j$ 段上ったことを表す**とし、ここで $j \in \{1, 2\}$ です。この状態定義により、前回が $1$ 段上りか $2$ 段上りかを有効に区別でき、現在の状態がどこから来たのかを判断できます。
|
||||
|
||||
- 前回が1段ジャンプだった場合、前々回は必ず2段ジャンプを選択していたはずです。つまり、$dp[i, 1]$ は $dp[i-1, 2]$ からのみ遷移できます。
|
||||
- 前回が2段ジャンプだった場合、前々回は1段ジャンプまたは2段ジャンプを選択できました。つまり、$dp[i, 2]$ は $dp[i-2, 1]$ または $dp[i-2, 2]$ から遷移できます。
|
||||
- 前回に $1$ 段上った場合、その前の回は $2$ 段上りしか選べないため、$dp[i, 1]$ は $dp[i-1, 2]$ からのみ遷移できます。
|
||||
- 前回に $2$ 段上った場合、その前の回は $1$ 段上りまたは $2$ 段上りを選べるため、$dp[i, 2]$ は $dp[i-2, 1]$ または $dp[i-2, 2]$ から遷移できます。
|
||||
|
||||
下の図に示すように、$dp[i, j]$ は状態 $[i, j]$ の解の数を表します。この時点で、状態遷移方程式は次のようになります:
|
||||
下図に示すように、この定義のもとでは $dp[i, j]$ は状態 $[i, j]$ に対応する方法数を表します。このとき状態遷移方程式は次のようになります。
|
||||
|
||||
$$
|
||||
\begin{cases}
|
||||
@@ -82,20 +82,20 @@ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2]
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||

|
||||

|
||||
|
||||
最終的に、$dp[n, 1] + dp[n, 2]$ を返せばよく、この2つの合計が $n$ 段目に登る解の総数を表します:
|
||||
最終的に、$dp[n, 1] + dp[n, 2]$ を返せば十分であり、その和が第 $n$ 段まで上る方法の総数を表します。
|
||||
|
||||
```src
|
||||
[file]{climbing_stairs_constraint_dp}-[class]{}-[func]{climbing_stairs_constraint_dp}
|
||||
```
|
||||
|
||||
上記のケースでは、前の状態のみを考慮すればよいため、状態定義を拡張することで依然として無記憶性を満たすことができます。しかし、一部の問題では非常に深刻な「状態効果」があります。
|
||||
上の例では、追加で考慮すべきなのは一つ前の状態だけであるため、状態定義を拡張することで問題を再び無後効性に適合させることができます。しかし、問題によっては非常に強い「後効性」があります。
|
||||
|
||||
!!! question "障害物生成付き階段登り"
|
||||
!!! question "階段昇りと障害物生成"
|
||||
|
||||
$n$ 段の階段があり、毎回1段または2段上ることができます。**$i$ 段目に登ったとき、システムが自動的に $2i$ 段目に障害物を置き、その後のすべてのラウンドで $2i$ 段目にジャンプすることが禁止される**と規定されています。例えば、最初の2ラウンドで2段目と3段目にジャンプした場合、その後は4段目と6段目にジャンプできません。頂上に登る方法は何通りありますか?
|
||||
全部で $n$ 段ある階段が与えられ、各ステップで $1$ 段または $2$ 段上ることができます。**第 $i$ 段に到達すると、システムは自動的に第 $2i$ 段に障害物を置き、それ以降はどの回でも第 $2i$ 段へ跳ぶことができない**とします。例えば、最初の 2 回でそれぞれ第 $2$ 段、第 $3$ 段に到達した場合、その後は第 $4$ 段と第 $6$ 段に跳ぶことはできません。頂上まで上る方法は何通りあるでしょうか。
|
||||
|
||||
この問題では、次のジャンプはすべての過去の状態に依存します。各ジャンプがより高い段に障害物を置き、将来のジャンプに影響するからです。このような問題では、動的プログラミングはしばしば解決に苦労します。
|
||||
この問題では、次の跳躍が過去のすべての状態に依存します。なぜなら、各跳躍がより高い段に障害物を設置し、将来の跳躍に影響するからです。この種の問題は、動的計画法では解きにくいことが多いです。
|
||||
|
||||
実際、多くの複雑な組み合わせ最適化問題(巡回セールスマン問題など)は無記憶性を満たしません。このような問題に対しては、通常、ヒューリスティック探索、遺伝的アルゴリズム、強化学習などの他の方法を選択して、限られた時間内に使用可能な局所最適解を得ます。
|
||||
実際、多くの複雑な組合せ最適化問題(例えば巡回セールスマン問題)は無後効性を満たしません。このような問題に対しては、通常、ヒューリスティック探索、遺伝的アルゴリズム、強化学習などの他の方法を用いて、限られた時間内に実用的な局所最適解を得ます。
|
||||
|
||||
@@ -1,65 +1,65 @@
|
||||
# 動的プログラミング問題解決アプローチ
|
||||
# 動的計画法の問題解決の考え方
|
||||
|
||||
前の2つのセクションでは、動的プログラミング問題の主要な特徴を紹介しました。次に、より実用的な2つの問題を一緒に探索しましょう。
|
||||
前の 2 節では動的計画法の問題の主要な特徴を紹介しました。ここからは、さらに実用的な 2 つの問題を一緒に考えていきます。
|
||||
|
||||
1. 問題が動的プログラミング問題かどうかをどのように判断するか?
|
||||
2. 動的プログラミング問題を解決する完全なステップは何か?
|
||||
1. ある問題が動的計画法の問題かどうかを、どのように判断すればよいでしょうか?
|
||||
2. 動的計画法の問題を解くには、どこから着手し、完全な手順はどのようなものでしょうか?
|
||||
|
||||
## 問題の判定
|
||||
|
||||
一般的に言えば、問題が重複する部分問題、最適部分構造を含み、無記憶性を示す場合、通常動的プログラミング解法に適しています。しかし、問題の説明から直接これらの特徴を抽出することはしばしば困難です。したがって、通常は条件を緩和し、**まず問題がバックトラッキング(全探索)を使用した解決に適しているかどうかを観察**します。
|
||||
一般に、ある問題が重複部分問題と最適部分構造を含み、さらに無後效性を満たしているなら、通常は動的計画法で解くのに適しています。しかし、問題文からこれらの性質を直接読み取るのは簡単ではありません。そのため通常は条件を少し緩めて、**まずその問題がバックトラッキング(全探索)で解くのに適しているか**を観察します。
|
||||
|
||||
**バックトラッキングに適した問題は通常「決定木モデル」に適合**し、これは木構造を使用して記述でき、各ノードは決定を表し、各パスは決定のシーケンスを表します。
|
||||
**バックトラッキングで解くのに適した問題は、通常「決定木モデル」を満たします**。この種の問題は木構造で表現でき、各ノードは 1 つの決定を表し、各経路は 1 つの決定列を表します。
|
||||
|
||||
言い換えると、問題が明示的な決定概念を含み、解が一連の決定を通じて生成される場合、それは決定木モデルに適合し、通常バックトラッキングを使用して解決できます。
|
||||
言い換えると、問題に明確な決定の概念が含まれており、解が一連の決定によって生成されるなら、その問題は決定木モデルを満たし、通常はバックトラッキングで解くことができます。
|
||||
|
||||
この基礎の上で、動的プログラミング問題を判定するための「ボーナスポイント」があります。
|
||||
これに加えて、動的計画法の問題には判定のための「加点要素」もあります。
|
||||
|
||||
- 問題に最大化(最小化)または最も(最も少ない)最適な解を見つけるという記述が含まれている。
|
||||
- 問題の状態がリスト、多次元行列、または木を使用して表現でき、状態がその周囲の状態と再帰関係を持っている。
|
||||
- 問題文に最大(最小)や最多(最少)などの最適化に関する記述がある。
|
||||
- 問題の状態が配列、多次元行列、または木で表現でき、ある状態とその周辺の状態の間に漸化的な関係がある。
|
||||
|
||||
対応して、「ペナルティポイント」もあります。
|
||||
反対に、「減点要素」もあります。
|
||||
|
||||
- 問題の目標は最適解だけでなく、すべての可能な解を見つけることである。
|
||||
- 問題の説明に順列と組み合わせの明らかな特徴があり、特定の複数の解を返す必要がある。
|
||||
- 問題の目的が最適解を求めることではなく、あり得るすべての解を列挙することである。
|
||||
- 問題文に明確な順列・組合せの特徴があり、具体的な複数の解を返す必要がある。
|
||||
|
||||
問題が決定木モデルに適合し、比較的明らかな「ボーナスポイント」を持つ場合、それが動的プログラミング問題であると仮定し、解決プロセス中に検証できます。
|
||||
ある問題が決定木モデルを満たし、さらに比較的明確な「加点要素」を備えているなら、その問題は動的計画法の問題であると仮定し、解く過程でそれを検証できます。
|
||||
|
||||
## 問題解決ステップ
|
||||
## 問題を解く手順
|
||||
|
||||
動的プログラミング問題解決プロセスは問題の性質と難易度によって異なりますが、一般的に次のステップに従います:決定の記述、状態の定義、$dp$ テーブルの確立、状態遷移方程式の導出、境界条件の決定など。
|
||||
動的計画法の解法の流れは問題の性質や難易度によって異なりますが、通常は次の手順に従います。すなわち、決定を記述し、状態を定義し、$dp$ テーブルを構築し、状態遷移方程式を導出し、境界条件を定めます。
|
||||
|
||||
問題解決ステップをより具体的に説明するために、古典的な問題「最小経路和」を例として使用します。
|
||||
解法の手順をより具体的に示すために、ここでは古典的な問題である「最小経路和」を例にします。
|
||||
|
||||
!!! question
|
||||
|
||||
$n \times m$ の二次元グリッド `grid` が与えられ、グリッドの各セルには負でない整数が含まれ、そのセルのコストを表します。ロボットは左上のセルから始まり、各ステップで下または右にのみ移動でき、右下のセルに到達するまで続けます。左上から右下への最小経路和を返してください。
|
||||
$n \times m$ の 2 次元グリッド `grid` が与えられます。グリッドの各セルには非負整数が格納されており、そのセルのコストを表します。ロボットは左上のセルを始点とし、毎回下または右に 1 マスだけ移動して、右下のセルまで進みます。左上から右下までの最小経路和を返してください。
|
||||
|
||||
下の図は例を示しており、与えられたグリッドの最小経路和は $13$ です。
|
||||
次の図は 1 つの例を示しており、このグリッドの最小経路和は $13$ です。
|
||||
|
||||

|
||||

|
||||
|
||||
**第1ステップ:各ラウンドの決定を考え、状態を定義し、それにより $dp$ テーブルを得る**
|
||||
**ステップ 1:各ラウンドの決定を考え、状態を定義して、$dp$ テーブルを得る**
|
||||
|
||||
この問題の各ラウンドの決定は、現在のセルから下または右に1ステップ移動することです。現在のセルの行と列のインデックスが $[i, j]$ であると仮定すると、下または右に移動した後、インデックスは $[i+1, j]$ または $[i, j+1]$ になります。したがって、状態には2つの変数が含まれるべきです:行インデックスと列インデックス、$[i, j]$ と表記されます。
|
||||
この問題における各ラウンドの決定は、現在のマスから下または右へ 1 マス進むことです。現在のマスの行・列インデックスを $[i, j]$ とすると、下または右へ 1 マス進んだ後のインデックスは $[i+1, j]$ または $[i, j+1]$ になります。したがって、状態には行インデックスと列インデックスの 2 つの変数を含め、$[i, j]$ と表します。
|
||||
|
||||
状態 $[i, j]$ は部分問題に対応します:開始点 $[0, 0]$ から $[i, j]$ への最小経路和、$dp[i, j]$ と表記されます。
|
||||
状態 $[i, j]$ に対応する部分問題は、始点 $[0, 0]$ から $[i, j]$ まで進む最小経路和であり、その解を $dp[i, j]$ と記します。
|
||||
|
||||
このようにして、下の図に示す二次元 $dp$ 行列を得ます。そのサイズは入力グリッド $grid$ と同じです。
|
||||
これで、次の図に示す 2 次元の $dp$ 行列が得られます。そのサイズは入力グリッド $grid$ と同じです。
|
||||
|
||||

|
||||

|
||||
|
||||
!!! note
|
||||
|
||||
動的プログラミングとバックトラッキングは決定のシーケンスとして記述でき、状態はすべての決定変数から構成されます。問題解決の進行を記述するすべての変数を含むべきで、次の状態を導出するのに十分な情報を含んでいる必要があります。
|
||||
動的計画法とバックトラッキングの過程は、いずれも 1 つの決定列として記述できます。そして状態は、すべての決定変数から構成されます。状態には解法の進行状況を表すすべての変数が含まれているべきであり、次の状態を導くのに十分な情報を持っている必要があります。
|
||||
|
||||
各状態は 1 つの部分問題に対応しており、すべての部分問題の解を保存するために $dp$ テーブルを定義します。状態の各独立変数は、$dp$ テーブルの 1 つの次元に対応します。本質的に、$dp$ テーブルは状態と部分問題の解との対応関係です。
|
||||
|
||||
各状態は部分問題に対応し、すべての部分問題の解を保存するための $dp$ テーブルを定義します。状態の各独立変数は $dp$ テーブルの次元です。本質的に、$dp$ テーブルは状態と部分問題の解の間のマッピングです。
|
||||
**ステップ 2:最適部分構造を見つけ、状態遷移方程式を導出する**
|
||||
|
||||
**第2ステップ:最適部分構造を特定し、状態遷移方程式を導出する**
|
||||
状態 $[i, j]$ は、上のマス $[i-1, j]$ または左のマス $[i, j-1]$ からしか遷移してきません。したがって最適部分構造は、$[i, j]$ に到達する最小経路和が、$[i, j-1]$ の最小経路和と $[i-1, j]$ の最小経路和のうち小さい方によって決まる、ということです。
|
||||
|
||||
状態 $[i, j]$ について、それは上のセル $[i-1, j]$ または左のセル $[i, j-1]$ からのみ導出できます。したがって、最適部分構造は:$[i, j]$ に到達する最小経路和は、$[i, j-1]$ と $[i-1, j]$ の最小経路和の小さい方によって決定されます。
|
||||
|
||||
上記の分析に基づいて、下の図に示す状態遷移方程式を導出できます:
|
||||
以上の分析から、次の図に示す状態遷移方程式を導くことができます。
|
||||
|
||||
$$
|
||||
dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
|
||||
@@ -69,75 +69,75 @@ $$
|
||||
|
||||
!!! note
|
||||
|
||||
定義された $dp$ テーブルに基づいて、元の問題と部分問題の関係を考え、部分問題の最適解から元の問題の最適解をどのように構築するか、つまり最適部分構造を見つけます。
|
||||
定義済みの $dp$ テーブルに基づいて、元の問題と部分問題の関係を考え、部分問題の最適解から元の問題の最適解を構成する方法、すなわち最適部分構造を見つけます。
|
||||
|
||||
最適部分構造を特定したら、それを使用して状態遷移方程式を構築できます。
|
||||
ひとたび最適部分構造が見つかれば、それを使って状態遷移方程式を構築できます。
|
||||
|
||||
**第3ステップ:境界条件と状態遷移順序を決定する**
|
||||
**ステップ 3:境界条件と状態遷移の順序を決める**
|
||||
|
||||
この問題では、最初の行の状態は左の状態からのみ来ることができ、最初の列の状態は上の状態からのみ来ることができるため、最初の行 $i = 0$ と最初の列 $j = 0$ が境界条件です。
|
||||
この問題では、先頭行にある状態は左の状態からしか得られず、先頭列にある状態は上の状態からしか得られません。したがって、先頭行 $i = 0$ と先頭列 $j = 0$ が境界条件になります。
|
||||
|
||||
下の図に示すように、各セルは左のセルと上のセルから導出されるため、ループを使用して行列を走査し、外側のループは行を反復し、内側のループは列を反復します。
|
||||
次の図に示すように、各マスは左のマスと上のマスから遷移してくるため、ループを用いて行列を走査します。外側のループで各行を、内側のループで各列を走査します。
|
||||
|
||||

|
||||

|
||||
|
||||
!!! note
|
||||
|
||||
境界条件は動的プログラミングで $dp$ テーブルを初期化するために使用され、探索では枝刈りに使用されます。
|
||||
境界条件は、動的計画法では $dp$ テーブルの初期化に使われ、探索では枝刈りに使われます。
|
||||
|
||||
状態遷移の順序で重要なのは、現在の問題の解を計算するときに、それが依存するより小さな部分問題の解がすべてすでに正しく計算済みであることを保証する点です。
|
||||
|
||||
状態遷移順序の核心は、現在の問題の解を計算するとき、それが依存するすべての小さな部分問題が既に正しく計算されていることを確保することです。
|
||||
以上の分析により、すでに動的計画法のコードを直接書くことができます。しかし、部分問題への分解はトップダウンの考え方であるため、「力任せ探索 $\rightarrow$ メモ化探索 $\rightarrow$ 動的計画法」の順に実装するほうが、思考の流れにはより自然です。
|
||||
|
||||
上記の分析に基づいて、動的プログラミングコードを直接書くことができます。しかし、部分問題の分解はトップダウンアプローチであるため、「力任せ探索 → メモ化探索 → 動的プログラミング」の順序で実装することが習慣的な思考により適合します。
|
||||
### 方法 1:力任せ探索
|
||||
|
||||
### 方法1:力任せ探索
|
||||
状態 $[i, j]$ から探索を開始し、より小さな状態 $[i-1, j]$ と $[i, j-1]$ へと分解していきます。再帰関数には次の要素が含まれます。
|
||||
|
||||
状態 $[i, j]$ から探索を開始し、それを常により小さな状態 $[i-1, j]$ と $[i, j-1]$ に分解します。再帰関数には以下の要素が含まれます。
|
||||
|
||||
- **再帰パラメータ**:状態 $[i, j]$。
|
||||
- **戻り値**:$[0, 0]$ から $[i, j]$ への最小経路和 $dp[i, j]$。
|
||||
- **再帰引数**:状態 $[i, j]$ 。
|
||||
- **戻り値**:$[0, 0]$ から $[i, j]$ までの最小経路和 $dp[i, j]$ 。
|
||||
- **終了条件**:$i = 0$ かつ $j = 0$ のとき、コスト $grid[0, 0]$ を返す。
|
||||
- **枝刈り**:$i < 0$ または $j < 0$ でインデックスが範囲外のとき、コスト $+\infty$ を返し、実行不可能性を表す。
|
||||
- **枝刈り**:$i < 0$ または $j < 0$ でインデックスが範囲外になった場合、コスト $+\infty$ を返し、実行不可能であることを表す。
|
||||
|
||||
実装コードは以下の通りです:
|
||||
実装コードは次のとおりです。
|
||||
|
||||
```src
|
||||
[file]{min_path_sum}-[class]{}-[func]{min_path_sum_dfs}
|
||||
```
|
||||
|
||||
下の図は $dp[2, 1]$ を根とする再帰木を示しており、いくつかの重複する部分問題を含み、その数はグリッド `grid` のサイズが増加すると急激に増加します。
|
||||
次の図は、$dp[2, 1]$ を根ノードとする再帰木を示しています。この中にはいくつかの重複部分問題が含まれており、その数はグリッド `grid` のサイズが大きくなるにつれて急激に増加します。
|
||||
|
||||
本質的に、重複する部分問題の理由は:**左上隅から特定のセルに到達する複数のパスが存在する**ことです。
|
||||
本質的に、重複部分問題が生じる理由は、**左上からあるセルへ到達する経路が複数存在すること**にあります。
|
||||
|
||||

|
||||
|
||||
各状態には下と右の2つの選択があるため、左上隅から右下隅までの総ステップ数は $m + n - 2$ で、最悪時間計算量は $O(2^{m + n})$ です。この計算方法はグリッドエッジ近くの状況を考慮していないことに注意してください。ネットワークエッジに到達したとき、選択肢が1つしか残らないため、実際のパス数はより少なくなります。
|
||||
各状態には下と右の 2 通りの選択肢があり、左上から右下まで進むには合計で $m + n - 2$ 歩必要です。したがって最悪時間計算量は $O(2^{m + n})$ です。ここで、$n$ と $m$ はそれぞれグリッドの行数と列数を表します。なお、この見積もりではグリッド境界付近の状況を考慮していません。境界に達すると選択肢は 1 つだけになるため、実際の経路数はこれより少なくなります。
|
||||
|
||||
### 方法2:メモ化探索
|
||||
### 方法 2:メモ化探索
|
||||
|
||||
グリッド `grid` と同じサイズのメモリスト `mem` を導入し、様々な部分問題の解を記録し、重複する部分問題を枝刈りします:
|
||||
グリッド `grid` と同じサイズのメモ配列 `mem` を導入し、各部分問題の解を記録して、重複部分問題を枝刈りします。
|
||||
|
||||
```src
|
||||
[file]{min_path_sum}-[class]{}-[func]{min_path_sum_dfs_mem}
|
||||
```
|
||||
|
||||
下の図に示すように、メモ化を導入した後、すべての部分問題の解は一度だけ計算される必要があるため、時間計算量は状態の総数、つまりグリッドサイズ $O(nm)$ に依存します。
|
||||
次の図に示すように、メモ化を導入すると、すべての部分問題の解は 1 回だけ計算すればよくなります。したがって時間計算量は状態総数、すなわちグリッドサイズの $O(nm)$ に依存します。
|
||||
|
||||

|
||||
|
||||
### 方法3:動的プログラミング
|
||||
### 方法 3:動的計画法
|
||||
|
||||
動的プログラミング解法を反復的に実装します。コードは以下の通りです:
|
||||
反復に基づいて動的計画法の解法を実装すると、コードは次のようになります。
|
||||
|
||||
```src
|
||||
[file]{min_path_sum}-[class]{}-[func]{min_path_sum_dp}
|
||||
```
|
||||
|
||||
下の図は最小経路和の状態遷移プロセスを示し、グリッド全体を走査するため、**時間計算量は $O(nm)$** です。
|
||||
次の図は最小経路和の状態遷移の過程を示しています。グリッド全体を走査するため、**時間計算量は $O(nm)$** です。
|
||||
|
||||
配列 `dp` のサイズは $n \times m$ であるため、**空間計算量は $O(nm)$** です。
|
||||
|
||||
=== "<1>"
|
||||

|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
@@ -174,9 +174,9 @@ $$
|
||||
|
||||
### 空間最適化
|
||||
|
||||
各セルは左と上のセルのみに関連するため、単一行配列を使用して $dp$ テーブルを実装できます。
|
||||
各マスは左のマスと上のマスにのみ関係するため、1 行の配列だけを使って $dp$ テーブルを実装できます。
|
||||
|
||||
配列 `dp` は1行の状態のみを表現できるため、最初の列の状態を事前に初期化できず、各行を走査するときに更新することに注意してください:
|
||||
ただし、配列 `dp` は 1 行分の状態しか表せないため、先頭列の状態を事前に初期化することはできず、各行を走査するときに更新する必要があります。
|
||||
|
||||
```src
|
||||
[file]{min_path_sum}-[class]{}-[func]{min_path_sum_dp_comp}
|
||||
|
||||
@@ -1,80 +1,80 @@
|
||||
# 編集距離問題
|
||||
|
||||
編集距離は、レーベンシュタイン距離とも呼ばれ、一つの文字列を別の文字列に変換するために必要な最小修正回数を指し、情報検索や自然言語処理で2つのシーケンス間の類似度を測定するためによく使用されます。
|
||||
編集距離は、Levenshtein 距離とも呼ばれ、2つの文字列の相互変換に必要な最小の編集回数を指し、通常は情報検索や自然言語処理において2つの系列の類似度を測るために用いられます。
|
||||
|
||||
!!! question
|
||||
|
||||
2つの文字列 $s$ と $t$ が与えられたとき、$s$ を $t$ に変換するために必要な最小編集回数を返してください。
|
||||
2つの文字列 $s$ と $t$ を入力し、$s$ を $t$ に変換するのに必要な最小編集回数を返してください。
|
||||
|
||||
1つの文字列に対して3種類の編集操作を行えます。1文字の挿入、1文字の削除、任意の文字への置換です。
|
||||
|
||||
文字列に対して3種類の編集を実行できます:文字の挿入、文字の削除、または文字を他の任意の文字に置換。
|
||||
下図に示すように、`kitten` を `sitting` に変換するには 3 回の編集が必要で、内訳は 2 回の置換と 1 回の挿入です。`hello` を `algo` に変換する場合も 3 回必要で、内訳は 2 回の置換と 1 回の削除です。
|
||||
|
||||
下の図に示すように、`kitten` を `sitting` に変換するには3回の編集が必要で、2回の置換と1回の挿入を含みます。`hello` を `algo` に変換するには3ステップが必要で、2回の置換と1回の削除を含みます。
|
||||

|
||||
|
||||

|
||||
**編集距離問題は決定木モデルで自然に説明できます**。文字列が木のノードに対応し、1回の決定(1回の編集操作)が木の1本の辺に対応します。
|
||||
|
||||
**編集距離問題は決定木モデルで自然に説明できます**。文字列は木のノードに対応し、1ラウンドの決定(編集操作)は木のエッジに対応します。
|
||||
下図に示すように、操作に制限がない場合、各ノードからは多くの辺を派生でき、それぞれの辺が1種類の操作に対応します。これは `hello` から `algo` への変換に多くの経路があり得ることを意味します。
|
||||
|
||||
下の図に示すように、操作に制限がない場合、各ノードは多くのエッジを導出でき、それぞれが1つの操作に対応するため、`hello` を `algo` に変換する可能な経路は多数あります。
|
||||
決定木の観点から見ると、本問の目標はノード `hello` とノード `algo` の間の最短経路を求めることです。
|
||||
|
||||
決定木の観点から、この問題の目標は、ノード `hello` とノード `algo` の間の最短経路を見つけることです。
|
||||

|
||||
|
||||

|
||||
### 動的計画法の考え方
|
||||
|
||||
### 動的プログラミングアプローチ
|
||||
**第1ステップ:各ラウンドの決定を考え、状態を定義して、$dp$ テーブルを得る**
|
||||
|
||||
**ステップ1:各ラウンドの決定を考え、状態を定義し、それにより $dp$ テーブルを得る**
|
||||
各ラウンドの決定は、文字列 $s$ に対して1回の編集操作を行うことです。
|
||||
|
||||
各ラウンドの決定は、文字列 $s$ に対して1つの編集操作を実行することを含みます。
|
||||
編集操作の過程で問題の規模が徐々に小さくなることを期待します。そうして初めて部分問題を構築できます。文字列 $s$ と $t$ の長さをそれぞれ $n$ と $m$ とし、まず両文字列の末尾の文字 $s[n-1]$ と $t[m-1]$ を考えます。
|
||||
|
||||
編集プロセス中に問題のサイズを段階的に縮小することを目指し、これにより部分問題を構築できます。文字列 $s$ と $t$ の長さをそれぞれ $n$ と $m$ とします。まず、両方の文字列の末尾文字 $s[n-1]$ と $t[m-1]$ を考慮します。
|
||||
- $s[n-1]$ と $t[m-1]$ が同じなら、それらをスキップして、直接 $s[n-2]$ と $t[m-2]$ を考えます。
|
||||
- $s[n-1]$ と $t[m-1]$ が異なるなら、$s$ に対して1回の編集(挿入、削除、置換)を行い、両文字列の末尾の文字を同じにします。そうすることでそれらをスキップし、より小さい問題を考えられます。
|
||||
|
||||
- $s[n-1]$ と $t[m-1]$ が同じ場合、それらをスキップして直接 $s[n-2]$ と $t[m-2]$ を考慮できます。
|
||||
- $s[n-1]$ と $t[m-1]$ が異なる場合、$s$ に対して1つの編集(挿入、削除、置換)を実行して、2つの文字列の末尾文字を一致させ、それらをスキップしてより小規模な問題を考慮できるようにする必要があります。
|
||||
つまり、文字列 $s$ に対する各ラウンドの決定(編集操作)は、$s$ と $t$ における残りの未一致文字を変化させます。したがって、状態は現在 $s$ と $t$ で考えている第 $i$ と第 $j$ 文字とし、$[i, j]$ と記します。
|
||||
|
||||
したがって、文字列 $s$ での各ラウンドの決定(編集操作)は、$s$ と $t$ でマッチされる残りの文字を変更します。したがって、状態は $s$ と $t$ で現在考慮されている $i$ 番目と $j$ 番目の文字であり、$[i, j]$ と表記されます。
|
||||
状態 $[i, j]$ に対応する部分問題は、**$s$ の先頭 $i$ 文字を $t$ の先頭 $j$ 文字に変換するのに必要な最小編集回数**です。
|
||||
|
||||
状態 $[i, j]$ は部分問題に対応します:**$s$ の最初の $i$ 文字を $t$ の最初の $j$ 文字に変更するために必要な最小編集回数**。
|
||||
これにより、サイズが $(i+1) \times (j+1)$ の2次元 $dp$ テーブルが得られます。
|
||||
|
||||
これから、サイズ $(i+1) \times (j+1)$ の二次元 $dp$ テーブルを得ます。
|
||||
**第2ステップ:最適部分構造を見つけ、状態遷移方程式を導く**
|
||||
|
||||
**ステップ2:最適部分構造を特定し、状態遷移方程式を導出する**
|
||||
部分問題 $dp[i, j]$ を考えます。これに対応する2つの文字列の末尾文字は $s[i-1]$ と $t[j-1]$ であり、編集操作の違いに応じて下図の3つの場合に分けられます。
|
||||
|
||||
部分問題 $dp[i, j]$ を考慮すると、これに対応する2つの文字列の末尾文字は $s[i-1]$ と $t[j-1]$ であり、下の図に示すように3つのシナリオに分けることができます。
|
||||
|
||||
1. $s[i-1]$ の後に $t[j-1]$ を追加すると、残りの部分問題は $dp[i, j-1]$ です。
|
||||
2. $s[i-1]$ を削除すると、残りの部分問題は $dp[i-1, j]$ です。
|
||||
3. $s[i-1]$ を $t[j-1]$ に置換すると、残りの部分問題は $dp[i-1, j-1]$ です。
|
||||
1. $s[i-1]$ の後ろに $t[j-1]$ を追加する。このとき残る部分問題は $dp[i, j-1]$ です。
|
||||
2. $s[i-1]$ を削除する。このとき残る部分問題は $dp[i-1, j]$ です。
|
||||
3. $s[i-1]$ を $t[j-1]$ に置き換える。このとき残る部分問題は $dp[i-1, j-1]$ です。
|
||||
|
||||

|
||||
|
||||
上記の分析に基づいて、最適部分構造を決定できます:$dp[i, j]$ の最小編集回数は、$dp[i, j-1]$、$dp[i-1, j]$、$dp[i-1, j-1]$ の中の最小値に編集ステップ $1$ を加えたものです。対応する状態遷移方程式は:
|
||||
以上の分析から、最適部分構造は次のように得られます。$dp[i, j]$ の最小編集回数は、$dp[i, j-1]$、$dp[i-1, j]$、$dp[i-1, j-1]$ の3つのうち最小の編集回数に、今回の編集回数 $1$ を加えたものです。対応する状態遷移方程式は次のとおりです:
|
||||
|
||||
$$
|
||||
dp[i, j] = \min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1
|
||||
$$
|
||||
|
||||
注意してください。**$s[i-1]$ と $t[j-1]$ が同じ場合、現在の文字に対して編集は必要ありません**。この場合、状態遷移方程式は:
|
||||
注意すべき点として、**$s[i-1]$ と $t[j-1]$ が同じ場合、現在の文字を編集する必要はありません**。この場合の状態遷移方程式は次のとおりです:
|
||||
|
||||
$$
|
||||
dp[i, j] = dp[i-1, j-1]
|
||||
$$
|
||||
|
||||
**ステップ3:境界条件と状態遷移の順序を決定する**
|
||||
**第3ステップ:境界条件と状態遷移の順序を決める**
|
||||
|
||||
両方の文字列が空の場合、編集回数は $0$ です。つまり、$dp[0, 0] = 0$ です。$s$ が空で $t$ が空でない場合、最小編集回数は $t$ の長さに等しく、つまり最初の行 $dp[0, j] = j$ です。$s$ が空でなく $t$ が空の場合、最小編集回数は $s$ の長さに等しく、つまり最初の列 $dp[i, 0] = i$ です。
|
||||
2つの文字列がともに空のとき、編集回数は $0$、すなわち $dp[0, 0] = 0$ です。$s$ が空で $t$ が空でないとき、最小編集回数は $t$ の長さに等しいため、先頭行は $dp[0, j] = j$ です。$s$ が空でなく $t$ が空のとき、最小編集回数は $s$ の長さに等しいため、先頭列は $dp[i, 0] = i$ です。
|
||||
|
||||
状態遷移方程式を観察すると、$dp[i, j]$ の解決は左、上、左上の解に依存するため、二重ループを使用して正しい順序で $dp$ テーブル全体を走査できます。
|
||||
状態遷移方程式を観察すると、$dp[i, j]$ の解は左、上、左上の解に依存します。そのため、2重ループで $dp$ テーブル全体を順方向に走査すれば十分です。
|
||||
|
||||
### コード実装
|
||||
### コードの実装
|
||||
|
||||
```src
|
||||
[file]{edit_distance}-[class]{}-[func]{edit_distance_dp}
|
||||
```
|
||||
|
||||
下の図に示すように、編集距離問題の状態遷移プロセスはナップサック問題と非常に似ており、二次元グリッドを埋めることと見なすことができます。
|
||||
下図に示すように、編集距離問題の状態遷移の過程はナップサック問題と非常によく似ており、どちらも2次元グリッドを埋めていく過程とみなせます。
|
||||
|
||||
=== "<1>"
|
||||

|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
@@ -120,9 +120,9 @@ $$
|
||||
|
||||
### 空間最適化
|
||||
|
||||
$dp[i, j]$ は上の $dp[i-1, j]$、左の $dp[i, j-1]$、左上の $dp[i-1, j-1]$ の解から導出され、直接走査では左上の解 $dp[i-1, j-1]$ が失われ、逆走査では事前に $dp[i, j-1]$ を構築できないため、どちらの走査順序も実行可能ではありません。
|
||||
$dp[i,j]$ は上の $dp[i-1, j]$、左の $dp[i, j-1]$、左上の $dp[i-1, j-1]$ から遷移されますが、順方向走査では左上の $dp[i-1, j-1]$ を失い、逆方向走査では $dp[i, j-1]$ を事前に構築できません。そのため、どちらの走査順序も適切ではありません。
|
||||
|
||||
この理由で、変数 `leftup` を使用して左上の $dp[i-1, j-1]$ からの解を一時的に保存し、左と上の解のみを考慮すればよくなります。この状況は無制限ナップサック問題と似ており、直接走査が可能です。コードは以下の通りです:
|
||||
そのため、変数 `leftup` を用いて左上の解 $dp[i-1, j-1]$ を一時保存し、左と上の解だけを考えればよくなります。このときの状況は完全ナップサック問題と同じであり、順方向走査を用いることができます。コードは次のとおりです:
|
||||
|
||||
```src
|
||||
[file]{edit_distance}-[class]{}-[func]{edit_distance_dp_comp}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# 動的プログラミング
|
||||
# 動的計画法
|
||||
|
||||

|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
川が流れて海に注ぐように、
|
||||
|
||||
動的プログラミングは小さな問題の解を織り合わせて、より大きな問題の解へと導きます。一歩一歩進んで、最終的な答えが待つ彼岸へと向かいます。
|
||||
小川は川へと注ぎ、河川は大海へと注ぐ。
|
||||
|
||||
動的計画法は小さな問題の解を集めて大きな問題の答えとし、一歩ずつ私たちを問題解決の彼岸へと導く。
|
||||
|
||||
@@ -1,110 +1,110 @@
|
||||
# 動的プログラミングの紹介
|
||||
# 動的計画法入門
|
||||
|
||||
<u>動的プログラミング</u>は重要なアルゴリズムパラダイムであり、問題を一連の小さな部分問題に分解し、これらの部分問題の解を保存することで冗長な計算を避け、時間効率を大幅に向上させます。
|
||||
<u>動的計画法(dynamic programming)</u>は重要なアルゴリズムパラダイムであり、問題をより小さな部分問題の列に分解し、それらの解を保存して重複計算を避けることで、時間効率を大幅に向上させます。
|
||||
|
||||
このセクションでは、古典的な問題から始めて、まず力任せの探索法による解法を提示し、重複する部分問題を特定してから、より効率的な動的プログラミング解法を段階的に導出します。
|
||||
本節では、古典的な例題から始めて、まずその力任せのバックトラッキング解法を示し、そこに含まれる重複部分問題を観察したうえで、より効率的な動的計画法の解法を段階的に導きます。
|
||||
|
||||
!!! question "階段登り"
|
||||
!!! question "階段を上る"
|
||||
|
||||
$n$ 段の階段があり、一度に $1$ 段または $2$ 段上ることができます。頂上に到達する方法は何通りありますか?
|
||||
全体で $n$ 段ある階段が与えられ、各ステップで $1$ 段または $2$ 段上ることができます。頂上まで到達する方法は何通りあるでしょうか?
|
||||
|
||||
下の図に示すように、$3$ 段の階段の頂上に到達する方法は $3$ 通りあります。
|
||||
次の図に示すように、$3$ 段の階段では、頂上まで到達する方法は全部で $3$ 通りあります。
|
||||
|
||||

|
||||

|
||||
|
||||
この問題は**バックトラッキングを用いてすべての可能性を網羅**することで方法の数を計算することを目的としています。具体的には、階段登りの問題を多段階選択プロセスとして考えます:地面から始めて、毎回 $1$ 段または $2$ 段上るかを選択し、階段の頂上に到達したら方法の数をカウントし、頂上を超えた場合はプルーニング(枝刈り)を行います。コードは以下の通りです:
|
||||
この問題の目的は方法の総数を求めることです。**考えられるすべての可能性をバックトラッキングで総当たりすることができます**。具体的には、階段を上ることを複数ラウンドの選択過程とみなし、地面から出発して各ラウンドで $1$ 段または $2$ 段上ります。階段の頂上に到達するたびに方法数を $1$ 増やし、頂上を越えた場合は枝刈りします。コードは次のとおりです:
|
||||
|
||||
```src
|
||||
[file]{climbing_stairs_backtrack}-[class]{}-[func]{climbing_stairs_backtrack}
|
||||
```
|
||||
|
||||
## 方法1:力任せ探索
|
||||
## 方法 1:総当たり探索
|
||||
|
||||
バックトラッキングアルゴリズムは問題を明示的に部分問題に分解しません。代わりに、問題を一連の決定ステップとして扱い、試行と枝刈りを通じてすべての可能性を探索します。
|
||||
バックトラッキング法は通常、問題を明示的に分解するのではなく、問題解決を一連の意思決定ステップとみなし、試行と枝刈りによってあらゆる可能な解を探索します。
|
||||
|
||||
この問題を分解アプローチを使って分析できます。$dp[i]$ を $i$ 段目に到達する方法の数とします。この場合、$dp[i]$ が元の問題であり、その部分問題は次のようになります:
|
||||
この問題を問題分解の観点から分析してみましょう。$i$ 段目まで上る方法が全部で $dp[i]$ 通りあるとすると、$dp[i]$ が元の問題であり、その部分問題には次が含まれます:
|
||||
|
||||
$$
|
||||
dp[i-1], dp[i-2], \dots, dp[2], dp[1]
|
||||
$$
|
||||
|
||||
各移動は $1$ 段または $2$ 段しか進めないため、$i$ 段目に立っているとき、前のステップは $i-1$ 段目または $i-2$ 段目のいずれかにいたはずです。つまり、$i$ 段目には $i-1$ 段目または $i-2$ 段目からしか到達できません。
|
||||
各ラウンドでは $1$ 段または $2$ 段しか上れないため、$i$ 段目の階段に立っているとき、直前のラウンドでは $i - 1$ 段目または $i - 2$ 段目にしか立てません。言い換えると、$i -1$ 段目または $i - 2$ 段目からしか $i$ 段目へ進めません。
|
||||
|
||||
これにより重要な結論が得られます:**$i-1$ 段目に到達する方法の数に $i-2$ 段目に到達する方法の数を加えたものが、$i$ 段目に到達する方法の数に等しい**。式は以下の通りです:
|
||||
ここから重要な帰結が得られます。**$i - 1$ 段目まで上る方法数と $i - 2$ 段目まで上る方法数の和が、$i$ 段目まで上る方法数に等しい**のです。式は次のとおりです:
|
||||
|
||||
$$
|
||||
dp[i] = dp[i-1] + dp[i-2]
|
||||
$$
|
||||
|
||||
これは、階段登り問題において部分問題間に再帰関係があることを意味し、**元の問題の解は部分問題の解から構築できます**。下の図はこの再帰関係を示しています。
|
||||
これは、階段を上る問題では各部分問題の間に漸化関係があり、**元の問題の解は部分問題の解から構築できる**ことを意味します。次の図はこの漸化関係を示しています。
|
||||
|
||||

|
||||

|
||||
|
||||
再帰式に従って力任せ探索解法を得ることができます。$dp[n]$ から始めて、**より大きな問題を再帰的に2つの小さな部分問題の和に分解**し、解が既知の最小の部分問題 $dp[1]$ と $dp[2]$ に到達するまで続けます。$dp[1] = 1$ と $dp[2] = 2$ で、それぞれ1段目と2段目に登る方法が $1$ 通りと $2$ 通りあることを表します。
|
||||
漸化式に基づいて総当たり探索の解法を得ることができます。$dp[n]$ を出発点とし、**より大きな問題を再帰的に 2 つのより小さな問題の和へ分解**していき、最小部分問題 $dp[1]$ と $dp[2]$ に到達したら返します。ここで最小部分問題の解は既知であり、$dp[1] = 1$、$dp[2] = 2$ です。これは、第 $1$ 段目と第 $2$ 段目まで上る方法がそれぞれ $1$ 通り、$2$ 通りであることを表します。
|
||||
|
||||
以下のコードを観察すると、標準的なバックトラッキングコードと同様に深さ優先探索に属しますが、より簡潔です:
|
||||
次のコードを見ると、標準的なバックトラッキングコードと同じく深さ優先探索に属しますが、より簡潔です:
|
||||
|
||||
```src
|
||||
[file]{climbing_stairs_dfs}-[class]{}-[func]{climbing_stairs_dfs}
|
||||
```
|
||||
|
||||
下の図は力任せ探索によって形成される再帰木を示しています。問題 $dp[n]$ について、その再帰木の深さは $n$ で、時間計算量は $O(2^n)$ です。この指数的増加により、$n$ が大きいとプログラムの実行がはるかに遅くなり、長い待機時間が生じます。
|
||||
次の図は総当たり探索によって形成される再帰木を示しています。問題 $dp[n]$ に対して、その再帰木の深さは $n$、時間計算量は $O(2^n)$ です。指数オーダーは爆発的に増加するため、比較的大きな $n$ を入力すると長時間待たされることになります。
|
||||
|
||||

|
||||

|
||||
|
||||
上の図を観察すると、**指数時間計算量は「重複する部分問題」によって引き起こされる**ことがわかります。例えば、$dp[9]$ は $dp[8]$ と $dp[7]$ に分解され、$dp[8]$ はさらに $dp[7]$ と $dp[6]$ に分解され、両方とも部分問題 $dp[7]$ を含んでいます。
|
||||
上の図を見ると、**指数オーダーの時間計算量は「重複部分問題」によって生じています**。たとえば $dp[9]$ は $dp[8]$ と $dp[7]$ に分解され、$dp[8]$ は $dp[7]$ と $dp[6]$ に分解されるため、どちらにも部分問題 $dp[7]$ が含まれています。
|
||||
|
||||
このように、部分問題にはさらに小さな重複する部分問題が含まれ、これは無限に続きます。計算リソースの大部分がこれらの重複する部分問題に浪費されています。
|
||||
このように、部分問題の中にはさらに小さな重複部分問題が含まれ、それが際限なく続いていきます。計算資源の大部分は、こうした重複部分問題に浪費されています。
|
||||
|
||||
## 方法2:メモ化探索
|
||||
## 方法 2:メモ化探索
|
||||
|
||||
アルゴリズムの効率を向上させるため、**すべての重複する部分問題を一度だけ計算したい**と考えます。この目的のため、各部分問題の解を記録する配列 `mem` を宣言し、探索プロセス中に重複する部分問題を枝刈りします。
|
||||
アルゴリズム効率を高めるため、**すべての重複部分問題を 1 回だけ計算したい**と考えます。そのために、各部分問題の解を記録する配列 `mem` を宣言し、探索の過程で重複部分問題を枝刈りします。
|
||||
|
||||
1. $dp[i]$ が初めて計算されるとき、後で使用するために `mem[i]` に記録します。
|
||||
2. $dp[i]$ を再度計算する必要があるとき、`mem[i]` から直接結果を取得でき、その部分問題の冗長な計算を避けられます。
|
||||
1. $dp[i]$ を初めて計算したとき、その結果を `mem[i]` に記録して後で使えるようにします。
|
||||
2. 再び $dp[i]$ を計算する必要が生じたときは、`mem[i]` から直接結果を取得し、その部分問題の重複計算を避けます。
|
||||
|
||||
コードは以下の通りです:
|
||||
コードは次のとおりです:
|
||||
|
||||
```src
|
||||
[file]{climbing_stairs_dfs_mem}-[class]{}-[func]{climbing_stairs_dfs_mem}
|
||||
```
|
||||
|
||||
下の図を観察すると、**メモ化後、すべての重複する部分問題は一度だけ計算される必要があり、時間計算量を $O(n)$ に最適化**します。これは大幅な改善です。
|
||||
次の図を見ると、**メモ化を行うことで、すべての重複部分問題は 1 回だけ計算すればよくなり、時間計算量は $O(n)$ まで改善されます**。これは大きな飛躍です。
|
||||
|
||||

|
||||

|
||||
|
||||
## 方法3:動的プログラミング
|
||||
## 方法 3:動的計画法
|
||||
|
||||
**メモ化探索は「トップダウン」方式**です:元の問題(根ノード)から始めて、より大きな部分問題をより小さなものに再帰的に分解し、最小の既知の部分問題(葉ノード)の解に到達するまで続けます。その後、バックトラッキングにより部分問題の解を収集し、元の問題の解を構築します。
|
||||
**メモ化探索は「トップダウン」の方法**です。元の問題(根ノード)から始めて、より大きな部分問題を再帰的により小さな部分問題へ分解し、解が既知である最小部分問題(葉ノード)に至ります。その後、バックトラックしながら各層で部分問題の解を集め、元の問題の解を構築します。
|
||||
|
||||
一方、**動的プログラミングは「ボトムアップ」方式**です:最小の部分問題の解から始めて、元の問題が解決されるまで、より大きな部分問題の解を反復的に構築します。
|
||||
これとは対照的に、**動的計画法は「ボトムアップ」の方法**です。最小部分問題の解から始めて、より大きな部分問題の解を反復的に構築し、最終的に元の問題の解を得ます。
|
||||
|
||||
動的プログラミングはバックトラッキングを必要としないため、ループを使った反復のみが必要で、再帰は不要です。以下のコードでは、配列 `dp` を初期化して部分問題の解を保存し、メモ化探索の配列 `mem` と同じ記録機能を果たします:
|
||||
動的計画法にはバックトラックの過程が含まれないため、再帰を使う必要はなく、ループによる反復だけで実装できます。次のコードでは、部分問題の解を保存する配列 `dp` を初期化しており、これはメモ化探索における配列 `mem` と同じ記録の役割を果たします:
|
||||
|
||||
```src
|
||||
[file]{climbing_stairs_dp}-[class]{}-[func]{climbing_stairs_dp}
|
||||
```
|
||||
|
||||
下の図は上記コードの実行プロセスをシミュレートしています。
|
||||
次の図は、以上のコードの実行過程をシミュレートしたものです。
|
||||
|
||||

|
||||

|
||||
|
||||
バックトラッキングアルゴリズムと同様に、動的プログラミングも「状態」の概念を使用して問題解決の特定の段階を表現し、各状態は部分問題とその局所最適解に対応します。例えば、階段登り問題の状態は現在のステップ番号 $i$ として定義されます。
|
||||
バックトラッキング法と同様に、動的計画法でも問題解決の特定段階を表すために「状態」という概念を用います。各状態は 1 つの部分問題と、それに対応する局所最適解に対応します。たとえば、階段を上る問題では、状態は現在いる階段の段数 $i$ と定義されます。
|
||||
|
||||
上記の内容に基づいて、動的プログラミングでよく使用される用語をまとめることができます。
|
||||
以上を踏まえると、動的計画法のよく使われる用語を次のようにまとめられます。
|
||||
|
||||
- 配列 `dp` は<u>DPテーブル</u>と呼ばれ、$dp[i]$ は状態 $i$ に対応する部分問題の解を表します。
|
||||
- 最小の部分問題(ステップ $1$ と $2$)に対応する状態は<u>初期状態</u>と呼ばれます。
|
||||
- 再帰式 $dp[i] = dp[i-1] + dp[i-2]$ は<u>状態遷移方程式</u>と呼ばれます。
|
||||
- 配列 `dp` を <u>dp テーブル</u> と呼び、$dp[i]$ は状態 $i$ に対応する部分問題の解を表します。
|
||||
- 最小部分問題に対応する状態(第 $1$ 段目と第 $2$ 段目の階段)を<u>初期状態</u>と呼びます。
|
||||
- 漸化式 $dp[i] = dp[i-1] + dp[i-2]$ を<u>状態遷移方程式</u>と呼びます。
|
||||
|
||||
## 空間最適化
|
||||
|
||||
注意深い読者は**$dp[i]$ は $dp[i-1]$ と $dp[i-2]$ のみに関連するため、すべての部分問題の解を保存するために配列 `dp` を使用する必要がない**ことに気づくでしょう。単に2つの変数を使って反復的に進めることができます。コードは以下の通りです:
|
||||
注意深い読者は気づいたかもしれません。**$dp[i]$ は $dp[i-1]$ と $dp[i-2]$ にしか依存しないため、すべての部分問題の解を保存するために配列 `dp` を使う必要はありません**。2 つの変数を順に更新していくだけで十分です。コードは次のとおりです:
|
||||
|
||||
```src
|
||||
[file]{climbing_stairs_dp}-[class]{}-[func]{climbing_stairs_dp_comp}
|
||||
```
|
||||
|
||||
上記のコードを観察すると、配列 `dp` が占有していた空間が削除されるため、空間計算量は $O(n)$ から $O(1)$ に削減されます。
|
||||
上のコードを見ると、配列 `dp` が占めていた領域を省けるため、空間計算量は $O(n)$ から $O(1)$ へと下がります。
|
||||
|
||||
多くの動的プログラミング問題では、現在の状態は限られた数の前の状態のみに依存するため、必要な状態のみを保持し、「次元削減」によってメモリ空間を節約できます。**この空間最適化技術は「ローリング変数」または「ローリング配列」として知られています**。
|
||||
動的計画法の問題では、現在の状態はしばしば直前の限られた個数の状態にしか関係しません。このような場合は、必要な状態だけを保持し、「次元削減」によってメモリ空間を節約できます。**この空間最適化の技巧は「ローリング変数」または「ローリング配列」と呼ばれます**。
|
||||
|
||||
@@ -1,97 +1,97 @@
|
||||
# 0-1ナップサック問題
|
||||
# 0-1 ナップサック問題
|
||||
|
||||
ナップサック問題は動的プログラミングの優れた入門問題であり、動的プログラミングで最も一般的な問題タイプです。0-1ナップサック問題、無制限ナップサック問題、複数ナップサック問題など、多くの変種があります。
|
||||
ナップサック問題は、動的計画法の入門として非常に適した問題であり、動的計画法で最もよく見られる問題形式の1つです。これには 0-1 ナップサック問題、完全ナップサック問題、多重ナップサック問題など、多くの派生があります。
|
||||
|
||||
このセクションでは、まず最も一般的な0-1ナップサック問題を解決します。
|
||||
本節では、まず最も一般的な 0-1 ナップサック問題を解いていきます。
|
||||
|
||||
!!! question
|
||||
|
||||
$n$ 個のアイテムが与えられ、$i$ 番目のアイテムの重量は $wgt[i-1]$ で値は $val[i-1]$ です。容量が $cap$ のナップサックがあります。各アイテムは1回のみ選択できます。容量制限下でナップサックに入れることができるアイテムの最大値は何ですか?
|
||||
$n$ 個の品物が与えられ、$i$ 番目の品物の重さは $wgt[i-1]$、価値は $val[i-1]$ であり、容量 $cap$ のナップサックがあります。各品物は1回しか選べないとき、ナップサック容量の制約下で入れられる品物の最大価値を求めてください。
|
||||
|
||||
下の図を観察すると、アイテム番号 $i$ は1から数え始め、配列インデックスは0から始まるため、アイテム $i$ の重量は $wgt[i-1]$ に対応し、値は $val[i-1]$ に対応します。
|
||||
以下の図を見てみましょう。品物番号 $i$ は $1$ から始まり、配列のインデックスは $0$ から始まるため、品物 $i$ は重さ $wgt[i-1]$、価値 $val[i-1]$ に対応します。
|
||||
|
||||

|
||||

|
||||
|
||||
0-1ナップサック問題を $n$ ラウンドの決定から構成されるプロセスとして考えることができます。各アイテムについて入れない、または入れるという2つの決定があり、したがって問題は決定木モデルに適合します。
|
||||
0-1 ナップサック問題は、$n$ 回の意思決定からなる過程とみなせます。各品物について「入れない」「入れる」という2つの選択肢があるため、この問題は決定木モデルを満たします。
|
||||
|
||||
この問題の目的は「限られた容量の下でナップサックに入れることができるアイテムの値を最大化する」ことであり、動的プログラミング問題である可能性が高いです。
|
||||
この問題の目的は「ナップサック容量の制約下で入れられる品物の最大価値」を求めることなので、動的計画法の問題である可能性が高いです。
|
||||
|
||||
**第1ステップ:各ラウンドの決定を考え、状態を定義し、それにより $dp$ テーブルを得る**
|
||||
**ステップ1:各ラウンドの選択を考え、状態を定義して、$dp$ テーブルを得る**
|
||||
|
||||
各アイテムについて、ナップサックに入れなければ容量は変わらず、入れれば容量は減少します。これから状態定義を得ることができます:現在のアイテム番号 $i$ とナップサック容量 $c$、$[i, c]$ と表記されます。
|
||||
各品物について、ナップサックに入れなければ容量は変わらず、入れれば容量は減少します。ここから状態を、現在の品物番号 $i$ とナップサック容量 $c$ として定義し、$[i, c]$ と表せます。
|
||||
|
||||
状態 $[i, c]$ は部分問題に対応します:**容量 $c$ のナップサックでの最初の $i$ 個のアイテムの最大値**、$dp[i, c]$ と表記されます。
|
||||
状態 $[i, c]$ に対応する部分問題は、**先頭 $i$ 個の品物を容量 $c$ のナップサックに入れるときの最大価値** であり、これを $dp[i, c]$ と記します。
|
||||
|
||||
探している解は $dp[n, cap]$ であるため、サイズ $(n+1) \times (cap+1)$ の二次元 $dp$ テーブルが必要です。
|
||||
求めるべきものは $dp[n, cap]$ なので、サイズ $(n+1) \times (cap+1)$ の2次元 $dp$ テーブルが必要です。
|
||||
|
||||
**第2ステップ:最適部分構造を特定し、状態遷移方程式を導出する**
|
||||
**ステップ2:最適部分構造を見つけ、状態遷移方程式を導く**
|
||||
|
||||
アイテム $i$ の決定を行った後、残るのは最初の $i-1$ 個のアイテムの決定の部分問題であり、これは2つのケースに分けることができます。
|
||||
品物 $i$ に対する選択を行った後に残るのは、先頭 $i-1$ 個の品物に対する部分問題であり、次の2つのケースに分けられます。
|
||||
|
||||
- **アイテム $i$ を入れない**:ナップサック容量は変わらず、状態は $[i-1, c]$ に変わります。
|
||||
- **アイテム $i$ を入れる**:ナップサック容量は $wgt[i-1]$ だけ減少し、値は $val[i-1]$ だけ増加し、状態は $[i-1, c-wgt[i-1]]$ に変わります。
|
||||
- **品物 $i$ を入れない** :ナップサック容量は変わらず、状態は $[i-1, c]$ に変化します。
|
||||
- **品物 $i$ を入れる** :ナップサック容量は $wgt[i-1]$ だけ減少し、価値は $val[i-1]$ だけ増加して、状態は $[i-1, c-wgt[i-1]]$ に変化します。
|
||||
|
||||
上記の分析により、この問題の最適部分構造が明らかになります:**最大値 $dp[i, c]$ は、アイテム $i$ を入れない方案とアイテム $i$ を入れる方案の2つのうち、より大きな値に等しい**。これから状態遷移方程式を導出できます:
|
||||
以上の分析から、この問題の最適部分構造が分かります。すなわち、**最大価値 $dp[i, c]$ は、品物 $i$ を入れない場合と入れる場合のうち、より価値の大きい方に等しい** ということです。これにより、次の状態遷移方程式を導けます。
|
||||
|
||||
$$
|
||||
dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
|
||||
$$
|
||||
|
||||
現在のアイテムの重量 $wgt[i - 1]$ が残りのナップサック容量 $c$ を超える場合、唯一の選択肢はナップサックに入れないことであることに注意することが重要です。
|
||||
注意すべき点として、現在の品物の重さ $wgt[i - 1]$ が残りのナップサック容量 $c$ を超える場合は、入れない選択しかできません。
|
||||
|
||||
**第3ステップ:境界条件と状態遷移の順序を決定する**
|
||||
**ステップ3:境界条件と状態遷移の順序を決める**
|
||||
|
||||
アイテムがない場合またはナップサック容量が $0$ の場合、最大値は $0$ です。つまり、最初の列 $dp[i, 0]$ と最初の行 $dp[0, c]$ はどちらも $0$ に等しいです。
|
||||
品物がない場合、またはナップサック容量が $0$ の場合、最大価値は $0$ です。すなわち、先頭列 $dp[i, 0]$ と先頭行 $dp[0, c]$ はいずれも $0$ になります。
|
||||
|
||||
現在の状態 $[i, c]$ は直接上の状態 $[i-1, c]$ と左上の状態 $[i-1, c-wgt[i-1]]$ から遷移するため、2層のループを通じて $dp$ テーブル全体を順序通りに走査します。
|
||||
現在の状態 $[i, c]$ は、上側の状態 $[i-1, c]$ と左上の状態 $[i-1, c-wgt[i-1]]$ から遷移してくるため、2重ループで $dp$ テーブル全体を順方向に走査すれば十分です。
|
||||
|
||||
上記の分析に従って、次に力任せ探索、メモ化探索、動的プログラミングの順序で解法を実装します。
|
||||
以上の分析に基づき、次に全探索、メモ化探索、動的計画法の順で実装していきます。
|
||||
|
||||
### 方法1:力任せ探索
|
||||
### 方法1:全探索
|
||||
|
||||
探索コードには以下の要素が含まれます。
|
||||
探索コードには次の要素が含まれます。
|
||||
|
||||
- **再帰パラメータ**:状態 $[i, c]$。
|
||||
- **戻り値**:部分問題 $dp[i, c]$ の解。
|
||||
- **終了条件**:アイテム番号が範囲外 $i = 0$ またはナップサックの残り容量が $0$ のとき、再帰を終了し値 $0$ を返す。
|
||||
- **枝刈り**:現在のアイテムの重量がナップサックの残り容量を超える場合、唯一の選択肢はナップサックに入れないことです。
|
||||
- **再帰引数**:状態 $[i, c]$ です。
|
||||
- **戻り値**:部分問題の解 $dp[i, c]$ です。
|
||||
- **終了条件**:品物番号が範囲外である $i = 0$、またはナップサックの残り容量が $0$ のとき、再帰を終了して価値 $0$ を返します。
|
||||
- **枝刈り**:現在の品物の重さがナップサックの残り容量を超える場合、入れない選択しかできません。
|
||||
|
||||
```src
|
||||
[file]{knapsack}-[class]{}-[func]{knapsack_dfs}
|
||||
```
|
||||
|
||||
下の図に示すように、各アイテムは選択しないと選択するという2つの探索分岐を生成するため、時間計算量は $O(2^n)$ です。
|
||||
以下の図のように、各品物ごとに「選ばない」「選ぶ」の2つの探索分岐が生じるため、時間計算量は $O(2^n)$ です。
|
||||
|
||||
再帰木を観察すると、$dp[1, 10]$ などの重複する部分問題があることが容易にわかります。アイテムが多く、ナップサック容量が大きい場合、特に同じ重量のアイテムが多い場合、重複する部分問題の数は大幅に増加します。
|
||||
再帰木を観察すると、$dp[1, 10]$ などの重複部分問題が存在することが分かります。品物数が多く、ナップサック容量が大きく、特に同じ重さの品物が多い場合には、重複部分問題の数は大幅に増加します。
|
||||
|
||||

|
||||

|
||||
|
||||
### 方法2:メモ化探索
|
||||
|
||||
重複する部分問題が一度だけ計算されることを確保するために、部分問題の解を記録するメモ化リスト `mem` を使用します。ここで `mem[i][c]` は $dp[i, c]$ に対応します。
|
||||
重複部分問題が一度だけ計算されるようにするため、メモ配列 `mem` を用いて部分問題の解を記録します。ここで `mem[i][c]` は $dp[i, c]$ に対応します。
|
||||
|
||||
メモ化を導入した後、**時間計算量は部分問題の数に依存**し、$O(n \times cap)$ になります。実装コードは以下の通りです:
|
||||
メモ化を導入すると、**時間計算量は部分問題の数に依存し**、すなわち $O(n \times cap)$ になります。実装コードは次のとおりです。
|
||||
|
||||
```src
|
||||
[file]{knapsack}-[class]{}-[func]{knapsack_dfs_mem}
|
||||
```
|
||||
|
||||
下の図はメモ化探索で枝刈りされる探索分岐を示しています。
|
||||
次の図は、メモ化探索で剪定された探索分岐を示しています。
|
||||
|
||||

|
||||

|
||||
|
||||
### 方法3:動的プログラミング
|
||||
### 方法3:動的計画法
|
||||
|
||||
動的プログラミングは本質的に状態遷移中に $dp$ テーブルを埋めることを含みます。コードは下の図に示されています:
|
||||
動的計画法の本質は、状態遷移に従って $dp$ テーブルを埋めていく過程です。コードは次のようになります。
|
||||
|
||||
```src
|
||||
[file]{knapsack}-[class]{}-[func]{knapsack_dp}
|
||||
```
|
||||
|
||||
下の図に示すように、時間計算量と空間計算量の両方が配列 `dp` のサイズ、つまり $O(n \times cap)$ によって決定されます。
|
||||
以下の図のように、時間計算量と空間計算量はいずれも配列 `dp` のサイズによって決まり、$O(n \times cap)$ です。
|
||||
|
||||
=== "<1>"
|
||||

|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
@@ -134,17 +134,17 @@ $$
|
||||
|
||||
### 空間最適化
|
||||
|
||||
各状態は上の行の状態のみに関連するため、2つの配列を使用してローリング前進させ、空間計算量を $O(n^2)$ から $O(n)$ に削減できます。
|
||||
各状態は直前の行の状態にしか依存しないため、2つの配列をローテーションして用いることで、空間計算量を $O(n^2)$ から $O(n)$ に削減できます。
|
||||
|
||||
さらに考えてみると、1つの配列だけで空間最適化を達成できるでしょうか?各状態が直接上のセルまたは左上のセルから遷移することが観察できます。配列が1つしかない場合、$i$ 行目の走査を開始するとき、その配列はまだ $i-1$ 行目の状態を保存しています。
|
||||
さらに考えると、1つの配列だけで空間最適化を実現できるでしょうか。観察すると、各状態は真上または左上のマスから遷移してきます。配列が1つしかないと仮定すると、$i$ 行目の走査を開始した時点では、その配列にはまだ $i-1$ 行目の状態が格納されています。
|
||||
|
||||
- 通常の順序で走査する場合、$dp[i, j]$ に走査したとき、左上の $dp[i-1, 1]$ ~ $dp[i-1, j-1]$ の値がすでに上書きされている可能性があり、正しい状態遷移結果を得ることができません。
|
||||
- 逆順で走査する場合、上書き問題はなく、状態遷移を正しく実行できます。
|
||||
- 順方向に走査すると、$dp[i, j]$ に到達した時点で、左上にある $dp[i-1, 1]$ ~ $dp[i-1, j-1]$ の値がすでに上書きされている可能性があり、正しい状態遷移結果を得られません。
|
||||
- 逆方向に走査すれば、上書きの問題は発生せず、状態遷移を正しく行えます。
|
||||
|
||||
下の図は単一配列での $i = 1$ 行目から $i = 2$ 行目への遷移プロセスを示しています。通常順序走査と逆順走査の違いについて考えてみてください。
|
||||
次の図は、単一配列のもとで $i = 1$ 行目から $i = 2$ 行目へ変換する過程を示しています。順方向走査と逆方向走査の違いを考えてみてください。
|
||||
|
||||
=== "<1>"
|
||||

|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
@@ -161,7 +161,7 @@ $$
|
||||
=== "<6>"
|
||||

|
||||
|
||||
コード実装では、配列 `dp` の最初の次元 $i$ を削除し、内側のループを逆走査に変更するだけです:
|
||||
コード実装では、配列 `dp` の第1次元 $i$ をそのまま削除し、内側のループを逆方向走査に変更するだけで済みます。
|
||||
|
||||
```src
|
||||
[file]{knapsack}-[class]{}-[func]{knapsack_dp_comp}
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
# まとめ
|
||||
|
||||
- 動的プログラミングは問題を分解し、部分問題の解を保存することで冗長な計算を避け、計算効率を向上させます。
|
||||
- 時間を考慮しなければ、すべての動的プログラミング問題はバックトラッキング(力任せ探索)を使用して解決できますが、再帰木には多くの重複する部分問題があり、効率が非常に低くなります。記憶化リストを導入することで、計算されたすべての部分問題の解を保存し、重複する部分問題が一度だけ計算されることを保証できます。
|
||||
- 記憶化探索はトップダウンの再帰解法であり、動的プログラミングはボトムアップの反復アプローチに対応し、「表を埋める」ことに似ています。現在の状態は特定の局所状態のみに依存するため、dpテーブルの1次元を削除して空間計算量を削減できます。
|
||||
- 部分問題の分解は汎用的なアルゴリズムアプローチであり、分割統治法、動的プログラミング、バックトラッキングで特徴が異なります。
|
||||
- 動的プログラミング問題には3つの主要な特徴があります:重複する部分問題、最適部分構造、無記憶性。
|
||||
- 元の問題の最適解がその部分問題の最適解から構築できる場合、最適部分構造を持ちます。
|
||||
- 無記憶性とは、状態の将来の発展が現在の状態のみに依存し、過去に経験したすべての状態に依存しないことを意味します。多くの組み合わせ最適化問題にはこの特性がなく、動的プログラミングを使用して迅速に解決することはできません。
|
||||
### 要点の振り返り
|
||||
|
||||
- 動的計画法は問題を分解し、部分問題の解を保存することで重複計算を避け、計算効率を高めます。
|
||||
- 時間を考慮しなければ、すべての動的計画法の問題はバックトラッキング(総当たり探索)で解けますが、再帰木には大量の重複部分問題が存在するため、効率はきわめて低くなります。メモ化配列を導入すると、計算済みのすべての部分問題の解を保存でき、重複部分問題が 1 回だけ計算されることを保証できます。
|
||||
- メモ化探索はトップダウンの再帰的解法であり、それに対応する動的計画法はボトムアップの漸化式による解法で、ちょうど「表を埋める」ようなものです。現在の状態は一部の局所状態にのみ依存するため、$dp$ 表の 1 次元を削減して空間計算量を下げることができます。
|
||||
- 部分問題への分解は汎用的なアルゴリズムの考え方であり、分割統治、動的計画法、バックトラッキングではそれぞれ異なる性質を持ちます。
|
||||
- 動的計画法の問題には 3 つの大きな特徴があります。重複部分問題、最適部分構造、無後効性です。
|
||||
- 元の問題の最適解が部分問題の最適解から構築できるなら、その問題は最適部分構造を持ちます。
|
||||
- 無後効性とは、ある状態の将来の発展がその状態のみに関係し、過去に経たすべての状態とは無関係であることを指します。多くの組合せ最適化問題は無後効性を持たず、動的計画法で高速に解くことはできません。
|
||||
|
||||
**ナップサック問題**
|
||||
|
||||
- ナップサック問題は最も典型的な動的プログラミング問題の1つで、0-1ナップサック、無制限ナップサック、複数ナップサックなどの変種があります。
|
||||
- 0-1ナップサックの状態定義は、最初の $i$ 個のアイテムを含む容量 $c$ のナップサックでの最大値です。アイテムをナップサックに入れないまたは入れるという決定に基づいて、最適部分構造を特定し、状態遷移方程式を構築できます。空間最適化では、各状態が直接上と左上の状態に依存するため、左上の状態の上書きを避けるためにリストを逆順で走査する必要があります。
|
||||
- 無制限ナップサック問題では、各種類のアイテムを選択できる数に制限がないため、アイテムを含める状態遷移は0-1ナップサックと異なります。状態が直接上と左の状態に依存するため、空間最適化では前方走査を含める必要があります。
|
||||
- コイン交換問題は無制限ナップサック問題の変種で、「最大」値を求めることから「最小」コイン数を求めることに変わり、状態遷移方程式は $\max()$ を $\min()$ に変更する必要があります。ナップサックの容量を「超えない」ことを追求することから、正確に目標金額を求めることに変わり、「目標金額を構成できない」無効解を表すために $amt + 1$ を使用します。
|
||||
- コイン交換問題IIは「最小コイン数」を求めることから「コインの組み合わせ数」を求めることに変わり、状態遷移方程式を $\min()$ から和算演算子に変更します。
|
||||
- ナップサック問題は最も典型的な動的計画法の問題の 1 つであり、0-1 ナップサック、完全ナップサック、多重ナップサックなどの派生があります。
|
||||
- 0-1 ナップサックの状態は、容量 $c$ のナップサックに対して、前 $i$ 個の品物で得られる最大価値として定義されます。ナップサックに入れない場合と入れる場合の 2 つの判断から最適部分構造を得て、状態遷移方程式を構築できます。空間最適化では、各状態が真上と左上の状態に依存するため、左上の状態が上書きされるのを避けるために配列を逆順に走査する必要があります。
|
||||
- 完全ナップサック問題では各品物の選択数に制限がないため、品物を入れる場合の状態遷移は 0-1 ナップサック問題とは異なります。状態は真上と真左の状態に依存するので、空間最適化では順方向に走査するべきです。
|
||||
- コイン両替問題は完全ナップサック問題の変種です。「最大」価値を求める問題から「最小」の硬貨枚数を求める問題へ変わるため、状態遷移方程式の $\max()$ は $\min()$ に置き換える必要があります。また、ナップサック容量を「超えない」ことを目指すのではなく、目標金額を「ちょうど」作ることを目指すため、$amt + 1$ を「目標金額を作れない」無効解の表現として用います。
|
||||
- コイン両替問題 II では、「最少硬貨枚数」を求める問題から「硬貨の組合せ数」を求める問題へ変わるため、状態遷移方程式も $\min()$ から総和演算子へ対応して変わります。
|
||||
|
||||
**編集距離問題**
|
||||
|
||||
- 編集距離(レーベンシュタイン距離)は2つの文字列間の類似度を測定し、一つの文字列を別の文字列に変更するために必要な最小編集ステップ数として定義され、編集操作には追加、削除、置換が含まれます。
|
||||
- 編集距離問題の状態定義は、$s$ の最初の $i$ 文字を $t$ の最初の $j$ 文字に変更するために必要な最小編集ステップ数です。$s[i] \ne t[j]$ の場合、追加、削除、置換の3つの決定があり、それぞれに対応する残余部分問題があります。これから最適部分構造を特定し、状態遷移方程式を構築できます。$s[i] = t[j]$ の場合、現在の文字の編集は必要ありません。
|
||||
- 編集距離では、状態が直接上、左、左上の状態に依存します。したがって、空間最適化後、前方走査も逆走査も正しく状態遷移を実行できません。これに対処するため、変数を使用して左上の状態を一時的に保存し、無制限ナップサック問題の状況と同等にし、空間最適化後に前方走査を可能にします。
|
||||
- 編集距離(Levenshtein 距離)は 2 つの文字列間の類似度を測るために用いられ、ある文字列を別の文字列へ変換するための最小編集回数として定義されます。編集操作には追加、削除、置換が含まれます。
|
||||
- 編集距離問題の状態は、$s$ の前 $i$ 文字を $t$ の前 $j$ 文字へ変更するのに必要な最小編集回数として定義されます。$s[i] \ne t[j]$ のときは、追加、削除、置換の 3 つの判断があり、それぞれに対応する残りの部分問題があります。これにより最適部分構造を見いだし、状態遷移方程式を構築できます。一方、$s[i] = t[j]$ のときは現在の文字を編集する必要はありません。
|
||||
- 編集距離では、状態は真上、真左、左上の状態に依存します。そのため、空間最適化後は順方向でも逆方向でも正しく状態遷移できません。そこで、変数を 1 つ用いて左上の状態を一時保存し、完全ナップサック問題と等価な形へ変換することで、空間最適化後に順方向走査を行えるようにします。
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
# 無制限ナップサック問題
|
||||
# 完全ナップサック問題
|
||||
|
||||
このセクションでは、まず別の一般的なナップサック問題である無制限ナップサックを解決し、次にその特殊ケースであるコイン交換問題を探索します。
|
||||
本節では、まずもう 1 つの代表的なナップサック問題である完全ナップサック問題を解き、その特殊例である硬貨交換問題について見ていきます。
|
||||
|
||||
## 無制限ナップサック問題
|
||||
## 完全ナップサック問題
|
||||
|
||||
!!! question
|
||||
|
||||
$n$ 個のアイテムが与えられ、$i$ 番目のアイテムの重量は $wgt[i-1]$ で値は $val[i-1]$ です。容量が $cap$ のバックパックがあります。**各アイテムは複数回選択できます**。容量を超えることなくバックパックに入れることができるアイテムの最大値は何ですか?以下の例を参照してください。
|
||||
$n$ 個の品物が与えられ、$i$ 番目の品物の重さは $wgt[i-1]$、価値は $val[i-1]$ であり、容量 $cap$ のナップサックがあります。**各品物は繰り返し選択できます**。ナップサック容量の制約下で入れられる品物の最大価値を求めてください。例を以下の図に示します。
|
||||
|
||||

|
||||

|
||||
|
||||
### 動的プログラミングアプローチ
|
||||
### 動的計画法の考え方
|
||||
|
||||
無制限ナップサック問題は0-1ナップサック問題と非常に似ており、**唯一の違いはアイテムを選択できる回数に制限がないことです**。
|
||||
完全ナップサック問題は 0-1 ナップサック問題と非常によく似ています。**違いは、品物の選択回数に制限がない点だけです**。
|
||||
|
||||
- 0-1ナップサック問題では、各アイテムは1つしかないため、アイテム $i$ をバックパックに入れた後は、前の $i-1$ 個のアイテムからのみ選択できます。
|
||||
- 無制限ナップサック問題では、各アイテムの数量は無制限であるため、アイテム $i$ をバックパックに入れた後も、**前の $i$ 個のアイテムから引き続き選択できます**。
|
||||
- 0-1 ナップサック問題では、各品物は 1 つしかないため、品物 $i$ をナップサックに入れた後は先頭 $i-1$ 個の品物からしか選べません。
|
||||
- 完全ナップサック問題では、各品物の数は無限であるため、品物 $i$ をナップサックに入れた後も、**引き続き先頭 $i$ 個の品物から選べます**。
|
||||
|
||||
無制限ナップサック問題のルールの下で、状態 $[i, c]$ は2つの方法で変化できます。
|
||||
完全ナップサック問題では、状態 $[i, c]$ の変化は 2 つの場合に分けられます。
|
||||
|
||||
- **アイテム $i$ を入れない**:0-1ナップサック問題と同様に、$[i-1, c]$ に遷移します。
|
||||
- **アイテム $i$ を入れる**:0-1ナップサック問題とは異なり、$[i, c-wgt[i-1]]$ に遷移します。
|
||||
- **品物 $i$ を入れない** :0-1 ナップサック問題と同様に、$[i-1, c]$ へ遷移します。
|
||||
- **品物 $i$ を入れる** :0-1 ナップサック問題とは異なり、$[i, c-wgt[i-1]]$ へ遷移します。
|
||||
|
||||
したがって、状態遷移方程式は次のようになります:
|
||||
したがって、状態遷移方程式は次のようになります。
|
||||
|
||||
$$
|
||||
dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])
|
||||
@@ -30,7 +30,7 @@ $$
|
||||
|
||||
### コード実装
|
||||
|
||||
2つの問題のコードを比較すると、状態遷移が $i-1$ から $i$ に変わり、残りは完全に同一です:
|
||||
2 つの問題のコードを比較すると、状態遷移の中で 1 か所だけ $i-1$ が $i$ に変わり、それ以外は完全に同じです。
|
||||
|
||||
```src
|
||||
[file]{unbounded_knapsack}-[class]{}-[func]{unbounded_knapsack_dp}
|
||||
@@ -38,12 +38,12 @@ $$
|
||||
|
||||
### 空間最適化
|
||||
|
||||
現在の状態は左と上の状態から来るため、**空間最適化解法は $dp$ テーブルの各行に対して前方走査を実行する必要があります**。
|
||||
現在の状態は左側と上側の状態から遷移してくるため、**空間最適化後は $dp$ テーブルの各行を順方向に走査する必要があります**。
|
||||
|
||||
この走査順序は0-1ナップサックの場合とは逆です。違いを理解するために下の図を参照してください。
|
||||
この走査順序は 0-1 ナップサックとはちょうど逆です。両者の違いは次の図を用いて理解してください。
|
||||
|
||||
=== "<1>"
|
||||

|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
@@ -60,67 +60,67 @@ $$
|
||||
=== "<6>"
|
||||

|
||||
|
||||
コード実装は非常に簡単で、配列 `dp` の最初の次元を削除するだけです:
|
||||
コード実装は比較的簡単で、配列 `dp` の第 1 次元を削除するだけです。
|
||||
|
||||
```src
|
||||
[file]{unbounded_knapsack}-[class]{}-[func]{unbounded_knapsack_dp_comp}
|
||||
```
|
||||
|
||||
## コイン交換問題
|
||||
## 硬貨交換問題
|
||||
|
||||
ナップサック問題は動的プログラミング問題の大きなクラスの代表であり、コイン交換問題など多くの変種があります。
|
||||
ナップサック問題は動的計画法の代表的な問題群であり、多くの派生問題があります。硬貨交換問題もその 1 つです。
|
||||
|
||||
!!! question
|
||||
|
||||
$n$ 種類のコインが与えられ、$i$ 番目の種類のコインの額面は $coins[i - 1]$ で、目標金額は $amt$ です。**各種類のコインは複数回選択できます**。目標金額を構成するのに必要な最小コイン数は何ですか?目標金額を構成できない場合は $-1$ を返してください。以下の例を参照してください。
|
||||
$n$ 種類の硬貨が与えられ、$i$ 番目の硬貨の額面は $coins[i - 1]$ 、目標金額は $amt$ です。**各硬貨は繰り返し選択できます**。目標金額を作るために必要な最小の硬貨枚数を求めてください。目標金額を作れない場合は $-1$ を返します。例を以下の図に示します。
|
||||
|
||||

|
||||

|
||||
|
||||
### 動的プログラミングアプローチ
|
||||
### 動的計画法の考え方
|
||||
|
||||
**コイン交換は無制限ナップサック問題の特殊ケースと見なすことができ**、以下の類似点と相違点を共有しています。
|
||||
**硬貨交換は完全ナップサック問題の特殊なケースとみなせます**。両者には次の対応関係と相違点があります。
|
||||
|
||||
- 2つの問題は互いに変換できます:「アイテム」は「コイン」に対応し、「アイテムの重量」は「コインの額面」に対応し、「バックパックの容量」は「目標金額」に対応します。
|
||||
- 最適化目標は逆です:無制限ナップサック問題はアイテムの値を最大化することを目的とし、コイン交換問題はコインの数を最小化することを目的とします。
|
||||
- 無制限ナップサック問題はバックパック容量を「超えない」解を求め、コイン交換は目標金額を「正確に」構成する解を求めます。
|
||||
- 2 つの問題は相互に変換でき、「品物」は「硬貨」、「品物の重さ」は「硬貨の額面」、「ナップサック容量」は「目標金額」に対応します。
|
||||
- 最適化の目標は逆であり、完全ナップサック問題は品物価値の最大化、硬貨交換問題は硬貨枚数の最小化を目指します。
|
||||
- 完全ナップサック問題はナップサック容量を「超えない」解を求めますが、硬貨交換は目標金額に「ちょうど」一致する解を求めます。
|
||||
|
||||
**第1ステップ:各ラウンドの意思決定を考え、状態を定義し、それにより $dp$ テーブルを導出する**
|
||||
**ステップ 1:各ラウンドの選択を考え、状態を定義して、$dp$ テーブルを得る**
|
||||
|
||||
状態 $[i, a]$ は部分問題に対応します:**最初の $i$ 種類のコインを使用して金額 $a$ を構成できる最小コイン数**、$dp[i, a]$ と表記されます。
|
||||
状態 $[i, a]$ に対応する部分問題は、**先頭 $i$ 種類の硬貨で金額 $a$ を作るための最小硬貨枚数**であり、これを $dp[i, a]$ と表します。
|
||||
|
||||
二次元 $dp$ テーブルのサイズは $(n+1) \times (amt+1)$ です。
|
||||
2 次元 $dp$ テーブルのサイズは $(n+1) \times (amt+1)$ です。
|
||||
|
||||
**第2ステップ:最適部分構造を特定し、状態遷移方程式を導出する**
|
||||
**ステップ 2:最適部分構造を見つけ、状態遷移方程式を導く**
|
||||
|
||||
この問題は状態遷移方程式の2つの側面で無制限ナップサック問題と異なります。
|
||||
本問の状態遷移方程式は、完全ナップサック問題と比べて次の 2 点が異なります。
|
||||
|
||||
- この問題は最小値を求めるため、演算子 $\max()$ を $\min()$ に変更する必要があります。
|
||||
- 最適化はコインの数に焦点を当てているため、コインが選択されたときに単純に $+1$ を追加します。
|
||||
- 本問では最小値を求めるため、演算子 $\max()$ を $\min()$ に変更する必要があります。
|
||||
- 最適化の対象は品物価値ではなく硬貨枚数であるため、硬貨を選んだときは $+1$ すれば十分です。
|
||||
|
||||
$$
|
||||
dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1)
|
||||
$$
|
||||
|
||||
**第3ステップ:境界条件と状態遷移順序を定義する**
|
||||
**ステップ 3:境界条件と状態遷移順序を決める**
|
||||
|
||||
目標金額が $0$ の場合、それを構成するのに必要な最小コイン数は $0$ であるため、最初の列のすべての $dp[i, 0]$ は $0$ です。
|
||||
目標金額が $0$ のとき、それを作るための最小硬貨枚数は $0$ です。つまり、先頭列のすべての $dp[i, 0]$ は $0$ になります。
|
||||
|
||||
コインがない場合、**任意の金額 >0 を構成することは不可能**であり、これは無効な解です。状態遷移方程式の $\min()$ 関数が無効な解を認識してフィルタリングできるように、$+\infty$ を使用してそれらを表現することを検討し、つまり最初の行のすべての $dp[0, a]$ を $+\infty$ に設定します。
|
||||
硬貨が 1 枚もない場合、**任意の $> 0$ の目標金額を作ることはできません**。これは無効解です。状態遷移方程式内の $\min()$ 関数が無効解を識別して除外できるように、それらを $+ \infty$ で表すことを考えます。すなわち、先頭行のすべての $dp[0, a]$ を $+ \infty$ とします。
|
||||
|
||||
### コード実装
|
||||
|
||||
ほとんどのプログラミング言語は $+\infty$ 変数を提供しておらず、整数 `int` の最大値のみを代替として使用できます。これによりオーバーフローが発生する可能性があります:状態遷移方程式の $+1$ 演算がオーバーフローする可能性があります。
|
||||
多くのプログラミング言語には $+ \infty$ を表す変数が用意されていないため、通常は整数型 `int` の最大値で代用します。しかし、その場合は大きな数のオーバーフローが起こり得ます。状態遷移方程式中の $+ 1$ 操作で桁あふれが発生する可能性があるためです。
|
||||
|
||||
この理由で、数値 $amt + 1$ を使用して無効な解を表します。なぜなら、$amt$ を構成するのに必要な最大コイン数は最大でも $amt$ だからです。結果を返す前に、$dp[n, amt]$ が $amt + 1$ に等しいかどうかを確認し、そうであれば $-1$ を返し、目標金額を構成できないことを示します。コードは以下の通りです:
|
||||
そのため、ここでは数値 $amt + 1$ を無効解の表現として用います。金額 $amt$ を作るための硬貨枚数は最大でも $amt$ 枚だからです。最後に返す前に、$dp[n, amt]$ が $amt + 1$ に等しいかを判定し、等しければ $-1$ を返して目標金額を作れないことを表します。コードは次のとおりです。
|
||||
|
||||
```src
|
||||
[file]{coin_change}-[class]{}-[func]{coin_change_dp}
|
||||
```
|
||||
|
||||
下の図はコイン交換問題の動的プログラミングプロセスを示しており、無制限ナップサック問題と非常に似ています。
|
||||
次の図は硬貨交換の動的計画法の過程を示しており、完全ナップサック問題と非常によく似ています。
|
||||
|
||||
=== "<1>"
|
||||

|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
@@ -166,31 +166,31 @@ $$
|
||||
|
||||
### 空間最適化
|
||||
|
||||
コイン交換問題の空間最適化は無制限ナップサック問題と同じ方法で処理されます:
|
||||
硬貨交換の空間最適化の方法は、完全ナップサック問題と同じです。
|
||||
|
||||
```src
|
||||
[file]{coin_change}-[class]{}-[func]{coin_change_dp_comp}
|
||||
```
|
||||
|
||||
## コイン交換問題II
|
||||
## 硬貨交換問題 II
|
||||
|
||||
!!! question
|
||||
|
||||
$n$ 種類のコインが与えられ、$i$ 番目の種類のコインの額面は $coins[i - 1]$ で、目標金額は $amt$ です。各種類のコインは複数回選択でき、**目標金額を構成できるコインの組み合わせは何通りありますか**。以下の例を参照してください。
|
||||
$n$ 種類の硬貨が与えられ、$i$ 番目の硬貨の額面は $coins[i - 1]$ 、目標金額は $amt$ です。各硬貨は繰り返し選択できるとして、**目標金額を作る硬貨の組合せ数**を求めてください。例を以下の図に示します。
|
||||
|
||||

|
||||

|
||||
|
||||
### 動的プログラミングアプローチ
|
||||
### 動的計画法の考え方
|
||||
|
||||
前の問題と比較して、この問題の目標は組み合わせの数を決定することであるため、部分問題は次のようになります:**最初の $i$ 種類のコインを使用して金額 $a$ を構成できる組み合わせの数**。$dp$ テーブルはサイズ $(n+1) \times (amt + 1)$ の二次元行列のまま残ります。
|
||||
前問と比べて、本問の目的は組合せ数を求めることです。そのため、部分問題は **先頭 $i$ 種類の硬貨で金額 $a$ を作れる組合せ数** になります。一方、$dp$ テーブルは引き続きサイズ $(n+1) \times (amt + 1)$ の 2 次元行列です。
|
||||
|
||||
現在の状態の組み合わせ数は、現在のコインを選択しない組み合わせと現在のコインを選択する組み合わせの合計です。状態遷移方程式は:
|
||||
現在の状態における組合せ数は、現在の硬貨を選ばない場合と選ぶ場合の 2 つの選択肢の組合せ数の和に等しくなります。状態遷移方程式は次のとおりです。
|
||||
|
||||
$$
|
||||
dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]]
|
||||
$$
|
||||
|
||||
目標金額が $0$ の場合、目標金額を構成するのにコインは必要ないため、最初の列のすべての $dp[i, 0]$ は $1$ に初期化されるべきです。コインがない場合、任意の金額 >0 を構成することは不可能であるため、最初の行のすべての $dp[0, a]$ は $0$ に設定されるべきです。
|
||||
目標金額が $0$ のときは、どの硬貨も選ばなくても目標金額を作れるため、先頭列のすべての $dp[i, 0]$ を $1$ に初期化します。硬貨がないときは、任意の $>0$ の目標金額を作れないため、先頭行のすべての $dp[0, a]$ は $0$ になります。
|
||||
|
||||
### コード実装
|
||||
|
||||
@@ -200,7 +200,7 @@ $$
|
||||
|
||||
### 空間最適化
|
||||
|
||||
空間最適化アプローチは同じで、コインの次元を削除するだけです:
|
||||
空間最適化の方法も同様で、硬貨の次元を削除するだけです。
|
||||
|
||||
```src
|
||||
[file]{coin_change_ii}-[class]{}-[func]{coin_change_ii_dp_comp}
|
||||
|
||||
Reference in New Issue
Block a user