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:
Yudong Jin
2026-03-30 07:30:15 +08:00
committed by GitHub
parent fe6443235b
commit d7b2277d2b
1444 changed files with 83312 additions and 8363 deletions

View File

@@ -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$ です。
![3段目に登る最小コスト](dp_problem_features.assets/min_cost_cs_example.png)
![第 3 段まで上る最小コスト](dp_problem_features.assets/min_cost_cs_example.png)
$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}
```
図は上記コードの動的プログラミングプロセスを示しています。
下図は上記コードの動的計画法の過程を示しています。
![階段登りの最小コストの動的プログラミングプロセス](dp_problem_features.assets/min_cost_cs_dp.png)
![階段昇り最小コストの動的計画法の過程](dp_problem_features.assets/min_cost_cs_dp.png)
この問題空間最適化可能で、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 回連続で上る方法は制約を満たさないため除外されます。
![制約付きで3段目に登る実行可能な選択肢の](dp_problem_features.assets/climbing_stairs_constraint_example.png)
![制約付きで第 3 段まで上る方法](dp_problem_features.assets/climbing_stairs_constraint_example.png)
この問題では、前回が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_problem_features.assets/climbing_stairs_constraint_state_transfer.png)
![制約を考慮した漸化関係](dp_problem_features.assets/climbing_stairs_constraint_state_transfer.png)
最終的に、$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$ 段に跳ぶことはできません。頂上まで上る方法は何通りあるでしょうか。
この問題では、次のジャンプはすべての過去の状態に依存します。各ジャンプがより高い段に障害物を置き、将来のジャンプに影響するからです。このような問題は、動的プログラミングはしばしば解決に苦労します。
この問題では、次の跳躍が過去のすべての状態に依存します。なぜなら、各跳躍がより高い段に障害物を設置し、将来の跳躍に影響するからです。この種の問題は、動的計画法では解きにくいことが多いです。
実際、多くの複雑な組み合わせ最適化問題(巡回セールスマン問題など)は無記憶性を満たしません。このような問題に対しては、通常、ヒューリスティック探索、遺伝的アルゴリズム、強化学習などの他の方法を選択して、限られた時間内に使用可能な局所最適解を得ます。
実際、多くの複雑な組せ最適化問題(例えば巡回セールスマン問題)は無後効性を満たしません。このような問題に対しては、通常、ヒューリスティック探索、遺伝的アルゴリズム、強化学習などの他の方法を用いて、限られた時間内に実用的な局所最適解を得ます。

View File

@@ -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$ です。
![最小経路和のデータ](dp_solution_pipeline.assets/min_path_sum_example.png)
![最小経路和のサンプルデータ](dp_solution_pipeline.assets/min_path_sum_example.png)
**第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$ と同じです。
![状態定義とDPテーブル](dp_solution_pipeline.assets/min_path_sum_solution_state_definition.png)
![状態定義と dp テーブル](dp_solution_pipeline.assets/min_path_sum_solution_state_definition.png)
!!! 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$ が境界条件になります。
の図に示すように、各セルは左のセルと上のセルから導出されるため、ループを使用して行列を走査し外側のループは行を反復し、内側のループは列を反復します。
の図に示すように、各マスは左のマスと上のマスから遷移してくるため、ループを用いて行列を走査します。外側のループで各行を、内側のループで各列を走査します。
![境界条件と状態遷移順序](dp_solution_pipeline.assets/min_path_sum_solution_initial_state.png)
![境界条件と状態遷移順序](dp_solution_pipeline.assets/min_path_sum_solution_initial_state.png)
!!! 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` のサイズが大きくなるにつれて急激に増加します。
本質的に、重複する部分問題理由は**左上から特定のセル到達する複数のパスが存在する**ことです。
本質的に、重複部分問題が生じる理由は**左上からあるセル到達する経路が複数存在すること**にあります。
![力任せ探索の再帰木](dp_solution_pipeline.assets/min_path_sum_dfs.png)
各状態には下と右の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)$ に依存します。
![メモ化探索の再帰木](dp_solution_pipeline.assets/min_path_sum_dfs_mem.png)
### 方法3動的プログラミング
### 方法 3動的計画法
動的プログラミング解法を反復的に実装します。コードは以下の通りです:
反復に基づいて動的計画法の解法を実装すると、コードは次のようになります。
```src
[file]{min_path_sum}-[class]{}-[func]{min_path_sum_dp}
```
の図は最小経路和の状態遷移プロセスを示し、グリッド全体を走査するため、**時間計算量は $O(nm)$** です。
の図は最小経路和の状態遷移の過程を示しています。グリッド全体を走査するため、**時間計算量は $O(nm)$** です。
配列 `dp` のサイズは $n \times m$ であるため、**空間計算量は $O(nm)$** です。
=== "<1>"
![最小経路和の動的プログラミングプロセス](dp_solution_pipeline.assets/min_path_sum_dp_step1.png)
![最小経路和の動的計画法の過程](dp_solution_pipeline.assets/min_path_sum_dp_step1.png)
=== "<2>"
![min_path_sum_dp_step2](dp_solution_pipeline.assets/min_path_sum_dp_step2.png)
@@ -174,9 +174,9 @@ $$
### 空間最適化
セルは左と上のセルのみに関連するため、単一行配列を使用して $dp$ テーブルを実装できます。
マスは左のマスと上のマスにのみ関係するため、1 行の配列だけを使って $dp$ テーブルを実装できます。
配列 `dp`1行の状態のみを表現できるため、最初の列の状態を事前に初期化できず、各行を走査するときに更新することに注意してください:
ただし、配列 `dp` 1 行分の状態しか表せないため、先頭列の状態を事前に初期化することはできず、各行を走査するときに更新する必要があります。
```src
[file]{min_path_sum}-[class]{}-[func]{min_path_sum_dp_comp}

View File

@@ -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回の削除を含みます。
![編集距離のサンプルデータ](edit_distance_problem.assets/edit_distance_example.png)
![編集距離の例データ](edit_distance_problem.assets/edit_distance_example.png)
**編集距離問題は決定木モデルで自然に説明できます**。文字列が木のードに対応し、1回の決定1回の編集操作が木の1本の辺に対応します。
**編集距離問題は決定木モデルで自然に説明できます**。文字列は木のードに対応し、1ラウンドの決定編集操作は木のエッジに対応します。
下図に示すように、操作に制限がない場合、各ードからは多くの辺を派生でき、それぞれの辺が1種類の操作に対応します。これは `hello` から `algo` への変換に多くの経路があり得ることを意味します。
下の図に示すように、操作に制限がない場合、各ードは多くのエッジを導出でき、それぞれが1つの操作に対応するため、`hello` `algo` に変換する可能な経路は多数あります。
決定木の観点から見ると、本問の目標はノード `hello` とノード `algo` の間の最短経路を求めることです。
決定木の観点から、この問題の目標は、ノード `hello` とノード `algo` の間の最短経路を見つけることです。
![決定木モデルに基づく編集距離問題の表現](edit_distance_problem.assets/edit_distance_decision_tree.png)
![決定木モデルに基づいて表現された編集距離問題](edit_distance_problem.assets/edit_distance_decision_tree.png)
### 動的計画法の考え方
### 動的プログラミングアプローチ
**第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]$ です。
![編集距離の状態遷移](edit_distance_problem.assets/edit_distance_state_transfer.png)
の分析に基づいて、最適部分構造を決定できます$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>"
![編集距離の動的プログラミングプロセス](edit_distance_problem.assets/edit_distance_dp_step1.png)
![編集距離の動的計画法の過程](edit_distance_problem.assets/edit_distance_dp_step1.png)
=== "<2>"
![edit_distance_dp_step2](edit_distance_problem.assets/edit_distance_dp_step2.png)
@@ -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}

View File

@@ -1,9 +1,9 @@
# 動的プログラミング
# 動的計画法
![動的プログラミング](../assets/covers/chapter_dynamic_programming.jpg)
![動的計画法](../assets/covers/chapter_dynamic_programming.jpg)
!!! abstract
川が流れて海に注ぐように、
動的プログラミングは小さな問題の解を織り合わせて、より大きな問題の解へと導きます。一歩一歩進んで、最終的な答えが待つ彼岸へと向かいます
小川は川へと注ぎ、河川は大海へと注ぐ。
動的計画法は小さな問題の解を集めて大きな問題の答えとし、一歩ずつ私たちを問題解決の彼岸へと導く

View File

@@ -1,110 +1,110 @@
# 動的プログラミングの紹介
# 動的計画法入門
<u>動的プログラミング</u>は重要なアルゴリズムパラダイムであり、問題を一連の小さな部分問題に分解し、れらの部分問題の解を保存することで冗長な計算を避け、時間効率を大幅に向上させます。
<u>動的計画法dynamic programming</u>は重要なアルゴリズムパラダイムであり、問題をより小さな部分問題の列に分解し、れらの解を保存して重複計算を避けることで、時間効率を大幅に向上させます。
このセクションでは、古典的な題から始めて、まず力任せの探索法による解法を示し、重複する部分問題を特定してから、より効率的な動的プログラミング解法を段階的に導出します。
本節では、古典的な題から始めて、まずその力任せのバックトラッキング解法を示し、そこに含まれる重複部分問題を観察したうえで、より効率的な動的計画法の解法を段階的に導ます。
!!! question "階段登り"
!!! question "階段を上る"
$n$ 段階段があり、一度に $1$ 段または $2$ 段上ることができます。頂上到達する方法は何通りありますか?
全体で $n$ 段ある階段が与えられ、各ステップで $1$ 段または $2$ 段上ることができます。頂上まで到達する方法は何通りあるでしょうか?
の図に示すように、$3$ 段の階段の頂上に到達する方法は $3$ 通りあります。
の図に示すように、$3$ 段の階段では、頂上まで到達する方法は全部で $3$ 通りあります。
![3段目に到達する方法の数](intro_to_dynamic_programming.assets/climbing_stairs_example.png)
![3 段の階段を上る方法の数](intro_to_dynamic_programming.assets/climbing_stairs_example.png)
この問題は**バックトラッキングを用いてすべての可能性を網羅**することで方法の数を計算することを目的としています。具体的には、階段登りの問題を多段階選択プロセスとして考えます:地面から始めて、毎回 $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]
$$
これは、階段登り問題において部分問題間に再帰関係があることを意味し、**元の問題の解は部分問題の解から構築できます**。下の図はこの再帰関係を示しています。
これは、階段を上る問題では各部分問題間に漸化関係があ、**元の問題の解は部分問題の解から構築できる**ことを意味します。次の図はこの漸化関係を示しています。
![解の数の再帰関係](intro_to_dynamic_programming.assets/climbing_stairs_state_transfer.png)
![方法数の漸化関係](intro_to_dynamic_programming.assets/climbing_stairs_state_transfer.png)
再帰式に従って力任せ探索解法を得ることができます。$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$ を入力すると長時間待たされることになります。
![階段登りの再帰木](intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png)
![階段上りに対応する再帰木](intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png)
上の図を観察すると、**指数時間計算量は「重複する部分問題」によって引き起こされる**ことがわかります。例えば$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)$ まで改善されます**。これは大きな飛躍です。
![メモ化探索にる再帰木](intro_to_dynamic_programming.assets/climbing_stairs_dfs_memo_tree.png)
![メモ化探索に対応する再帰木](intro_to_dynamic_programming.assets/climbing_stairs_dfs_memo_tree.png)
## 方法3動的プログラミング
## 方法 3動的計画法
**メモ化探索は「トップダウン」方式**です元の問題(根ノード)から始めて、より大きな部分問題をより小さなものに再帰的に分解し、最小の既知の部分問題(葉ノード)の解に到達するまで続けます。その後、バックトラッキングにより部分問題の解を収集し、元の問題の解を構築します。
**メモ化探索は「トップダウン」の方法**です元の問題(根ノード)から始めて、より大きな部分問題を再帰的により小さな部分問題へ分解し、解が既知である最小部分問題(葉ノード)に至ります。その後、バックトラックしながら各層で部分問題の解を集め、元の問題の解を構築します。
一方、**動的プログラミングは「ボトムアップ」方式**です最小部分問題の解から始めて、元の問題が解決されるまで、より大きな部分問題の解を反復的に構築します。
これとは対照的に、**動的計画法は「ボトムアップ」の方法**です最小部分問題の解から始めて、より大きな部分問題の解を反復的に構築し、最終的に元の問題の解を得ます。
動的プログラミングはバックトラッキングを必要としないため、ループを使った反復のみが必要で、再帰は不要です。以下のコードでは、配列 `dp` を初期化して部分問題の解を保存し、メモ化探索配列 `mem` と同じ記録機能を果たします:
動的計画法にはバックトラックの過程が含まれないため、再帰を使う必要はなく、ループによる反復だけで実装できます。次のコードでは、部分問題の解を保存する配列 `dp` を初期化しており、これはメモ化探索における配列 `mem` と同じ記録の役割を果たします:
```src
[file]{climbing_stairs_dp}-[class]{}-[func]{climbing_stairs_dp}
```
の図は上記コードの実行プロセスをシミュレートしています。
の図は、以上のコードの実行過程をシミュレートしたものです。
![階段りの動的プログラミングプロセス](intro_to_dynamic_programming.assets/climbing_stairs_dp.png)
![階段りの動的計画法の過程](intro_to_dynamic_programming.assets/climbing_stairs_dp.png)
バックトラッキングアルゴリズムと同様に、動的プログラミングも「状態」の概念を使用して問題解決の特定段階を表現し、各状態は部分問題とその局所最適解に対応します。えば、階段登り問題の状態は現在のステップ番号 $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)$ へと下がります。
多くの動的プログラミング問題では、現在の状態は限られた数の前の状態のみに依存するため、必要な状態のみを保持し、「次元削減」によってメモリ空間を節約できます。**この空間最適化技術は「ローリング変数」または「ローリング配列」として知られています**。
動的計画法の問題では、現在の状態はしばしば直前の限られた個数の状態にしか関係しません。このような場合は、必要な状態だけを保持し、「次元削減」によってメモリ空間を節約できます。**この空間最適化の技巧は「ローリング変数」または「ローリング配列」と呼ばれます**。

View File

@@ -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ナップサックのデータ](knapsack_problem.assets/knapsack_example.png)
![0-1 ナップサックのサンプルデータ](knapsack_problem.assets/knapsack_example.png)
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]$ などの重複部分問題が存在することがかります。品物数が多く、ナップサック容量が大き、特に同じ重さの品物が多い場合には、重複部分問題の数は大幅に増加します。
![0-1ナップサック問題の力任せ探索再帰木](knapsack_problem.assets/knapsack_dfs.png)
![0-1 ナップサック問題の探索再帰木](knapsack_problem.assets/knapsack_dfs.png)
### 方法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}
```
の図はメモ化探索で枝刈りされ探索分岐を示しています。
の図はメモ化探索で剪定され探索分岐を示しています。
![0-1ナップサック問題のメモ化探索再帰木](knapsack_problem.assets/knapsack_dfs_mem.png)
![0-1 ナップサック問題のメモ化探索再帰木](knapsack_problem.assets/knapsack_dfs_mem.png)
### 方法3動的プログラミング
### 方法3動的計画法
動的プログラミングは本質的に状態遷移中に $dp$ テーブルを埋めることを含みます。コードは下の図に示されています
動的計画法の本質は、状態遷移に従って $dp$ テーブルを埋めていく過程です。コードは次のようになります
```src
[file]{knapsack}-[class]{}-[func]{knapsack_dp}
```
下の図に示すように、時間計算量と空間計算量の両方が配列 `dp` のサイズ、つまり $O(n \times cap)$ によって決定されます。
下の図ように、時間計算量と空間計算量はいずれも配列 `dp` のサイズによって決まり$O(n \times cap)$ す。
=== "<1>"
![0-1ナップサック問題の動的プログラミングプロセス](knapsack_problem.assets/knapsack_dp_step1.png)
![0-1 ナップサック問題の動的計画法の過程](knapsack_problem.assets/knapsack_dp_step1.png)
=== "<2>"
![knapsack_dp_step2](knapsack_problem.assets/knapsack_dp_step2.png)
@@ -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>"
![0-1ナップサックの空間最適化動的プログラミングプロセス](knapsack_problem.assets/knapsack_dp_comp_step1.png)
![0-1 ナップサックの空間最適化後の動的計画法の過程](knapsack_problem.assets/knapsack_dp_comp_step1.png)
=== "<2>"
![knapsack_dp_comp_step2](knapsack_problem.assets/knapsack_dp_comp_step2.png)
@@ -161,7 +161,7 @@ $$
=== "<6>"
![knapsack_dp_comp_step6](knapsack_problem.assets/knapsack_dp_comp_step6.png)
コード実装では、配列 `dp`最初の次元 $i$ を削除し、内側のループを逆走査に変更するだけです:
コード実装では、配列 `dp`第1次元 $i$ をそのまま削除し、内側のループを逆方向走査に変更するだけで済みます。
```src
[file]{knapsack}-[class]{}-[func]{knapsack_dp_comp}

View File

@@ -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 つ用いて左上の状態を一時保存し、完全ナップサック問題と等価な形へ変換することで、空間最適化後に順方向走査を行えるようにします。

View File

@@ -1,28 +1,28 @@
# 無制限ナップサック問題
# 完全ナップサック問題
このセクションでは、まず別の一般的なナップサック問題である無制限ナップサックを解決し、次にその特殊ケースであるコイン交換問題を探索します。
本節では、まずもう 1 つの代表的なナップサック問題である完全ナップサック問題を解き、その特殊例である硬貨交換問題について見ていきます。
## 無制限ナップサック問題
## 完全ナップサック問題
!!! question
$n$ 個のアイテムが与えられ、$i$ 番目のアイテムの重は $wgt[i-1]$値は $val[i-1]$ です。容量 $cap$ のバックパックがあります。**各アイテムは複数回選択できます**。容量を超えることなくバックパックに入れることができるアイテムの最大値は何ですか?以下の例を参照してください
$n$ 個の品物が与えられ、$i$ 番目の品物の重は $wgt[i-1]$、価値は $val[i-1]$ であり、容量 $cap$ のナップサックがあります。**各品物は繰り返し選択できます**。ナップサック容量の制約下で入れられる品物の最大価値を求めてください。例を以下の図に示します
![無制限ナップサック問題のデータ](unbounded_knapsack_problem.assets/unbounded_knapsack_example.png)
![完全ナップサック問題のサンプルデータ](unbounded_knapsack_problem.assets/unbounded_knapsack_example.png)
### 動的プログラミングアプローチ
### 動的計画法の考え方
無制限ナップサック問題は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>"
![空間最適化後の無制限ナップサック問題の動的プログラミングプロセス](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step1.png)
![完全ナップサック問題における空間最適化後の動的計画法の過程](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step1.png)
=== "<2>"
![unbounded_knapsack_dp_comp_step2](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step2.png)
@@ -60,67 +60,67 @@ $$
=== "<6>"
![unbounded_knapsack_dp_comp_step6](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step6.png)
コード実装は非常に簡単で、配列 `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$ を返します。例を以下の図に示します
![コイン交換問題のデータ](unbounded_knapsack_problem.assets/coin_change_example.png)
![硬貨交換問題のサンプルデータ](unbounded_knapsack_problem.assets/coin_change_example.png)
### 動的プログラミングアプローチ
### 動的計画法の考え方
**コイン交換は無制限ナップサック問題の特殊ケースと見なすことができ**、以下の類似点と相違点を共有しています。
**硬貨交換は完全ナップサック問題の特殊ケースとみなせます**。両者には次の対応関係と相違点があります。
- 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>"
![コイン交換問題の動的プログラミングプロセス](unbounded_knapsack_problem.assets/coin_change_dp_step1.png)
![硬貨交換問題の動的計画法の過程](unbounded_knapsack_problem.assets/coin_change_dp_step1.png)
=== "<2>"
![coin_change_dp_step2](unbounded_knapsack_problem.assets/coin_change_dp_step2.png)
@@ -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$ です。各硬貨は繰り返し選択できるとして、**目標金額を作る硬貨の組合せ数**を求めてください。例を以下の図に示します
![コイン交換問題IIの例データ](unbounded_knapsack_problem.assets/coin_change_ii_example.png)
![硬貨交換問題 II のサンプルデータ](unbounded_knapsack_problem.assets/coin_change_ii_example.png)
### 動的プログラミングアプローチ
### 動的計画法の考え方
の問題と比較して、この問題の目は組み合わせの数を決定することであるため、部分問題は次のようになります:**最初の $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}