動的計画法の問題の特徴
私たちは、動的プログラミングが問題を部分問題に分解することでどのように問題を解決するかを学びました。実際、部分問題の分解は一般的なアルゴリズムのアイデアであり、分割統治、動的プログラミング、およびバックトラッキングにおいて異なる重点が置かれています。
- 分割統治アルゴリズムは、最小のサブ問題に到達するまで元の問題を複数の独立したサブ問題に再帰的に分割し、バックトラッキングでサブ問題の解をマージして、最終的に元の問題の解を取得します。
- 動的計画法も問題を再帰的に分解しますが、分割統治アルゴリズムとの主な違いは、動的計画法の部分問題が相互依存しており、分解プロセス中に多くの重複する部分問題が現れることです。
- バックトラッキング アルゴリズムは、試行と回帰で考えられるすべての解決策を使い果たし、枝刈りによって不要な検索分岐を回避します。元の問題の解決策は一連の決定ステップで構成されており、各決定ステップの前のサブシーケンスを部分問題とみなすことができます。
実際、動的計画法は、最適化問題を解決するためによく使用されます。最適化問題には、重複する部分問題が含まれるだけでなく、最適な部分構造と余効がないという 2 つの特徴もあります。
最適な下部構造
最適な下部構造の概念を実証するのにより適したものにするために、階段を登る問題をわずかに変更します。
!!! 質問「階段を上るのに最も安いコスト」
给定一个楼梯,你每步可以上 $1$ 阶或者 $2$ 阶,每一阶楼梯上都贴有一个非负整数,表示你在该台阶所需要付出的代价。给定一个非负整数数组 $cost$ ,其中 $cost[i]$ 表示在第 $i$ 个台阶需要付出的代价,$cost[0]$ 为地面起始点。请计算最少需要付出多少代价才能到达顶部?
下図のように、1 1の場合1、2、2 __2、3、3 __レベル3のコストはそれぞれ1 11、10 10_10、1 1_1、次に地面から 3 番目まで登ります3レベル3の最小コストは2 2です2。
设dp [ i ] dp[i]d p [ i ] はii 番目に登ることを意味しますレベルiiのコストにより、レベルiで支払われた累積価格次数i はi − 1 i - 1からのみ可能です私−1次またはi − 2 i − 2私−2次が来るのでdp[i]dp[i]d p [ i ]は、 dp [ i − 1 ] + コスト [ i ] dp [i - 1] + コスト [i]にのみ等しくなります。d p [ i−1 ]+コスト[ i ]またはdp [ i − 2 ] + コスト [ i ] dp[i - 2] + コスト[i ]d p [ i−2 ]+コスト[ i ]。 _ コストを最小限に抑えるには、次の 2 つのうちの小さい方を選択する必要があります。
dp [ i ] = 最小値 ( dp [ i − 1 ] , dp [ i − 2 ] ) + コスト [ i ] dp[i] = \min(dp[i-1], dp[i-2]) +コスト[i]d p [ i ]=min ( d p [ i−1 ] 、d p [ i−2 ])+コスト[ i ] _
これは、最適な部分構造の意味につながります。つまり、元の問題に対する最適な解決策は、部分的な問題に対する最適な解決策から構築されます。
この問題には明らかに最適な部分構造があります。2 つの部分問題dp [ i − 1 ] dp[i-1]を最適に解きます。d p [ i−1 ]和dp [ i − 2 ] dp[i-2]d p [ i−2 ] を使用し、それを使用して元の問題dp[i]dp[i]d p [ i ]の最適解。
では、前のセクションの階段を登る問題には最適な下部構造があるのでしょうか? 解の数を解くことが目的で、一見計数問題のように見えますが、別の言い方をすれば「解の数を最大まで解く」ことになります。予想外にも、質問は修正前後で同等であるにもかかわらず、最適な部分構造が出現したことがわかりました: No. nnn次の解の最大数はn − 1 n-1n−1次と n− 2 n-2n−次数2の解の最大数の合計したがって、最適部分構造の説明方法は比較的柔軟であり、問題ごとに異なる意味を持ちます。
状態遷移方程式と初期状態によると、dp[1] = コスト[1] dp[1] = コスト[1]d p [ 1 ]=Cost [ 1 ]とdp [ 2 ] = コスト [ 2 ] dp[2] = コスト [2 ]d p [ 2 ]=cos t [ 2 ]、動的プログラミング コードを取得できます。
=== 「パイソン」
```python title="min_cost_climbing_stairs_dp.py"
[class]{}-[func]{min_cost_climbing_stairs_dp}
```
=== 「C++」
```cpp title="min_cost_climbing_stairs_dp.cpp"
[class]{}-[func]{minCostClimbingStairsDP}
```
=== 「ジャワ」
```java title="min_cost_climbing_stairs_dp.java"
[class]{min_cost_climbing_stairs_dp}-[func]{minCostClimbingStairsDP}
```
=== 「C#」
```csharp title="min_cost_climbing_stairs_dp.cs"
[class]{min_cost_climbing_stairs_dp}-[func]{minCostClimbingStairsDP}
```
=== 「行きます」
```go title="min_cost_climbing_stairs_dp.go"
[class]{}-[func]{minCostClimbingStairsDP}
```
=== 「スウィフト」
```swift title="min_cost_climbing_stairs_dp.swift"
[class]{}-[func]{minCostClimbingStairsDP}
```
=== 「JS」
```javascript title="min_cost_climbing_stairs_dp.js"
[class]{}-[func]{minCostClimbingStairsDP}
```
=== 「TS」
```typescript title="min_cost_climbing_stairs_dp.ts"
[class]{}-[func]{minCostClimbingStairsDP}
```
=== 「ダーツ」
```dart title="min_cost_climbing_stairs_dp.dart"
[class]{}-[func]{minCostClimbingStairsDP}
```
=== 「錆」
```rust title="min_cost_climbing_stairs_dp.rs"
[class]{}-[func]{min_cost_climbing_stairs_dp}
```
=== 「C」
```c title="min_cost_climbing_stairs_dp.c"
[class]{}-[func]{minCostClimbingStairsDP}
```
=== 「ジグ」
```zig title="min_cost_climbing_stairs_dp.zig"
[class]{}-[func]{minCostClimbingStairsDP}
```
以下の図は、上記のコードの動的プログラミング プロセスを示しています。
この問題は、空間の複雑さがO ( n ) O (n)から変化するように、1 次元を 0 次元に圧縮して空間を最適化することもできます。O(n) 降低至 O ( 1 ) O(1) お( 1 )。
=== 「パイソン」
```python title="min_cost_climbing_stairs_dp.py"
[class]{}-[func]{min_cost_climbing_stairs_dp_comp}
```
=== 「C++」
```cpp title="min_cost_climbing_stairs_dp.cpp"
[class]{}-[func]{minCostClimbingStairsDPComp}
```
=== 「ジャワ」
```java title="min_cost_climbing_stairs_dp.java"
[class]{min_cost_climbing_stairs_dp}-[func]{minCostClimbingStairsDPComp}
```
=== 「C#」
```csharp title="min_cost_climbing_stairs_dp.cs"
[class]{min_cost_climbing_stairs_dp}-[func]{minCostClimbingStairsDPComp}
```
=== 「行きます」
```go title="min_cost_climbing_stairs_dp.go"
[class]{}-[func]{minCostClimbingStairsDPComp}
```
=== 「スウィフト」
```swift title="min_cost_climbing_stairs_dp.swift"
[class]{}-[func]{minCostClimbingStairsDPComp}
```
=== 「JS」
```javascript title="min_cost_climbing_stairs_dp.js"
[class]{}-[func]{minCostClimbingStairsDPComp}
```
=== 「TS」
```typescript title="min_cost_climbing_stairs_dp.ts"
[class]{}-[func]{minCostClimbingStairsDPComp}
```
=== 「ダーツ」
```dart title="min_cost_climbing_stairs_dp.dart"
[class]{}-[func]{minCostClimbingStairsDPComp}
```
=== 「錆」
```rust title="min_cost_climbing_stairs_dp.rs"
[class]{}-[func]{min_cost_climbing_stairs_dp_comp}
```
=== 「C」
```c title="min_cost_climbing_stairs_dp.c"
[class]{}-[func]{minCostClimbingStairsDPComp}
```
=== 「ジグ」
```zig title="min_cost_climbing_stairs_dp.zig"
[class]{}-[func]{minCostClimbingStairsDPComp}
```
後遺症はありません
後遺症がないことは、問題を効果的に解決できる動的プログラミングの重要な特性の 1 つです。これは、次のように定義されます:特定の状態が与えられた場合、その将来の発展は現在の状態にのみ関連しており、現在のすべての状態とは何の関係もありません。状態は過去に経験したものです。
状態iiが与えられた場合の階段の上り下り問題を例にとります。i、状態i + 1 i+1私+1と状態i + 2 i+2私+2、それぞれジャンプ1 11ステップとジャンプ2 22ステップ。これら 2 つの選択を行う際に、状態iiiの前の状態iiiの将来には何の影響もありません。
ただし、階段を登る問題に制約を追加すると、状況は異なります。
!!! 質問「拘束具を付けて階段を登る」
给定一个共有 $n$ 阶的楼梯,你每步可以上 $1$ 阶或者 $2$ 阶,**但不能连续两轮跳 $1$ 阶**,请问有多少种方案可以爬到楼顶。
たとえば、下の写真では、3番目の3番目の場所に登りますレベル3には2 23 つの連続ジャンプを含む2 つの可能な解決策1 1一次解は制約を満たさないため、破棄されます。
この問題では、前のラウンドがジャンプ1 1だった場合、レベル1まで上がったら、2 2レベル2。これは、次のステップの選択は現在の状態 (現在の階段の数) によって独立して決定することはできず、前の状態 (前のラウンドの階段の数) にも関連していることを意味します。
状態遷移方程式dp [ i ] = dp [ i − 1 ] + dp [ i − 2 ] dp[i] = dp[i-1] + dp [i-2]d p [ i ]=d p [ i−1 ]+d p [ i−dp [ i − 1 ] dp[i-1]であるため、 2 ]も失敗します。d p [ i−1 ] はこのラウンドで1 1をジャンプすることを表します1レベルですが、多くの「前のラウンド ジャンプ1 1」1次アップ」スキームであり、制約を満たすためにdp [ i − 1 ] dp[i-1]d p [ i−1 ] dp[i]dp[i]に直接含まれますd p [ i ]中。
これを行うには、状態定義を拡張する必要があります: state [i, j] [i, j][私、j ] は、 ii 番目にあることを示します。レベルi 、最終ラウンドでjjをジャンプj order、ここでj ∈ { 1 , 2 } j \in \{1, 2\}j∈{ 1 、2 }。この状態定義は、前のラウンドのジャンプを効果的に区別します1 1レベル1またはレベル2 2レベル2では、これを使用して次にジャンプする方法を決定できます。
- いつjjj は1 1に等しい1、つまり最後のラウンドは1 1レベル1では、このラウンドでは2 2。レベル2。
- いつjjj は2 2に等しい2、つまり前のラウンドは2 2レベル2では、このラウンドで1 1ジャンプすることを選択できますレベル1またはジャンプ2 2レベル2。
以下の図に示すように、この定義では、dp [ i , j ] dp[i, j]d p [ i ,j ]は状態[i, j] [i, j][私、j ] はプランの数に対応します。このときの状態遷移方程式は次のようになります。
{ dp [ i , 1 ] = dp [ i − 1 , 2 ] dp [ i , 2 ] = dp [ i − 2 , 1 ] + dp [ i − 2 , 2 ] \begin{cases} dp[i, 1] ] = dp[i-1, 2] \\ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2] \end{cases}{ d p [ i ,1 ]=d p [ i−1 、2 ]d p [ i ,2 ]=d p [ i−2 、1 ]+d p [ i−2 、2 ]
最後に、dp [ n , 1 ] + dp [ n , 2 ] dp[n, 1] + dp[n, 2] を返します。d p [ n ,1 ]+d p [ n ,2 ]、2 つの合計はnn 番目n次の解の総数。
=== 「パイソン」
```python title="climbing_stairs_constraint_dp.py"
[class]{}-[func]{climbing_stairs_constraint_dp}
```
=== 「C++」
```cpp title="climbing_stairs_constraint_dp.cpp"
[class]{}-[func]{climbingStairsConstraintDP}
```
=== 「ジャワ」
```java title="climbing_stairs_constraint_dp.java"
[class]{climbing_stairs_constraint_dp}-[func]{climbingStairsConstraintDP}
```
=== 「C#」
```csharp title="climbing_stairs_constraint_dp.cs"
[class]{climbing_stairs_constraint_dp}-[func]{climbingStairsConstraintDP}
```
=== 「行きます」
```go title="climbing_stairs_constraint_dp.go"
[class]{}-[func]{climbingStairsConstraintDP}
```
=== 「スウィフト」
```swift title="climbing_stairs_constraint_dp.swift"
[class]{}-[func]{climbingStairsConstraintDP}
```
=== 「JS」
```javascript title="climbing_stairs_constraint_dp.js"
[class]{}-[func]{climbingStairsConstraintDP}
```
=== 「TS」
```typescript title="climbing_stairs_constraint_dp.ts"
[class]{}-[func]{climbingStairsConstraintDP}
```
=== 「ダーツ」
```dart title="climbing_stairs_constraint_dp.dart"
[class]{}-[func]{climbingStairsConstraintDP}
```
=== 「錆」
```rust title="climbing_stairs_constraint_dp.rs"
[class]{}-[func]{climbing_stairs_constraint_dp}
```
=== 「C」
```c title="climbing_stairs_constraint_dp.c"
[class]{}-[func]{climbingStairsConstraintDP}
```
=== 「ジグ」
```zig title="climbing_stairs_constraint_dp.zig"
[class]{}-[func]{climbingStairsConstraintDP}
```
上記の場合、前の状態を考慮するだけでよいため、状態定義を拡張することによって、問題が再び影響を受けないようにすることができます。ただし、いくつかの問題は非常に深刻な「影響」をもたらします。
!!! 質問「階段の上り下りと障害物の生成」
给定一个共有 $n$ 阶的楼梯,你每步可以上 $1$ 阶或者 $2$ 阶。**规定当爬到第 $i$ 阶时,系统自动会给第 $2i$ 阶上放上障碍物,之后所有轮都不允许跳到第 $2i$ 阶上**。例如,前两轮分别跳到了第 $2$、$3$ 阶上,则之后就不能跳到第 $4$、$6$ 阶上。请问有多少种方案可以爬到楼顶。
この問題では、各ジャンプがより高いはしごに障害物を作成し、将来のジャンプに影響を与えるため、次のジャンプは過去のすべての状態に依存します。このタイプの問題については、動的計画法で解決するのが難しいことがよくあります。
実際、多くの複雑な組み合わせ最適化問題 (巡回セールスマン問題など) は、残効のない特性を満たしていません。このタイプの問題では、通常、限られた時間内で利用可能な局所最適解を取得するために、ヒューリスティック検索、遺伝的アルゴリズム、強化学習などの他の方法を使用することを選択します。