動的計画法: 2 つの配列の dp 問題
序文
動的プログラミングに関する以前の記事:
2 つの配列の dp 問題
1. 最長共通部分列(中)
リンク:最長共通部分列
-
質問の説明
-
質問を行う手順
-
状態表現
2 つの配列の dp は 1 次元の dp では状態を明確に表現できないため、通常は 2 次元の配列を使用します。
したがって、定義された状態は dp[i][j] として表されます。これは、 s1 の [0, i] 区間と s2 の [0, j] 区間の間の最長の共通部分列です。 -
状態遷移方程式
s1 の [0, i] 区間と s2 の [0, j] 区間について、次の状況について説明します。
(1) s1[i] == s2[j]を知る必要があるだけです。 s1 の [0, j]、i - 1] 区間と s2 の [0, j - 1] 区間の間の最長共通部分列に 1 を加算します。つまり、dp[i] [j] = dp [ i - 1] [j - 1 ] + 1。(たとえば、 s1 = "abc" および s2 = "akc" は、"ab" と "ak" に 1 を加えた最長の共通部分列です) (2) s1 [i] != s2[j] 、これは次の時点
で最長です。今度は、共通部分列が s1[i] と s2[j] で同時に終了してはなりません。
① s2[j]で終わる場合がありますので、s1の[0, i - 1]とs2の[0, j]の区間で求めます:このときの最大長はdp[i - 1] [j]です。(例: s1 = "ack"、s2 = "bc")
② s1[i]で終わる場合があり、s1の[0, i]とs2の[0, j - 1]の範囲に移動して検索します。 : このときの最大長は dp[i][j - 1] です。(例: s1 = "ac"、s2 = "cb")
③ どちらも終了ではない可能性もありますが、この状況は最初の 2 つの状況に含まれ、最初の 2 つの状況以下である必要があります。(例: s1 = "acd"、s2 = "aca")
したがって、(2)の場合、 dp[i] [j] = max(dp[i - 1] [j], dp[i] [ j - 1] )。 -
初期化
-
フォームに記入する
順序 上の図を参照すると、フォームに記入する順序は上から下、各行は左から右です。 -
戻り値
ステータス表現に従い、戻り値は dp[m] [n] となります(m、n はそれぞれ s1、s2 の長さ)。
- コード
class Solution {
public:
int longestCommonSubsequence(string s1, string s2) {
int m = s1.size(), n = s2.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
//处理下标映射
s1 = " " + s1, s2 = " " + s2;
for(int i = 1; i <= m; i++)
for(int j = 1; j <= n; j++)
{
if(s1[i] == s2[j])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
return dp[m][n];
}
};
2. 異なるサブシーケンス (難しい)
リンク:さまざまなサブシーケンス
-
質問の説明
-
質問を行う手順
-
ステータスの表示:
この質問は難しいですが、以前にこの質問をした経験がある場合は、実際には悪くありません。
この種の問題では、2 次元テーブルを使用し、状態表現をdp[i][j] として定義します。これは、[0, i] の t の [0, j] 区間に現れる解の数です。 s の間隔。 -
状態遷移方程式
s の [0, i] 区間と t の [0, j] 区間について、次のような状況を議論します。 (1)
s [i] == t[j] :
① たとえば、 t = "rab" かつ s = "rabcb" の場合、最初の方法はs[i] と t[j] を同時に終了として選択しますこのときの選択肢の数は [0, j - 1] 区間ですs の [0, i - 1] 区間に現れる t の解の数 (rabc に ra が現れる回数)、つまり dp[i - 1] [j - 1]。
②2番目の方法は、s[i]とt[j]を同時に終端に選ばない方法で、このときの選択肢の数は、tの[0, j]区間に現れる選択肢の数になります。 [0, i - 1] s の区間 (t = s の "rabc" に "rab" が出現する回数)、つまり dp[i - 1] [j]。
どちらも要件を満たしています: したがって、(1) case dp[i] [j] = dp[i - 1] [j] + dp[i - 1] [j - 1] (2) s[i] ! =
t [ j] :
現時点では選択肢は (1) のケース②の 1 つだけなので、ケース (2) dp[i] [j] = dp[i - 1] [j] となります。 -
初期化
この質問の初期化は前の質問と同様で、追加の行と列を開き、追加の行と列を空の文字列として扱います。このうち、 t が空文字列の場合は、 s に解が存在する必要があるため (s も空文字列を取る)、最初の列は 1 に初期化されます。 -
フォームに記入する順番です。
フォームがわからない場合は、最初の質問を参照してください。フォームに記入する順序は、上から下、各行は左から右です。 -
戻り値
ステータス表現に従い、戻り値は dp[m] [n] (m、n はそれぞれ s、t の長さ) となります。
- コード
class Solution {
public:
int numDistinct(string s, string t) {
int m = s.size(), n = t.size();
//这个题目中间填表的时候会溢出,而且溢的不是一点点
//不过溢出的部分不影响结果,用uint即可
vector<vector<unsigned int>> dp(m + 1, vector<unsigned int>(n + 1));
s = " " + s, t = " " + t; //处理下标映射
for(int i = 0; i < m; i++) dp[i][0] = 1;
for(int i = 1; i <= m; i++)
for(int j = 1; j <= n; j++)
{
dp[i][j] = dp[i - 1][j];
if(s[i] == t[j]) //s[i] == t[j]会多一种选择
dp[i][j] += dp[i - 1][j - 1];
}
return dp[m][n];
}
};
3. ワイルドカードマッチング (難しい)
リンク:ワイルドカード マッチング
-
質問の説明
-
質問を行う手順
-
状態表現
これまでの問題解決の経験に基づいて、2 次元テーブルを定義し、状態表現をdp[i][j] として定義します。p の [0, j] 間隔が [0, i] と一致するかどうかを示します。 s の間隔。 -
状態遷移方程式
s の [0, i] 区間と p の [0, j] 区間について、次の状況を説明します。 (1)
s [i] == p[j] または p[j] == '?' の場合、 dp[i] [j] = dp[i - 1] [j - 1]、つまり、 p の [j - 1] 領域が p の [i - 1] 領域と一致する限り、 s の場合、[0, j ] は s の [0, i] と一致します。(例: s = "abc"、p = "ab?")
(2) p[j] == ' * 'の場合、p[0, j] と s[0 を作るには 3 つの可能性があります。 , i] 一致:
① p の [0, j] は s の [0, i - 1] と一致します。 p[j] == ' * ' s[i] を追加して元の文字列、つまり dp [ i - 1] [j] は true dp[i] [j] は true です。(例: s = "abc"、p = "a*"、"ab"、"a*" が一致します)
② p の [0, j - 1] は s , ' *の [0, i] と一致します。 ' このとき、空の文字列と一致するだけです。つまり、 dp[i] [j - 1] が true で、 dp[i] [j] が true です。(例: s = "ab"、p = "ab*")
③p[0, j - 1] は s の [0, i - 1] と一致し、p[j] == ' * ' で s[i を置き換えます] ] ですが、この状況は実際には最初の状況に起因する可能性があります。s[0, i - 1] と p[0, j - 1] が一致する場合、s[0, i - 1] と p[0, j]このとき、「 * 」は文字列を空にします。dp[i - 1] [j - 1] は true == dp[i - 1] [j] は true です。
dp[i][j] の 1 つが true である限り、上記の状況は true です。 -
初期化
前と同様に、境界を越えることを避け、初期化を容易にするために、空の文字列の概念を導入し、追加の行と列を開きます。
①両方とも空の文字列であり、dp[0] [0] = true と一致します。
② s が空文字列で、p が空文字列でない場合(1 行目の [0, 0] を除く)、p の [0, j] 区間が連続する '*' であれば、空文字列も空文字列とすることができます。一致、 dp[0] [ 0...j] = true。([0, j]間隔は連続した'*'を表します)
③pは空文字列、sは空文字列ではありません(1列目の[0,0]を除く)、現時点では[0]以外は一致しません][ 1列目は0] その他はfalseに初期化されます。 -
フォームに記入する順番です。
フォームがわからない場合は、最初の質問を参照してください。フォームに記入する順序は、上から下、各行は左から右です。 -
戻り値
ステータス表現に従い、戻り値は dp[m] [n] となります(m、n はそれぞれ s、p の長さ)。
- コード
class Solution {
public:
bool isMatch(string s, string p) {
int m = s.size(), n = p.size();
s = " " + s, p = " " + p; //处理下标映射
//dp[i][j]:p的[0, j]区间能否匹配s的[0, i]区间
vector<vector<bool>> dp(m + 1, vector<bool>(n + 1));
dp[0][0] = true;
for(int j = 1; j <=n; j++) //初始化s为空串,p有连续'*'可匹配的情况
{
if(p[j] == '*')
dp[0][j] = true;
else
break; //出现非'*'直接结束循环,后面不可能匹配了
}
for(int i = 1; i <= m; i++)
for(int j = 1; j <= n; j++)
{
if(p[j] == '*')
dp[i][j] = dp[i - 1][j] || dp[i][j - 1];
else if(s[i] == p[j] || p[j] == '?')
dp[i][j] = dp[i - 1][j - 1];
}
return dp[m][n];
}
};
4. 正規表現(難しい)
リンク:正規表現
-
質問の説明
-
質問を行う手順
-
状態表現
これまでの問題解決の経験に基づいて、2 次元のテーブルを定義し、状態表現をdp[i][j] として定義します。p の [0, j] 領域が [0 、i] s の領域。 -
状態遷移方程式
この質問のポイント: 「a*」は、この部分が複数回または 0 回出現する可能性があることを示します。つまり、 a は空の文字列を表すため、を解析する際には「文字 + 」を全体として考慮する必要があります。
s の [0, i] 区間と p の [0, j] 区間について、状況に応じて議論します。
(1) s[i] == p[j] または p[j] == ' . 'のみが必要です p の [0, j - 1] が s の [0, i - 1] と一致する限り、つまり、dp[i - 1] [j - 1] が true の場合、 dp[i] [j ] が true になります。(例: s = "abc"、p = "abc")
(2) p[j] == ' * 'の場合、p[0, j] と s[0, :
① p[0, j - 2] は s[0, i] と一致し、その後の「文字 +」は空の文字列を表します。つまり、 dp[i] [j - 2] が true の場合、 dp[i] [j] も true になります。(例: s = "abc"、p = "abcg*"、p の後の "g*" は空文字列として直接使用できます) ② p[0, j] と s[0, i - 1] が
一致、元の「文字 +」もう 1 文字を表す必要があります。
ただし、ここでの複数表現された文字は固定されています。つまり、この複数表現された文字が要件を満たす前に、 p[j - 1] == s[i] または p[j - 1] == ' . ' が満たされる必要があります。 。つまり、前の条件 dp[i - 1] [j] が true の場合、 dp[i] [j] は true になります。
(たとえば、 s = "abbb"、p = "ab*" の場合、"ab*" は "abb" と一致しますが、"b*" のみは、複数の ' b ' が一致要件を満たすことを意味します。 If s = " abbc" p は s と一致しません)
上記の状況は、dp[i][j] のいずれかが当てはまる限り当てはまります。 -
初期化 境界
を越えることを避け、初期化を容易にするために、空の文字列の概念を導入し、もう 1 つの行と列を開きます。
①両方とも空の文字列であり、dp[0] [0] = true と一致します。
② sが空文字列で、pが空文字列ではない場合(1行目の[0,0]を除く)、pが連続する「文字+*+文字+*...」の場合、これらの「文字」をすべて+ *" 空の文字列として、 s を照合できます。つまり、 dp[0] [j] = true(j = 2; j <= n; j += 2) です。
③p は空文字列、s は空文字列ではありません (1 列目の [0, 0] を除く)現時点では一致できません1 列目 [0] [0] 以外は false に初期化されます。 -
フォームに記入する順番です。
フォームがわからない場合は、最初の質問を参照してください。フォームに記入する順序は、上から下、各行は左から右です。 -
戻り値
ステータス表現に従い、戻り値は dp[m] [n] となります(m、n はそれぞれ s、p の長さ)。
- コード
class Solution {
public:
bool isMatch(string s, string p) {
int m = s.size(), n = p.size();
//处理下标映射
s = " " + s, p = " " + p;
//dp[i][j]:p的[0,j]区域能否和s的[0,i]区域匹配
vector<vector<bool>> dp(m + 1, vector<bool>(n + 1));
dp[0][0] = 1; //空串可以匹配空串
for(int j = 2; j <= n; j += 2) //s为空串时p为连续的"字符 + *"是可以匹配的
{
if(p[j] == '*')
dp[0][j] = true;
else
break;
}
for(int i = 1; i <= m; i++)
for(int j = 1; j <= n; j++)
{
if(p[j] == '*')
{
dp[i][j] = dp[i][j-2] || (p[j-1] == '.' || p[j-1] == s[i]) && dp[i-1][j];
}
else if(s[i] == p[j] || p[j] == '.')
{
dp[i][j] = dp[i - 1][j - 1];
}
}
return dp[m][n];
}
};
5. インターリーブ文字列 (中)
リンク:インターリーブされた文字列
-
質問の説明
-
質問を行う手順
-
状態表現:
問題を解決する際のこれまでの経験に基づいて、2 次元のテーブルを定義し、状態表現をdp[i][j] として定義します。s1 の [0, i] 間隔と [0, j] は可能でしょうか。 ] s2 の間隔をインターリーブして s3 を形成する [0, i + j] 間隔。 -
状態遷移方程式:
s1 の [0, i] 区間と s2 の [0, j] 区間をインターリーブして s3 の [0, i + j] 区間を形成できるかどうかを状況に応じて議論します: ( 1) s1[i
] == s3[i + j]。このとき、s1 の [0, i - 1] 区間と s2 の [0, j] 区間が s3 の [0, i + j - 1] 区間を形成できれば成立します。 , dp[i] [j] = ( s1 [i] == s3[i + j] && dp[i - 1] [j])
(2) s2[j] == s3[i + j]。このとき、s1 の [0, i] 区間と s2 の [0, j - 1] 区間が s3 の [0, i + j - 1] 区間を形成できれば成立します。 , dp[i] [j] = ( s2 [j] == s3[i + j] && dp[i] [j - 1])
上記の状況のいずれかが真である限り、上記の状況は真です。 dp[ i] [j]。 -
初期化 境界
を越えることを避け、初期化を容易にするために、空の文字列の概念を導入し、もう 1 つの行と列を開きます。
① s1 と s2 が両方とも空の文字列である場合、空の文字列 s3 、つまり dp[0][0] = true を形成できます。
② s1 が空文字列であり、s2 が空文字列ではない場合(第 1 列の [0, 0] を除く)、それらが等しい限り、s3 は s2 のみで構成できます。つまり、 dp[0] [j] = true ([1, j] 間隔 s2 と s3 は等しい)。
③s2が空文字列であり、s1が空文字列ではない場合(1行目の[0,0]を除く)、それらが等しい限り、s3はs1のみで構成できます。つまり、dp[i] [0] = true ([1, i] 間隔 s1 と s3 が等しい)。 -
フォームに記入する順番です。
フォームがわからない場合は、最初の質問を参照してください。フォームに記入する順序は、上から下、各行は左から右です。 -
戻り値
ステータス表現に従い、戻り値は dp[m] [n] となります(m、n はそれぞれ s1、s2 の長さ)。
- コード
class Solution
{
public:
bool isInterleave(string s1, string s2, string s3)
{
int m = s1.size(), n = s2.size();
if(m + n != s3.size()) return false; //两者相加比s3长度小,一定没办法组成的
s1 = " " + s1, s2 = " " + s2, s3 = " " + s3; //处理下标映射
//dp[i][j]:s1的[1,i]区间和s2的[1,j]区间能否交错组成s3的[1,i+j]区间
vector<vector<bool>> dp(m + 1, vector<bool>(n + 1));
dp[0][0] = true;
for(int j = 1; j <= n; j++) // 初始化第⼀⾏,即s1为空,s2单独组成s3
{
if(s2[j] == s3[j]) dp[0][j] = true;
else break;
}
for(int i = 1; i <= m; i++) // 初始化第⼀列,即s2为空,s1单独组成s3
{
if(s1[i] == s3[i]) dp[i][0] = true;
else break;
}
for(int i = 1; i <= m; i++)
for(int j = 1; j <= n; j++)
dp[i][j] = (s1[i] == s3[i + j] && dp[i - 1][j])
|| (s2[j] == s3[i + j] && dp[i][j - 1]);
return dp[m][n];
}
};
6. 2 つの文字列の最小 ASCII ストリップ合計 (中)
リンケージ: 2 つの文字列の最小 ASCII ストリップ合計
-
質問の説明
-
質問を行う手順
-
状態表現
問題を解決する際のこれまでの経験に基づいて、2 次元テーブルを定義し、状態表現をdp[i][j] として定義します。つまり、s1 の [0, i] 間隔と [0, j] 間隔です。 s2 の削除消費量は同じ最小値に達する必要があります。 -
状態遷移方程式
s1 の [0, i] 区間が s2 の [0, j] 区間とどのように同じになるかについて説明します。 (1)
s1 [i] == s2[j] のとき、 s1 1, i - 1] と s2[1, j - 1] の [0, j] 間隔を同じにする必要があるだけです。つまり、dp[i] [j] = dp[i - 1 ] [j - 1]。
(2) s1[i] != s2[j] の場合、次の 2 つのオプションがあります。
① s1 の [1, i - 1] と s2 の [1, j] を同じにする、冗長な s1[i] を削除する、つまり、dp[i] [j] = dp[i - 1] [j] + s1[i] です。
②s1の[1, i]はs2の[1, j -1]と同じなので、冗長なs2[j]を削除、つまり dp[ i] [j] = dp[i] [j - 1 ] + s2[j ]。
ケース①②の最小値を取るだけ、つまりケース(2) dp[i][j] = min(dp[i] [j - 1] + s2[j], dp[i - 1] [j] + s1 [i])。
ここで、(1) の消費量は (2) の消費量以下でなければならないことに注意してください。たとえば、短い文字列と長い文字列がある場合、消費する x は同じになります。ここで、短い文字列の後にいくつかの文字を追加します。同等にしたい場合は、消費量が x 以上である必要があります。 -
初期化 境界
を越えることを避け、初期化を容易にするために、空の文字列の概念を導入し、もう 1 つの行と列を開きます。
① s1 と s2 が両方とも空文字列の場合、消費量は 0、つまり dp[0] [0] = 0 となります。
② s1 が空文字列で、s2 が空文字列でない場合(第 1 列の [0, 0] を除く)、s2 は空文字列になるまで完全に削除する必要があります。つまり、dp[0] [j] = dp[0] [j - 1] + s2[j] (j = 1; j <= n; j++) です。
③s2が空文字列で、s1が空文字列ではない場合(1行目の[0,0]を除く)、s1は空文字列になるまで完全に削除する必要があります。つまり、dp[i] [0] = dp[i - 1] [0] + s1[i] (i = 1; i <= m; i++) となります。 -
フォームに記入する順番です。
フォームがわからない場合は、最初の質問を参照してください。フォームに記入する順序は、上から下、各行は左から右です。 -
戻り値
ステータス表現に従い、戻り値は dp[m] [n] となります(m、n はそれぞれ s1、s2 の長さ)。
- コード
class Solution {
public:
int minimumDeleteSum(string s1, string s2) {
int m = s1.size(), n = s2.size();
s1 = " " + s1, s2 = " " + s2; //处理下标映射
//dp[i][j]:s1的[1,i]区间和s2的[1,j]区间要达到相同的最小删除消耗
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
//s1为空串,s2要删除为空串的最小消耗
for(int j = 1; j <= n; j++)
dp[0][j] = dp[0][j - 1] + s2[j];
//s2为空串,s1要删除到空串的最小消耗
for(int i = 1; i <= m; i++)
dp[i][0] = dp[i - 1][0] + s1[i];
for(int i = 1; i <= m; i++)
for(int j = 1; j <= n; j++)
{
if(s1[i] == s2[j])
dp[i][j] = dp[i - 1][j - 1];
else
dp[i][j] = min(dp[i][j - 1] + s2[j], dp[i - 1][j] + s1[i]);
}
return dp[m][n];
}
};
7. 最長反復部分配列 (中)
リンク:最長の繰り返し部分配列
-
質問の説明
-
質問を行う手順
-
状態表現の
問題は難しくありませんが、部分列ではなく部分配列であることに注意してください。前の方法で状態表現を定義すると、エラーが発生します。たとえば、状態表現を dp として定義すると、 [i][j]: n1 の [0, i] と n2 の間の [0, j] 区間の共通部分配列の最長の長さ。
n1 = [3, 1, 1] と n2 = [1, 0, 1] を例にとると、n1 の [3, 1] 区間と n1 の [1, 0] 区間に共通する最長の部分配列の長さn2 が 1 n1[ 2] のとき == n2[2] のとき、最長共通部分配列は計算できません dp[i - 1][j - 1] + 1 を求めても、n1 なので絶対に不可能です[2] と n2[2 ] は、最長のサブ配列の後に必ずしも接続されているとは限りません。サブ配列は連続している必要があります。!!
前回は区間に焦点を当てており、状態遷移方程式を導出する方法がなかったため、n1[i] と n2[j] を部分配列として解析を終了しました。
2 次元テーブルを定義し、状態表現をdp[i][j] として定義します。これは、 n1 の i 位置と n2 の j 位置で終わる共通の最長部分配列の長さです。 -
状態遷移方程式
n1[i] と n2[j] について、
(1) n1[i] == n2[j] のとき、 n1[i - 1] と n2[j] に接続できる1] は最も長い共通部分配列の終わりであり、長さは 1 増加します、つまり、dp[i] [j] = dp[i - 1] [j - 1] + 1 です。
(2) n1[i] != n2[j] の場合、n1[i] と n2[j] で終わる共通の最長部分配列は存在しません。つまり、 dp[i] [j] = 0 です。 -
初期化 境界
を越えないようにするために、追加の行と列を開きます。dp 配列の添字は 1 から始まり、追加の行と列は 0 に初期化されます。(n1 配列と n2 配列は添字 0 から始まるため、n1 配列と n2 配列の添字マッピングに注意してください) -
フォームに記入する順序:
フォームに記入する順序は上から下、各行は左から右です。 -
戻り値
最長の部分配列の終わりを直接判断する方法はないため、dp 中に最大値が更新されます。
- コード
class Solution {
public:
int findLength(vector<int>& n1, vector<int>& n2) {
int m = n1.size(), n = n2.size();
//dp[i][j]表示以nums1的i位置和nums2的j位置结尾的公共最长子数组长度
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
int ret = 0;
for(int i = 1; i <= m; i++)
for(int j = 1; j <= n; j++)
{
if(n1[i - 1] == n2[j - 1]) //注意下标映射
dp[i][j] = dp[i - 1][j - 1] + 1;
ret = max(ret, dp[i][j]);
}
return ret;
}
};