docs: add Japanese translate documents (#1812)

* docs: add Japanese documents (`ja/docs`)

* docs: add Japanese documents (`ja/codes`)

* docs: add Japanese documents

* Remove pythontutor blocks in ja/

* Add an empty at the end of each markdown file.

* Add the missing figures (use the English version temporarily).

* Add index.md for Japanese version.

* Add index.html for Japanese version.

* Add missing index.assets

* Fix backtracking_algorithm.md for Japanese version.

* Add avatar_eltociear.jpg. Fix image links on the Japanese landing page.

* Add the Japanese banner.

---------

Co-authored-by: krahets <krahets@163.com>
This commit is contained in:
Ikko Eltociear Ashimine
2025-10-17 06:04:43 +09:00
committed by GitHub
parent 2487a27036
commit 954c45864b
886 changed files with 33569 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1,101 @@
# 動的プログラミング問題の特徴
前のセクションでは、動的プログラミングが問題を部分問題に分解することで元の問題を解決する方法を学びました。実際、部分問題の分解は一般的なアルゴリズムアプローチであり、分割統治法、動的プログラミング、バックトラッキングでは異なる重点があります。
- 分割統治法アルゴリズムは元の問題を複数の独立した部分問題に再帰的に分割し、最小の部分問題に到達するまで続け、バックトラッキング時に部分問題の解を組み合わせて最終的に元の問題の解を得ます。
- 動的プログラミングも問題を再帰的に分解しますが、分割統治法アルゴリズムとの主な違いは、動的プログラミングの部分問題が相互依存的であり、分解プロセス中に多くの重複する部分問題が現れることです。
- バックトラッキングアルゴリズムは試行錯誤によってすべての可能な解を網羅し、枝刈りによって不必要な探索分岐を避けます。元の問題の解は一連の決定ステップから構成され、各決定ステップ前の各部分シーケンスを部分問題として考えることができます。
実際、動的プログラミングは最適化問題を解決するためによく使用され、これらは重複する部分問題を含むだけでなく、他に2つの主要な特徴があります最適部分構造と無記憶性です。
## 最適部分構造
階段登り問題を少し修正して、最適部分構造の概念を実証するのにより適したものにします。
!!! question "階段登りの最小コスト"
階段があり、一度に1段または2段上ることができ、階段の各段にはその段で支払う必要があるコストを表す非負の整数があります。非負の整数配列 $cost$ が与えられ、$cost[i]$ は $i$ 段目で支払う必要があるコストを表し、$cost[0]$ は地面(開始点)です。頂上に到達するために必要な最小コストは何ですか?
下の図に示すように、1段目、2段目、3段目のコストがそれぞれ $1$、$10$、$1$ の場合、地面から3段目に登る最小コストは $2$ です。
![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] = \min(dp[i-1], dp[i-2]) + cost[i]
$$
これにより最適部分構造の意味がわかります:**元の問題の最適解は部分問題の最適解から構築される**。
この問題は明らかに最適部分構造を持っています2つの部分問題 $dp[i-1]$ と $dp[i-2]$ の最適解からより良い方を選択し、それを使用して元の問題 $dp[i]$ の最適解を構築します。
では、前のセクションの階段登り問題は最適部分構造を持っているでしょうか?その目標は解の数を求めることで、これは数え上げ問題のようですが、別の方法で尋ねてみましょう:「解の最大数を求める」。驚くことに、**問題が変わったにもかかわらず、最適部分構造が現れた**ことがわかります:$n$ 段目での解の最大数は、$n-1$ 段目と $n-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)
この問題も空間最適化が可能で、1次元を0に圧縮し、空間計算量を $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$ の将来に影響しないからです。
しかし、階段登り問題に制約を追加すると、状況が変わります。
!!! question "制約付き階段登り"
$n$ 段の階段があり、毎回1段または2段上ることができますが、**1段を2回連続でジャンプすることはできません**。頂上に登る方法は何通りありますか?
下の図に示すように、3段目に登る実行可能な選択肢は2つだけで、1段を3回連続でジャンプする選択肢は制約条件を満たさないため破棄されます。
![制約付きで3段目に登る実行可能な選択肢の数](dp_problem_features.assets/climbing_stairs_constraint_example.png)
この問題では、前回が1段ジャンプだった場合、次回は必ず2段ジャンプでなければなりません。これは**次のステップの選択が現在の状態(現在の階段段数)だけでは独立して決定できず、前の状態(前回の階段段数)にも依存する**ことを意味します。
この問題がもはや無記憶性を満たさず、状態遷移方程式 $dp[i] = dp[i-1] + dp[i-2]$ も失敗することは容易にわかります。なぜなら $dp[i-1]$ は今回の1段ジャンプを表しますが、多くの「前回が1段ジャンプだった」選択肢を含んでおり、制約を満たすためにはこれらを直接 $dp[i]$ に含めることはできません。
このため、状態定義を拡張する必要があります:**状態 $[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]$ から遷移できます。
下の図に示すように、$dp[i, j]$ は状態 $[i, j]$ の解の数を表します。この時点で、状態遷移方程式は次のようになります:
$$
\begin{cases}
dp[i, 1] = dp[i-1, 2] \\
dp[i, 2] = dp[i-2, 1] + dp[i-2, 2]
\end{cases}
$$
![制約を考慮した再帰関係](dp_problem_features.assets/climbing_stairs_constraint_state_transfer.png)
最終的に、$dp[n, 1] + dp[n, 2]$ を返せばよく、この2つの合計が $n$ 段目に登る解の総数を表します:
```src
[file]{climbing_stairs_constraint_dp}-[class]{}-[func]{climbing_stairs_constraint_dp}
```
上記のケースでは、前の状態のみを考慮すればよいため、状態定義を拡張することで依然として無記憶性を満たすことができます。しかし、一部の問題では非常に深刻な「状態効果」があります。
!!! question "障害物生成付き階段登り"
$n$ 段の階段があり、毎回1段または2段上ることができます。**$i$ 段目に登ったとき、システムが自動的に $2i$ 段目に障害物を置き、その後のすべてのラウンドで $2i$ 段目にジャンプすることが禁止される**と規定されています。例えば、最初の2ラウンドで2段目と3段目にジャンプした場合、その後は4段目と6段目にジャンプできません。頂上に登る方法は何通りありますか
この問題では、次のジャンプはすべての過去の状態に依存します。各ジャンプがより高い段に障害物を置き、将来のジャンプに影響するからです。このような問題では、動的プログラミングはしばしば解決に苦労します。
実際、多くの複雑な組み合わせ最適化問題(巡回セールスマン問題など)は無記憶性を満たしません。このような問題に対しては、通常、ヒューリスティック探索、遺伝的アルゴリズム、強化学習などの他の方法を選択して、限られた時間内に使用可能な局所最適解を得ます。

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,183 @@
# 動的プログラミング問題解決アプローチ
前の2つのセクションでは、動的プログラミング問題の主要な特徴を紹介しました。次に、より実用的な2つの問題を一緒に探索しましょう。
1. 問題が動的プログラミング問題かどうかをどのように判断するか?
2. 動的プログラミング問題を解決する完全なステップは何か?
## 問題の判定
一般的に言えば、問題が重複する部分問題、最適部分構造を含み、無記憶性を示す場合、通常動的プログラミング解法に適しています。しかし、問題の説明から直接これらの特徴を抽出することはしばしば困難です。したがって、通常は条件を緩和し、**まず問題がバックトラッキング(全探索)を使用した解決に適しているかどうかを観察**します。
**バックトラッキングに適した問題は通常「決定木モデル」に適合**し、これは木構造を使用して記述でき、各ノードは決定を表し、各パスは決定のシーケンスを表します。
言い換えると、問題が明示的な決定概念を含み、解が一連の決定を通じて生成される場合、それは決定木モデルに適合し、通常バックトラッキングを使用して解決できます。
この基礎の上で、動的プログラミング問題を判定するための「ボーナスポイント」があります。
- 問題に最大化(最小化)または最も(最も少ない)最適な解を見つけるという記述が含まれている。
- 問題の状態がリスト、多次元行列、または木を使用して表現でき、状態がその周囲の状態と再帰関係を持っている。
対応して、「ペナルティポイント」もあります。
- 問題の目標は最適解だけでなく、すべての可能な解を見つけることである。
- 問題の説明に順列と組み合わせの明らかな特徴があり、特定の複数の解を返す必要がある。
問題が決定木モデルに適合し、比較的明らかな「ボーナスポイント」を持つ場合、それが動的プログラミング問題であると仮定し、解決プロセス中に検証できます。
## 問題解決ステップ
動的プログラミング問題解決プロセスは問題の性質と難易度によって異なりますが、一般的に次のステップに従います:決定の記述、状態の定義、$dp$ テーブルの確立、状態遷移方程式の導出、境界条件の決定など。
問題解決ステップをより具体的に説明するために、古典的な問題「最小経路和」を例として使用します。
!!! question
$n \times m$ の二次元グリッド `grid` が与えられ、グリッドの各セルには負でない整数が含まれ、そのセルのコストを表します。ロボットは左上のセルから始まり、各ステップで下または右にのみ移動でき、右下のセルに到達するまで続けます。左上から右下への最小経路和を返してください。
下の図は例を示しており、与えられたグリッドの最小経路和は $13$ です。
![最小経路和の例データ](dp_solution_pipeline.assets/min_path_sum_example.png)
**第1ステップ各ラウンドの決定を考え、状態を定義し、それにより $dp$ テーブルを得る**
この問題の各ラウンドの決定は、現在のセルから下または右に1ステップ移動することです。現在のセルの行と列のインデックスが $[i, j]$ であると仮定すると、下または右に移動した後、インデックスは $[i+1, j]$ または $[i, j+1]$ になります。したがって、状態には2つの変数が含まれるべきです行インデックスと列インデックス、$[i, j]$ と表記されます。
状態 $[i, j]$ は部分問題に対応します:開始点 $[0, 0]$ から $[i, j]$ への最小経路和、$dp[i, j]$ と表記されます。
このようにして、下の図に示す二次元 $dp$ 行列を得ます。そのサイズは入力グリッド $grid$ と同じです。
![状態定義とDPテーブル](dp_solution_pipeline.assets/min_path_sum_solution_state_definition.png)
!!! note
動的プログラミングとバックトラッキングは決定のシーケンスとして記述でき、状態はすべての決定変数から構成されます。問題解決の進行を記述するすべての変数を含むべきで、次の状態を導出するのに十分な情報を含んでいる必要があります。
各状態は部分問題に対応し、すべての部分問題の解を保存するための $dp$ テーブルを定義します。状態の各独立変数は $dp$ テーブルの次元です。本質的に、$dp$ テーブルは状態と部分問題の解の間のマッピングです。
**第2ステップ最適部分構造を特定し、状態遷移方程式を導出する**
状態 $[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]
$$
![最適部分構造と状態遷移方程式](dp_solution_pipeline.assets/min_path_sum_solution_state_transition.png)
!!! note
定義された $dp$ テーブルに基づいて、元の問題と部分問題の関係を考え、部分問題の最適解から元の問題の最適解をどのように構築するか、つまり最適部分構造を見つけます。
最適部分構造を特定したら、それを使用して状態遷移方程式を構築できます。
**第3ステップ境界条件と状態遷移順序を決定する**
この問題では、最初の行の状態は左の状態からのみ来ることができ、最初の列の状態は上の状態からのみ来ることができるため、最初の行 $i = 0$ と最初の列 $j = 0$ が境界条件です。
下の図に示すように、各セルは左のセルと上のセルから導出されるため、ループを使用して行列を走査し、外側のループは行を反復し、内側のループは列を反復します。
![境界条件と状態遷移順序](dp_solution_pipeline.assets/min_path_sum_solution_initial_state.png)
!!! note
境界条件は動的プログラミングで $dp$ テーブルを初期化するために使用され、探索では枝刈りに使用されます。
状態遷移順序の核心は、現在の問題の解を計算するとき、それが依存するすべての小さな部分問題が既に正しく計算されていることを確保することです。
上記の分析に基づいて、動的プログラミングコードを直接書くことができます。しかし、部分問題の分解はトップダウンアプローチであるため、「力任せ探索 → メモ化探索 → 動的プログラミング」の順序で実装することが習慣的な思考により適合します。
### 方法1力任せ探索
状態 $[i, j]$ から探索を開始し、それを常により小さな状態 $[i-1, j]$ と $[i, j-1]$ に分解します。再帰関数には以下の要素が含まれます。
- **再帰パラメータ**:状態 $[i, j]$。
- **戻り値**$[0, 0]$ から $[i, j]$ への最小経路和 $dp[i, j]$。
- **終了条件**$i = 0$ かつ $j = 0$ のとき、コスト $grid[0, 0]$ を返す。
- **枝刈り**$i < 0$ または $j < 0$ でインデックスが範囲外のとき、コスト $+\infty$ を返し、実行不可能性を表す。
実装コードは以下の通りです:
```src
[file]{min_path_sum}-[class]{}-[func]{min_path_sum_dfs}
```
下の図は $dp[2, 1]$ を根とする再帰木を示しており、いくつかの重複する部分問題を含み、その数はグリッド `grid` のサイズが増加すると急激に増加します。
本質的に、重複する部分問題の理由は:**左上隅から特定のセルに到達する複数のパスが存在する**ことです。
![力任せ探索の再帰木](dp_solution_pipeline.assets/min_path_sum_dfs.png)
各状態には下と右の2つの選択があるため、左上隅から右下隅までの総ステップ数は $m + n - 2$ で、最悪時間計算量は $O(2^{m + n})$ です。この計算方法はグリッドエッジ近くの状況を考慮していないことに注意してください。ネットワークエッジに到達したとき、選択肢が1つしか残らないため、実際のパス数はより少なくなります。
### 方法2メモ化探索
グリッド `grid` と同じサイズのメモリスト `mem` を導入し、様々な部分問題の解を記録し、重複する部分問題を枝刈りします:
```src
[file]{min_path_sum}-[class]{}-[func]{min_path_sum_dfs_mem}
```
下の図に示すように、メモ化を導入した後、すべての部分問題の解は一度だけ計算される必要があるため、時間計算量は状態の総数、つまりグリッドサイズ $O(nm)$ に依存します。
![メモ化探索の再帰木](dp_solution_pipeline.assets/min_path_sum_dfs_mem.png)
### 方法3動的プログラミング
動的プログラミング解法を反復的に実装します。コードは以下の通りです:
```src
[file]{min_path_sum}-[class]{}-[func]{min_path_sum_dp}
```
下の図は最小経路和の状態遷移プロセスを示し、グリッド全体を走査するため、**時間計算量は $O(nm)$** です。
配列 `dp` のサイズは $n \times m$ であるため、**空間計算量は $O(nm)$** です。
=== "<1>"
![最小経路和の動的プログラミングプロセス](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)
=== "<3>"
![min_path_sum_dp_step3](dp_solution_pipeline.assets/min_path_sum_dp_step3.png)
=== "<4>"
![min_path_sum_dp_step4](dp_solution_pipeline.assets/min_path_sum_dp_step4.png)
=== "<5>"
![min_path_sum_dp_step5](dp_solution_pipeline.assets/min_path_sum_dp_step5.png)
=== "<6>"
![min_path_sum_dp_step6](dp_solution_pipeline.assets/min_path_sum_dp_step6.png)
=== "<7>"
![min_path_sum_dp_step7](dp_solution_pipeline.assets/min_path_sum_dp_step7.png)
=== "<8>"
![min_path_sum_dp_step8](dp_solution_pipeline.assets/min_path_sum_dp_step8.png)
=== "<9>"
![min_path_sum_dp_step9](dp_solution_pipeline.assets/min_path_sum_dp_step9.png)
=== "<10>"
![min_path_sum_dp_step10](dp_solution_pipeline.assets/min_path_sum_dp_step10.png)
=== "<11>"
![min_path_sum_dp_step11](dp_solution_pipeline.assets/min_path_sum_dp_step11.png)
=== "<12>"
![min_path_sum_dp_step12](dp_solution_pipeline.assets/min_path_sum_dp_step12.png)
### 空間最適化
各セルは左と上のセルのみに関連するため、単一行配列を使用して $dp$ テーブルを実装できます。
配列 `dp` は1行の状態のみを表現できるため、最初の列の状態を事前に初期化できず、各行を走査するときに更新することに注意してください
```src
[file]{min_path_sum}-[class]{}-[func]{min_path_sum_dp_comp}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,129 @@
# 編集距離問題
編集距離は、レーベンシュタイン距離とも呼ばれ、一つの文字列を別の文字列に変換するために必要な最小修正回数を指し、情報検索や自然言語処理で2つのシーケンス間の類似度を測定するためによく使用されます。
!!! question
2つの文字列 $s$ と $t$ が与えられたとき、$s$ を $t$ に変換するために必要な最小編集回数を返してください。
文字列に対して3種類の編集を実行できます文字の挿入、文字の削除、または文字を他の任意の文字に置換。
下の図に示すように、`kitten``sitting` に変換するには3回の編集が必要で、2回の置換と1回の挿入を含みます。`hello``algo` に変換するには3ステップが必要で、2回の置換と1回の削除を含みます。
![編集距離の例データ](edit_distance_problem.assets/edit_distance_example.png)
**編集距離問題は決定木モデルで自然に説明できます**。文字列は木のードに対応し、1ラウンドの決定編集操作は木のエッジに対応します。
下の図に示すように、操作に制限がない場合、各ードは多くのエッジを導出でき、それぞれが1つの操作に対応するため、`hello``algo` に変換する可能な経路は多数あります。
決定木の観点から、この問題の目標は、ノード `hello` とノード `algo` の間の最短経路を見つけることです。
![決定木モデルに基づいて表現された編集距離問題](edit_distance_problem.assets/edit_distance_decision_tree.png)
### 動的プログラミングアプローチ
**ステップ1各ラウンドの決定を考え、状態を定義し、それにより $dp$ テーブルを得る**
各ラウンドの決定は、文字列 $s$ に対して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つの編集挿入、削除、置換を実行して、2つの文字列の末尾文字を一致させ、それらをスキップしてより小規模な問題を考慮できるようにする必要があります。
したがって、文字列 $s$ での各ラウンドの決定(編集操作)は、$s$ と $t$ でマッチされる残りの文字を変更します。したがって、状態は $s$ と $t$ で現在考慮されている $i$ 番目と $j$ 番目の文字であり、$[i, j]$ と表記されます。
状態 $[i, j]$ は部分問題に対応します:**$s$ の最初の $i$ 文字を $t$ の最初の $j$ 文字に変更するために必要な最小編集回数**。
これから、サイズ $(i+1) \times (j+1)$ の二次元 $dp$ テーブルを得ます。
**ステップ2最適部分構造を特定し、状態遷移方程式を導出する**
部分問題 $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]$ です。
![編集距離の状態遷移](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] = \min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1
$$
注意してください。**$s[i-1]$ と $t[j-1]$ が同じ場合、現在の文字に対して編集は必要ありません**。この場合、状態遷移方程式は:
$$
dp[i, j] = dp[i-1, j-1]
$$
**ステップ3境界条件と状態遷移の順序を決定する**
両方の文字列が空の場合、編集回数は $0$ です。つまり、$dp[0, 0] = 0$ です。$s$ が空で $t$ が空でない場合、最小編集回数は $t$ の長さに等しく、つまり最初の行 $dp[0, j] = j$ です。$s$ が空でなく $t$ が空の場合、最小編集回数は $s$ の長さに等しく、つまり最初の列 $dp[i, 0] = i$ です。
状態遷移方程式を観察すると、$dp[i, j]$ の解決は左、上、左上の解に依存するため、二重ループを使用して正しい順序で $dp$ テーブル全体を走査できます。
### コード実装
```src
[file]{edit_distance}-[class]{}-[func]{edit_distance_dp}
```
下の図に示すように、編集距離問題の状態遷移プロセスはナップサック問題と非常に似ており、二次元グリッドを埋めることと見なすことができます。
=== "<1>"
![編集距離の動的プログラミングプロセス](edit_distance_problem.assets/edit_distance_dp_step1.png)
=== "<2>"
![edit_distance_dp_step2](edit_distance_problem.assets/edit_distance_dp_step2.png)
=== "<3>"
![edit_distance_dp_step3](edit_distance_problem.assets/edit_distance_dp_step3.png)
=== "<4>"
![edit_distance_dp_step4](edit_distance_problem.assets/edit_distance_dp_step4.png)
=== "<5>"
![edit_distance_dp_step5](edit_distance_problem.assets/edit_distance_dp_step5.png)
=== "<6>"
![edit_distance_dp_step6](edit_distance_problem.assets/edit_distance_dp_step6.png)
=== "<7>"
![edit_distance_dp_step7](edit_distance_problem.assets/edit_distance_dp_step7.png)
=== "<8>"
![edit_distance_dp_step8](edit_distance_problem.assets/edit_distance_dp_step8.png)
=== "<9>"
![edit_distance_dp_step9](edit_distance_problem.assets/edit_distance_dp_step9.png)
=== "<10>"
![edit_distance_dp_step10](edit_distance_problem.assets/edit_distance_dp_step10.png)
=== "<11>"
![edit_distance_dp_step11](edit_distance_problem.assets/edit_distance_dp_step11.png)
=== "<12>"
![edit_distance_dp_step12](edit_distance_problem.assets/edit_distance_dp_step12.png)
=== "<13>"
![edit_distance_dp_step13](edit_distance_problem.assets/edit_distance_dp_step13.png)
=== "<14>"
![edit_distance_dp_step14](edit_distance_problem.assets/edit_distance_dp_step14.png)
=== "<15>"
![edit_distance_dp_step15](edit_distance_problem.assets/edit_distance_dp_step15.png)
### 空間最適化
$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]$ からの解を一時的に保存し、左と上の解のみを考慮すればよくなります。この状況は無制限ナップサック問題と似ており、直接走査が可能です。コードは以下の通りです:
```src
[file]{edit_distance}-[class]{}-[func]{edit_distance_dp_comp}
```

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,110 @@
# 動的プログラミングの紹介
<u>動的プログラミング</u>は重要なアルゴリズムパラダイムであり、問題を一連の小さな部分問題に分解し、これらの部分問題の解を保存することで冗長な計算を避け、時間効率を大幅に向上させます。
このセクションでは、古典的な問題から始めて、まず力任せの探索法による解法を提示し、重複する部分問題を特定してから、より効率的な動的プログラミング解法を段階的に導出します。
!!! question "階段登り"
$n$ 段の階段があり、一度に $1$ 段または $2$ 段上ることができます。頂上に到達する方法は何通りありますか?
下の図に示すように、$3$ 段の階段の頂上に到達する方法は $3$ 通りあります。
![3段目に到達する方法の数](intro_to_dynamic_programming.assets/climbing_stairs_example.png)
この問題は**バックトラッキングを用いてすべての可能性を網羅**することで方法の数を計算することを目的としています。具体的には、階段登りの問題を多段階選択プロセスとして考えます:地面から始めて、毎回 $1$ 段または $2$ 段上るかを選択し、階段の頂上に到達したら方法の数をカウントし、頂上を超えた場合はプルーニング(枝刈り)を行います。コードは以下の通りです:
```src
[file]{climbing_stairs_backtrack}-[class]{}-[func]{climbing_stairs_backtrack}
```
## 方法1力任せ探索
バックトラッキングアルゴリズムは問題を明示的に部分問題に分解しません。代わりに、問題を一連の決定ステップとして扱い、試行と枝刈りを通じてすべての可能性を探索します。
この問題を分解アプローチを使って分析できます。$dp[i]$ を $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$ 段目からしか到達できません。
これにより重要な結論が得られます:**$i-1$ 段目に到達する方法の数に $i-2$ 段目に到達する方法の数を加えたものが、$i$ 段目に到達する方法の数に等しい**。式は以下の通りです:
$$
dp[i] = dp[i-1] + dp[i-2]
$$
これは、階段登り問題において部分問題間に再帰関係があることを意味し、**元の問題の解は部分問題の解から構築できます**。下の図はこの再帰関係を示しています。
![解の数の再帰関係](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$ 通りあることを表します。
以下のコードを観察すると、標準的なバックトラッキングコードと同様に深さ優先探索に属しますが、より簡潔です:
```src
[file]{climbing_stairs_dfs}-[class]{}-[func]{climbing_stairs_dfs}
```
下の図は力任せ探索によって形成される再帰木を示しています。問題 $dp[n]$ について、その再帰木の深さは $n$ で、時間計算量は $O(2^n)$ です。この指数的増加により、$n$ が大きいとプログラムの実行がはるかに遅くなり、長い待機時間が生じます。
![階段登りの再帰木](intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png)
上の図を観察すると、**指数時間計算量は「重複する部分問題」によって引き起こされる**ことがわかります。例えば、$dp[9]$ は $dp[8]$ と $dp[7]$ に分解され、$dp[8]$ はさらに $dp[7]$ と $dp[6]$ に分解され、両方とも部分問題 $dp[7]$ を含んでいます。
このように、部分問題にはさらに小さな重複する部分問題が含まれ、これは無限に続きます。計算リソースの大部分がこれらの重複する部分問題に浪費されています。
## 方法2メモ化探索
アルゴリズムの効率を向上させるため、**すべての重複する部分問題を一度だけ計算したい**と考えます。この目的のため、各部分問題の解を記録する配列 `mem` を宣言し、探索プロセス中に重複する部分問題を枝刈りします。
1. $dp[i]$ が初めて計算されるとき、後で使用するために `mem[i]` に記録します。
2. $dp[i]$ を再度計算する必要があるとき、`mem[i]` から直接結果を取得でき、その部分問題の冗長な計算を避けられます。
コードは以下の通りです:
```src
[file]{climbing_stairs_dfs_mem}-[class]{}-[func]{climbing_stairs_dfs_mem}
```
下の図を観察すると、**メモ化後、すべての重複する部分問題は一度だけ計算される必要があり、時間計算量を $O(n)$ に最適化**します。これは大幅な改善です。
![メモ化探索による再帰木](intro_to_dynamic_programming.assets/climbing_stairs_dfs_memo_tree.png)
## 方法3動的プログラミング
**メモ化探索は「トップダウン」方式**です:元の問題(根ノード)から始めて、より大きな部分問題をより小さなものに再帰的に分解し、最小の既知の部分問題(葉ノード)の解に到達するまで続けます。その後、バックトラッキングにより部分問題の解を収集し、元の問題の解を構築します。
一方、**動的プログラミングは「ボトムアップ」方式**です:最小の部分問題の解から始めて、元の問題が解決されるまで、より大きな部分問題の解を反復的に構築します。
動的プログラミングはバックトラッキングを必要としないため、ループを使った反復のみが必要で、再帰は不要です。以下のコードでは、配列 `dp` を初期化して部分問題の解を保存し、メモ化探索の配列 `mem` と同じ記録機能を果たします:
```src
[file]{climbing_stairs_dp}-[class]{}-[func]{climbing_stairs_dp}
```
下の図は上記コードの実行プロセスをシミュレートしています。
![階段登りの動的プログラミングプロセス](intro_to_dynamic_programming.assets/climbing_stairs_dp.png)
バックトラッキングアルゴリズムと同様に、動的プログラミングも「状態」の概念を使用して問題解決の特定の段階を表現し、各状態は部分問題とその局所最適解に対応します。例えば、階段登り問題の状態は現在のステップ番号 $i$ として定義されます。
上記の内容に基づいて、動的プログラミングでよく使用される用語をまとめることができます。
- 配列 `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つの変数を使って反復的に進めることができます。コードは以下の通りです
```src
[file]{climbing_stairs_dp}-[class]{}-[func]{climbing_stairs_dp_comp}
```
上記のコードを観察すると、配列 `dp` が占有していた空間が削除されるため、空間計算量は $O(n)$ から $O(1)$ に削減されます。
多くの動的プログラミング問題では、現在の状態は限られた数の前の状態のみに依存するため、必要な状態のみを保持し、「次元削減」によってメモリ空間を節約できます。**この空間最適化技術は「ローリング変数」または「ローリング配列」として知られています**。

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,168 @@
# 0-1ナップサック問題
ナップサック問題は動的プログラミングの優れた入門問題であり、動的プログラミングで最も一般的な問題タイプです。0-1ナップサック問題、無制限ナップサック問題、複数ナップサック問題など、多くの変種があります。
このセクションでは、まず最も一般的な0-1ナップサック問題を解決します。
!!! question
$n$ 個のアイテムが与えられ、$i$ 番目のアイテムの重量は $wgt[i-1]$ で値は $val[i-1]$ です。容量が $cap$ のナップサックがあります。各アイテムは1回のみ選択できます。容量制限下でナップサックに入れることができるアイテムの最大値は何ですか
下の図を観察すると、アイテム番号 $i$ は1から数え始め、配列インデックスは0から始まるため、アイテム $i$ の重量は $wgt[i-1]$ に対応し、値は $val[i-1]$ に対応します。
![0-1ナップサックの例データ](knapsack_problem.assets/knapsack_example.png)
0-1ナップサック問題を $n$ ラウンドの決定から構成されるプロセスとして考えることができます。各アイテムについて入れない、または入れるという2つの決定があり、したがって問題は決定木モデルに適合します。
この問題の目的は「限られた容量の下でナップサックに入れることができるアイテムの値を最大化する」ことであり、動的プログラミング問題である可能性が高いです。
**第1ステップ各ラウンドの決定を考え、状態を定義し、それにより $dp$ テーブルを得る**
各アイテムについて、ナップサックに入れなければ容量は変わらず、入れれば容量は減少します。これから状態定義を得ることができます:現在のアイテム番号 $i$ とナップサック容量 $c$、$[i, c]$ と表記されます。
状態 $[i, c]$ は部分問題に対応します:**容量 $c$ のナップサックでの最初の $i$ 個のアイテムの最大値**、$dp[i, c]$ と表記されます。
探している解は $dp[n, cap]$ であるため、サイズ $(n+1) \times (cap+1)$ の二次元 $dp$ テーブルが必要です。
**第2ステップ最適部分構造を特定し、状態遷移方程式を導出する**
アイテム $i$ の決定を行った後、残るのは最初の $i-1$ 個のアイテムの決定の部分問題であり、これは2つのケースに分けることができます。
- **アイテム $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] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
$$
現在のアイテムの重量 $wgt[i - 1]$ が残りのナップサック容量 $c$ を超える場合、唯一の選択肢はナップサックに入れないことであることに注意することが重要です。
**第3ステップ境界条件と状態遷移の順序を決定する**
アイテムがない場合またはナップサック容量が $0$ の場合、最大値は $0$ です。つまり、最初の列 $dp[i, 0]$ と最初の行 $dp[0, c]$ はどちらも $0$ に等しいです。
現在の状態 $[i, c]$ は直接上の状態 $[i-1, c]$ と左上の状態 $[i-1, c-wgt[i-1]]$ から遷移するため、2層のループを通じて $dp$ テーブル全体を順序通りに走査します。
上記の分析に従って、次に力任せ探索、メモ化探索、動的プログラミングの順序で解法を実装します。
### 方法1力任せ探索
探索コードには以下の要素が含まれます。
- **再帰パラメータ**:状態 $[i, c]$。
- **戻り値**:部分問題 $dp[i, c]$ の解。
- **終了条件**:アイテム番号が範囲外 $i = 0$ またはナップサックの残り容量が $0$ のとき、再帰を終了し値 $0$ を返す。
- **枝刈り**:現在のアイテムの重量がナップサックの残り容量を超える場合、唯一の選択肢はナップサックに入れないことです。
```src
[file]{knapsack}-[class]{}-[func]{knapsack_dfs}
```
下の図に示すように、各アイテムは選択しないと選択するという2つの探索分岐を生成するため、時間計算量は $O(2^n)$ です。
再帰木を観察すると、$dp[1, 10]$ などの重複する部分問題があることが容易にわかります。アイテムが多く、ナップサック容量が大きい場合、特に同じ重量のアイテムが多い場合、重複する部分問題の数は大幅に増加します。
![0-1ナップサック問題の力任せ探索再帰木](knapsack_problem.assets/knapsack_dfs.png)
### 方法2メモ化探索
重複する部分問題が一度だけ計算されることを確保するために、部分問題の解を記録するメモ化リスト `mem` を使用します。ここで `mem[i][c]` は $dp[i, c]$ に対応します。
メモ化を導入した後、**時間計算量は部分問題の数に依存**し、$O(n \times cap)$ になります。実装コードは以下の通りです:
```src
[file]{knapsack}-[class]{}-[func]{knapsack_dfs_mem}
```
下の図はメモ化探索で枝刈りされる探索分岐を示しています。
![0-1ナップサック問題のメモ化探索再帰木](knapsack_problem.assets/knapsack_dfs_mem.png)
### 方法3動的プログラミング
動的プログラミングは本質的に状態遷移中に $dp$ テーブルを埋めることを含みます。コードは下の図に示されています:
```src
[file]{knapsack}-[class]{}-[func]{knapsack_dp}
```
下の図に示すように、時間計算量と空間計算量の両方が配列 `dp` のサイズ、つまり $O(n \times cap)$ によって決定されます。
=== "<1>"
![0-1ナップサック問題の動的プログラミングプロセス](knapsack_problem.assets/knapsack_dp_step1.png)
=== "<2>"
![knapsack_dp_step2](knapsack_problem.assets/knapsack_dp_step2.png)
=== "<3>"
![knapsack_dp_step3](knapsack_problem.assets/knapsack_dp_step3.png)
=== "<4>"
![knapsack_dp_step4](knapsack_problem.assets/knapsack_dp_step4.png)
=== "<5>"
![knapsack_dp_step5](knapsack_problem.assets/knapsack_dp_step5.png)
=== "<6>"
![knapsack_dp_step6](knapsack_problem.assets/knapsack_dp_step6.png)
=== "<7>"
![knapsack_dp_step7](knapsack_problem.assets/knapsack_dp_step7.png)
=== "<8>"
![knapsack_dp_step8](knapsack_problem.assets/knapsack_dp_step8.png)
=== "<9>"
![knapsack_dp_step9](knapsack_problem.assets/knapsack_dp_step9.png)
=== "<10>"
![knapsack_dp_step10](knapsack_problem.assets/knapsack_dp_step10.png)
=== "<11>"
![knapsack_dp_step11](knapsack_problem.assets/knapsack_dp_step11.png)
=== "<12>"
![knapsack_dp_step12](knapsack_problem.assets/knapsack_dp_step12.png)
=== "<13>"
![knapsack_dp_step13](knapsack_problem.assets/knapsack_dp_step13.png)
=== "<14>"
![knapsack_dp_step14](knapsack_problem.assets/knapsack_dp_step14.png)
### 空間最適化
各状態は上の行の状態のみに関連するため、2つの配列を使用してローリング前進させ、空間計算量を $O(n^2)$ から $O(n)$ に削減できます。
さらに考えてみると、1つの配列だけで空間最適化を達成できるでしょうか各状態が直接上のセルまたは左上のセルから遷移することが観察できます。配列が1つしかない場合、$i$ 行目の走査を開始するとき、その配列はまだ $i-1$ 行目の状態を保存しています。
- 通常の順序で走査する場合、$dp[i, j]$ に走査したとき、左上の $dp[i-1, 1]$ $dp[i-1, j-1]$ の値がすでに上書きされている可能性があり、正しい状態遷移結果を得ることができません。
- 逆順で走査する場合、上書き問題はなく、状態遷移を正しく実行できます。
下の図は単一配列での $i = 1$ 行目から $i = 2$ 行目への遷移プロセスを示しています。通常順序走査と逆順走査の違いについて考えてみてください。
=== "<1>"
![0-1ナップサックの空間最適化動的プログラミングプロセス](knapsack_problem.assets/knapsack_dp_comp_step1.png)
=== "<2>"
![knapsack_dp_comp_step2](knapsack_problem.assets/knapsack_dp_comp_step2.png)
=== "<3>"
![knapsack_dp_comp_step3](knapsack_problem.assets/knapsack_dp_comp_step3.png)
=== "<4>"
![knapsack_dp_comp_step4](knapsack_problem.assets/knapsack_dp_comp_step4.png)
=== "<5>"
![knapsack_dp_comp_step5](knapsack_problem.assets/knapsack_dp_comp_step5.png)
=== "<6>"
![knapsack_dp_comp_step6](knapsack_problem.assets/knapsack_dp_comp_step6.png)
コード実装では、配列 `dp` の最初の次元 $i$ を削除し、内側のループを逆走査に変更するだけです:
```src
[file]{knapsack}-[class]{}-[func]{knapsack_dp_comp}
```

