動的プログラミング中間段階
序文
動的プログラミング (Dynamic Programming、略して DP) は、多段階の意思決定プロセスの最適化問題を解く手法です。これは、複雑な問題を重複する部分問題に分解し、各部分問題の最適解を維持することで問題の最適解を導き出す戦略です。
動的プログラミングの主なアイデアは、解決された部分問題の最適解を使用して、より大きな問題の最適解を導き出し、計算の繰り返しを回避することです。したがって、動的プログラミングでは、通常、ボトムアップ アプローチを使用して最初に小規模な問題を解決し、次に問題全体の最適な解決策が解決されるまで、徐々に規模の大きな問題を導き出します。
動的プログラミングには通常、次の基本的な手順が含まれます。
- 状態を定義する: 問題をいくつかのサブ問題に分割し、サブ問題の解決策を表す状態を定義します。
- 状態遷移方程式を定義する: 部分問題間の関係に従って、状態遷移方程式、つまり既知の状態から未知の状態の計算プロセスを推定する方法を設計します。
- 初期状態を決定します。最小の部分問題の解を定義します。
- ボトムアップ解: 状態遷移方程式に従ってすべての状態の最適解を計算します。
- 最適解から問題の解決策を構築します。
動的プログラミングは、最短経路問題、ナップザック問題、最長共通部分列問題、編集距離問題など、多くの実際的な問題を解決できます。同時に、動的計画法は、分割統治アルゴリズムや貪欲アルゴリズムなど、他の多くのアルゴリズムの中核となるアイデアでもあります。
動的計画法は、多段階の意思決定プロセスの最適化問題を解く手法であり、複雑な問題を重複する部分問題に分解し、各部分問題の最適解を維持することで問題の最適解を導き出します。動的プログラミングには、状態の定義、状態遷移方程式の設計、初期状態の決定、ボトムアップ解、問題解の構築などのステップが含まれます。動的プログラミングは多くの実際的な問題を解決でき、他のアルゴリズムの中核となるアイデアの 1 つでもあります。
1. 最大の部分配列の合計
整数の配列 nums を指定して、最大の合計を持つ連続部分配列 (部分配列には少なくとも 1 つの要素が含まれます) を見つけて、その最大の合計を返します。
サブ配列は配列の連続した部分です。
例 1:
入力: nums = [-2,1,-3,4,-1,2,1,-5,4]
出力: 6
説明: 連続する部分配列の合計 [4,-1,2,1] が最大です、6です。
例 2:
入力: nums = [1]
出力: 1
例 3:
入力: nums = [5,4,-1,7,8]
出力: 23
1.1. アイデア
nums 配列の長さが n で、添え字の範囲が 0 から n−1 であるとします。
f(i) を使用して、i 番目の数字で終わる「連続する部分配列の最大合計」を表すと、必要な答えは、時間計算量 O(n) と空間計算量 O(n) の実現です
。 、 f を使用する f(i) の値を格納するには配列が使用され、すべての
f(i) を取得するにはループが使用されます。f(i) が f(i−1) にのみ関連していることを考慮すると、現在の f(i) に対する f(i−1) の値を維持するために変数 pre を 1 つだけ使用することができ、それによって空間の複雑さを O に減らすことができます。 (1)、これは「ローリング配列」の考え方に少し似ています。
1.2. コードの実装
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int pre = 0, maxAns = nums[0];
for (const auto &x: nums) {
pre = max(pre + x, x);
maxAns = max(maxAns, pre);
}
return maxAns;
}
};
2. ジャンプゲーム
負でない整数の配列 nums を指定すると、最初は配列の最初のインデックスに位置します。
配列内の各要素は、その位置でジャンプできる最大長を表します。
最後の添え字に到達できるかどうかを判断します。
例 1:
入力: nums = [2,3,1,1,4]
出力: true
説明: 最初に添字 0 から添字 1 まで 1 ステップジャンプし、次に添字 1 から最後の添字まで 3 ステップジャンプできます。
例 2:
入力: nums = [3,2,1,0,4]
出力: false
説明: 何があっても、添字 3 の位置には常に到達します。ただし、この添字の最大ジャンプ長は 0 であるため、最後の添字に到達することはできません。
出典: LeetCode。
2.1. アイデア
状態表現: f[i] が i に到達できるかどうかを示すものとします。
再帰分析: f[i] の到達可能性を考慮すると、i が到達可能である場合、以前の到達可能な点 j から直接ジャンプしたはずであり、ji 間の距離は次の値以下であることがわかります。 j に対応するステップ数で、i の直前にスキップできる点がある場合は i に到達可能であることを意味し、そうでない場合は i に到達できないため、i より前のすべての点をトラバースできます。
漸化式:
f[i] = (f[i - 1] && nums[i - 1] >= 1) || (f[i - 2] && nums[i - 2] >= 2)......|| (f[0] && nums[0] >= i)
最初は f[0] = true、その後は前から後ろに再帰的に行われます。
最適化: i が到達不能な場合、i より前のすべての点は最も遠い i に到達できず、i の後の点にも到達できないため、i 以降のすべての点も到達不能でなければならないことに注意してください。時間計算量は依然としてO ( n 2 ) O(n^2)ですが、O ( n2 )ただし、多くの二重カウントは避けられます。
2.2. コードの実装
class Solution {
public:
bool canJump(vector<int>& nums) {
int l = nums.size();
vector<bool> f(l);
f[0] = true;
for(int i = 1; i < l; i ++)
{
for(int j = i - 1; j >= 0; j --)
if(f[j] && nums[j] >= i - j)
{
f[i] = true;
break;
}
//如果i不可达,则直接退出循环,i后面的点都不可达
if(!f[i]) break;
}
return f[l - 1];
}
};
概要: dp[i] を使用して、i に到達できるかどうかを示します。たとえば、[2,3,1,1,4]、dp[0] = true を初期化し、最初の要素 2 を考慮し、次に dp[1] = true、dp[2] = true、次の要素 3 を走査します。に到達できる場合は、 dp[2] = true dp[3] = true dp[4] = true などとなります。
3. デコード方法
文字 A ~ Z を含むメッセージは、次のマッピングによってエンコードされます。
'A' -> "1"
'B' -> "2"
...
'Z' -> "26"
エンコードされたメッセージをデコードするには、上記のマッピング方法に基づいて、すべての数字を文字に逆マッピングする必要があります (方法は複数ある場合があります)。たとえば、「11106」は次のようにマッピングできます。
- 「AAJF」、メッセージを (1 1 10 6) としてグループ化します。
- 「KJF」、メッセージを (11 10 6) としてグループ化します
。「6」と「06」は「06」を「F」にマッピングできないため、メッセージを (1 11 06) としてグループ化できないことに注意してください。マッピングは同等ではありません。
数値のみを含む空ではない文字列 s を指定した場合、デコード メソッドの合計数を数えて返します。
質問データは、回答が 32 ビット整数であることを保証します。
例 1:
入力: s = "12"
出力: 2
説明: "AB" (1 2) または "L" (12) としてデコードできます。
例 2:
入力: s = "226"
出力: 3
説明: "BZ" (2 26)、"VF" (22 6)、または "BBF" (2 2 6) としてデコードできます。
例 3:
入力: s = "06"
出力: 0
説明: 先行ゼロがあるため、「06」を「F」にマッピングできません (「6」と「06」は等価ではありません)。
出典: LeetCode。
3.1. アイデア
与えられた文字列 s について、その長さを n とし、その文字列を左から右に s[1]、s[2]、⋯、s[n] とします。動的プログラミングの方法を使用して、文字列のデコード方法の数を計算できます。
fi は、文字列 s の最初の i 文字 s[1…i] の復号化メソッドの数を表します。状態遷移を実行するとき、 s 内のどの文字が最後のデコードに使用されたかを考慮すると、次の 2 つの状況が発生します。
- 最初のケースは、デコードに文字、つまり s[i] を使用するため、s[i]≠0 である限り、A〜I の文字にデコードできます。残りの最初の i-1 文字の復号方法の数は f (i-1) であるため、状態遷移方程式は次のように記述できます。
- 2 番目のケースは、エンコードに s[i−1] と s[i] という 2 つの文字を使用する場合です。最初のケースと同様に、s[i−1] を 0 に等しくすることはできず、s[i−1] と s[i] で構成される整数は 26 以下でなければなりません。そうすることで、これらをデコードできるようになります。いくつかの手紙。残りの最初の i-2 文字の復号化方法の数は f (i-2) であるため、状態遷移方程式は次のように書くことができます。
3.2. コードの実装
class Solution {
public:
int numDecodings(string s) {
int n = s.size();
// a = f[i-2], b = f[i-1], c = f[i]
int a = 0, b = 1, c;
for (int i = 1; i <= n; ++i) {
c = 0;
if (s[i - 1] != '0') {
c += b;
}
if (i > 1 && s[i - 2] != '0' && ((s[i - 2] - '0') * 10 + (s[i - 1] - '0') <= 26)) {
c += a;
}
tie(a, b) = {
b, c};
}
return c;
}
};
時間計算量: O(n)。
空間複雑度: O(1)。
要約する
動的計画法(Dynamic Programming)は、多段階の意思決定最適化問題を解く手法であり、複雑な問題を重複する部分問題に分解し、各部分問題の最適解を維持することで問題の最適解を導き出します。動的プログラミングは、最短経路問題、ナップザック問題、最長共通部分列問題、編集距離問題など、多くの実際的な問題を解決できます。
動的プログラミングの基本的な考え方は、解決された部分問題の最適解を使用して、より大きな問題の最適解を導き出し、計算の繰り返しを回避することです。通常、ボトムアップのアプローチを使用して、最初に小規模な問題を解決し、次に問題全体の最適な解決策が解決されるまで、徐々に大規模な問題を導き出します。
動的プログラミングには通常、次の基本的な手順が含まれます。
- 状態を定義する: 問題をいくつかのサブ問題に分割し、サブ問題の解決策を表す状態を定義します。
- 状態遷移方程式を定義する: 部分問題間の関係に従って、状態遷移方程式、つまり既知の状態から未知の状態の計算プロセスを推定する方法を設計します。
- 初期状態を決定します。最小の部分問題の解を定義します。
- ボトムアップ解: 状態遷移方程式に従ってすべての状態の最適解を計算します。
- 最適解から問題の解決策を構築します。
動的計画法の時間計算量は通常O ( n 2 ) O(n^2)です。O ( n2)或 O ( n 3 ) O(n^3) O ( n3 ) の場合、空間複雑度は O(n) です。ここで、n は問題の規模を表します。実際のアプリケーションでは、空間の複雑さを軽減するために、通常、ローリング アレイなどの手法を使用して動的プログラミング アルゴリズムを最適化できます。