View File

@@ -0,0 +1,23 @@
# まとめ
- 動的プログラミングは問題を分解し、部分問題の解を保存することで冗長な計算を避け、計算効率を向上させます。
- 時間を考慮しなければ、すべての動的プログラミング問題はバックトラッキング(力任せ探索)を使用して解決できますが、再帰木には多くの重複する部分問題があり、効率が非常に低くなります。記憶化リストを導入することで、計算されたすべての部分問題の解を保存し、重複する部分問題が一度だけ計算されることを保証できます。
- 記憶化探索はトップダウンの再帰解法であり、動的プログラミングはボトムアップの反復アプローチに対応し、「表を埋める」ことに似ています。現在の状態は特定の局所状態のみに依存するため、dpテーブルの1次元を削除して空間計算量を削減できます。
- 部分問題の分解は汎用的なアルゴリズムアプローチであり、分割統治法、動的プログラミング、バックトラッキングで特徴が異なります。
- 動的プログラミング問題には3つの主要な特徴があります重複する部分問題、最適部分構造、無記憶性。
- 元の問題の最適解がその部分問題の最適解から構築できる場合、最適部分構造を持ちます。
- 無記憶性とは、状態の将来の発展が現在の状態のみに依存し、過去に経験したすべての状態に依存しないことを意味します。多くの組み合わせ最適化問題にはこの特性がなく、動的プログラミングを使用して迅速に解決することはできません。
**ナップサック問題**
- ナップサック問題は最も典型的な動的プログラミング問題の1つで、0-1ナップサック、無制限ナップサック、複数ナップサックなどの変種があります。
- 0-1ナップサックの状態定義は、最初の $i$ 個のアイテムを含む容量 $c$ のナップサックでの最大値です。アイテムをナップサックに入れないまたは入れるという決定に基づいて、最適部分構造を特定し、状態遷移方程式を構築できます。空間最適化では、各状態が直接上と左上の状態に依存するため、左上の状態の上書きを避けるためにリストを逆順で走査する必要があります。
- 無制限ナップサック問題では、各種類のアイテムを選択できる数に制限がないため、アイテムを含める状態遷移は0-1ナップサックと異なります。状態が直接上と左の状態に依存するため、空間最適化では前方走査を含める必要があります。
- コイン交換問題は無制限ナップサック問題の変種で、「最大」値を求めることから「最小」コイン数を求めることに変わり、状態遷移方程式は $\max()$ を $\min()$ に変更する必要があります。ナップサックの容量を「超えない」ことを追求することから、正確に目標金額を求めることに変わり、「目標金額を構成できない」無効解を表すために $amt + 1$ を使用します。
- コイン交換問題IIは「最小コイン数」を求めることから「コインの組み合わせ数」を求めることに変わり、状態遷移方程式を $\min()$ から和算演算子に変更します。
**編集距離問題**
- 編集距離レーベンシュタイン距離は2つの文字列間の類似度を測定し、一つの文字列を別の文字列に変更するために必要な最小編集ステップ数として定義され、編集操作には追加、削除、置換が含まれます。
- 編集距離問題の状態定義は、$s$ の最初の $i$ 文字を $t$ の最初の $j$ 文字に変更するために必要な最小編集ステップ数です。$s[i] \ne t[j]$ の場合、追加、削除、置換の3つの決定があり、それぞれに対応する残余部分問題があります。これから最適部分構造を特定し、状態遷移方程式を構築できます。$s[i] = t[j]$ の場合、現在の文字の編集は必要ありません。
- 編集距離では、状態が直接上、左、左上の状態に依存します。したがって、空間最適化後、前方走査も逆走査も正しく状態遷移を実行できません。これに対処するため、変数を使用して左上の状態を一時的に保存し、無制限ナップサック問題の状況と同等にし、空間最適化後に前方走査を可能にします。

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,207 @@
# 無制限ナップサック問題
このセクションでは、まず別の一般的なナップサック問題である無制限ナップサックを解決し、次にその特殊ケースであるコイン交換問題を探索します。
## 無制限ナップサック問題
!!! question
$n$ 個のアイテムが与えられ、$i$ 番目のアイテムの重量は $wgt[i-1]$ で値は $val[i-1]$ です。容量が $cap$ のバックパックがあります。**各アイテムは複数回選択できます**。容量を超えることなくバックパックに入れることができるアイテムの最大値は何ですか?以下の例を参照してください。
![無制限ナップサック問題の例データ](unbounded_knapsack_problem.assets/unbounded_knapsack_example.png)
### 動的プログラミングアプローチ
無制限ナップサック問題は0-1ナップサック問題と非常に似ており、**唯一の違いはアイテムを選択できる回数に制限がないことです**。
- 0-1ナップサック問題では、各アイテムは1つしかないため、アイテム $i$ をバックパックに入れた後は、前の $i-1$ 個のアイテムからのみ選択できます。
- 無制限ナップサック問題では、各アイテムの数量は無制限であるため、アイテム $i$ をバックパックに入れた後も、**前の $i$ 個のアイテムから引き続き選択できます**。
無制限ナップサック問題のルールの下で、状態 $[i, c]$ は2つの方法で変化できます。
- **アイテム $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])
$$
### コード実装
2つの問題のコードを比較すると、状態遷移が $i-1$ から $i$ に変わり、残りは完全に同一です:
```src
[file]{unbounded_knapsack}-[class]{}-[func]{unbounded_knapsack_dp}
```
### 空間最適化
現在の状態は左と上の状態から来るため、**空間最適化解法は $dp$ テーブルの各行に対して前方走査を実行する必要があります**。
この走査順序は0-1ナップサックの場合とは逆です。違いを理解するために下の図を参照してください。
=== "<1>"
![空間最適化後の無制限ナップサック問題の動的プログラミングプロセス](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)
=== "<3>"
![unbounded_knapsack_dp_comp_step3](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step3.png)
=== "<4>"
![unbounded_knapsack_dp_comp_step4](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step4.png)
=== "<5>"
![unbounded_knapsack_dp_comp_step5](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step5.png)
=== "<6>"
![unbounded_knapsack_dp_comp_step6](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step6.png)
コード実装は非常に簡単で、配列 `dp` の最初の次元を削除するだけです:
```src
[file]{unbounded_knapsack}-[class]{}-[func]{unbounded_knapsack_dp_comp}
```
## コイン交換問題
ナップサック問題は動的プログラミング問題の大きなクラスの代表であり、コイン交換問題など多くの変種があります。
!!! question
$n$ 種類のコインが与えられ、$i$ 番目の種類のコインの額面は $coins[i - 1]$ で、目標金額は $amt$ です。**各種類のコインは複数回選択できます**。目標金額を構成するのに必要な最小コイン数は何ですか?目標金額を構成できない場合は $-1$ を返してください。以下の例を参照してください。
![コイン交換問題の例データ](unbounded_knapsack_problem.assets/coin_change_example.png)
### 動的プログラミングアプローチ
**コイン交換は無制限ナップサック問題の特殊ケースと見なすことができ**、以下の類似点と相違点を共有しています。
- 2つの問題は互いに変換できます「アイテム」は「コイン」に対応し、「アイテムの重量」は「コインの額面」に対応し、「バックパックの容量」は「目標金額」に対応します。
- 最適化目標は逆です:無制限ナップサック問題はアイテムの値を最大化することを目的とし、コイン交換問題はコインの数を最小化することを目的とします。
- 無制限ナップサック問題はバックパック容量を「超えない」解を求め、コイン交換は目標金額を「正確に」構成する解を求めます。
**第1ステップ各ラウンドの意思決定を考え、状態を定義し、それにより $dp$ テーブルを導出する**
状態 $[i, a]$ は部分問題に対応します:**最初の $i$ 種類のコインを使用して金額 $a$ を構成できる最小コイン数**、$dp[i, a]$ と表記されます。
二次元 $dp$ テーブルのサイズは $(n+1) \times (amt+1)$ です。
**第2ステップ最適部分構造を特定し、状態遷移方程式を導出する**
この問題は状態遷移方程式の2つの側面で無制限ナップサック問題と異なります。
- この問題は最小値を求めるため、演算子 $\max()$ を $\min()$ に変更する必要があります。
- 最適化はコインの数に焦点を当てているため、コインが選択されたときに単純に $+1$ を追加します。
$$
dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1)
$$
**第3ステップ境界条件と状態遷移順序を定義する**
目標金額が $0$ の場合、それを構成するのに必要な最小コイン数は $0$ であるため、最初の列のすべての $dp[i, 0]$ は $0$ です。
コインがない場合、**任意の金額 >0 を構成することは不可能**であり、これは無効な解です。状態遷移方程式の $\min()$ 関数が無効な解を認識してフィルタリングできるように、$+\infty$ を使用してそれらを表現することを検討し、つまり最初の行のすべての $dp[0, a]$ を $+\infty$ に設定します。
### コード実装
ほとんどのプログラミング言語は $+\infty$ 変数を提供しておらず、整数 `int` の最大値のみを代替として使用できます。これによりオーバーフローが発生する可能性があります:状態遷移方程式の $+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)
=== "<2>"
![coin_change_dp_step2](unbounded_knapsack_problem.assets/coin_change_dp_step2.png)
=== "<3>"
![coin_change_dp_step3](unbounded_knapsack_problem.assets/coin_change_dp_step3.png)
=== "<4>"
![coin_change_dp_step4](unbounded_knapsack_problem.assets/coin_change_dp_step4.png)
=== "<5>"
![coin_change_dp_step5](unbounded_knapsack_problem.assets/coin_change_dp_step5.png)
=== "<6>"
![coin_change_dp_step6](unbounded_knapsack_problem.assets/coin_change_dp_step6.png)
=== "<7>"
![coin_change_dp_step7](unbounded_knapsack_problem.assets/coin_change_dp_step7.png)
=== "<8>"
![coin_change_dp_step8](unbounded_knapsack_problem.assets/coin_change_dp_step8.png)
=== "<9>"
![coin_change_dp_step9](unbounded_knapsack_problem.assets/coin_change_dp_step9.png)
=== "<10>"
![coin_change_dp_step10](unbounded_knapsack_problem.assets/coin_change_dp_step10.png)
=== "<11>"
![coin_change_dp_step11](unbounded_knapsack_problem.assets/coin_change_dp_step11.png)
=== "<12>"
![coin_change_dp_step12](unbounded_knapsack_problem.assets/coin_change_dp_step12.png)
=== "<13>"
![coin_change_dp_step13](unbounded_knapsack_problem.assets/coin_change_dp_step13.png)
=== "<14>"
![coin_change_dp_step14](unbounded_knapsack_problem.assets/coin_change_dp_step14.png)
=== "<15>"
![coin_change_dp_step15](unbounded_knapsack_problem.assets/coin_change_dp_step15.png)
### 空間最適化
コイン交換問題の空間最適化は無制限ナップサック問題と同じ方法で処理されます:
```src
[file]{coin_change}-[class]{}-[func]{coin_change_dp_comp}
```
## コイン交換問題II
!!! question
$n$ 種類のコインが与えられ、$i$ 番目の種類のコインの額面は $coins[i - 1]$ で、目標金額は $amt$ です。各種類のコインは複数回選択でき、**目標金額を構成できるコインの組み合わせは何通りありますか**。以下の例を参照してください。
![コイン交換問題IIの例データ](unbounded_knapsack_problem.assets/coin_change_ii_example.png)
### 動的プログラミングアプローチ
前の問題と比較して、この問題の目標は組み合わせの数を決定することであるため、部分問題は次のようになります:**最初の $i$ 種類のコインを使用して金額 $a$ を構成できる組み合わせの数**。$dp$ テーブルはサイズ $(n+1) \times (amt + 1)$ の二次元行列のまま残ります。
現在の状態の組み合わせ数は、現在のコインを選択しない組み合わせと現在のコインを選択する組み合わせの合計です。状態遷移方程式は:
$$
dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]]
$$
目標金額が $0$ の場合、目標金額を構成するのにコインは必要ないため、最初の列のすべての $dp[i, 0]$ は $1$ に初期化されるべきです。コインがない場合、任意の金額 >0 を構成することは不可能であるため、最初の行のすべての $dp[0, a]$ は $0$ に設定されるべきです。
### コード実装
```src
[file]{coin_change_ii}-[class]{}-[func]{coin_change_ii_dp}
```
### 空間最適化
空間最適化アプローチは同じで、コインの次元を削除するだけです:
```src
[file]{coin_change_ii}-[class]{}-[func]{coin_change_ii_dp_comp}
```