動的プログラミング: 2 つの配列の dp 問題 (C++)

序文

動的プログラミングに関する以前の記事:

  1. 動的プログラミングの概要: フィボナッチ数列モデルとマルチステート
  2. 動的プログラミング: パスと部分配列の問題
  3. 動的計画法: 後続問題
  4. 動的計画法: 回文問題

2 つの配列の dp 問題

1. 最長共通部分列(中)

リンク:最長共通部分列

  • 質問の説明
    ここに画像の説明を挿入します

  • 質問を行う手順

  1. 状態表現
    2 つの配列の dp は 1 次元の dp では状態を明確に表現できないため、通常は 2 次元の配列を使用します

    したがって、定義された状態は dp[i][j] として表されます。これは、 s1 の [0, i] 区間と s2 の [0, j] 区間の間の最長の共通部分列です

  2. 状態遷移方程式
    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] )

  3. 初期化

ここに画像の説明を挿入します

  1. フォームに記入する
    順序 上の図を参照すると、フォームに記入する順序は上から下、各行は左から右です

  2. 戻り値
    ステータス表現に従い、戻り値は 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. 異なるサブシーケンス (難しい)

リンク:さまざまなサブシーケンス

  • 質問の説明
    ここに画像の説明を挿入します

  • 質問を行う手順

  1. ステータスの表示:
    この質問は難しいですが、以前にこの質問をした経験がある場合は、実際には悪くありません。
    この種の問題では、2 次元テーブルを使用し、状態表現をdp[i][j] として定義します。これは、[0, i] の t の [0, j] 区間に現れる解の数です。 s の間隔

  2. 状態遷移方程式
    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] となります

  3. 初期化
    この質問の初期化は前の質問と同様で、追加の行と列を開き、追加の行と列を空の文字列として扱います。このうち、 t が空文字列の場合は、 s に解が存在する必要があるため (s も空文字列を取る)、最初の列は 1 に初期化されます

  4. フォームに記入する順番です。
    フォームがわからない場合は、最初の質問を参照してください。フォームに記入する順序は、上から下、各行は左から右です

  5. 戻り値
    ステータス表現に従い、戻り値は 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. ワイルドカードマッチング (難しい)

リンク:ワイルドカード マッチング

  • 質問の説明
    ここに画像の説明を挿入します

  • 質問を行う手順

  1. 状態表現
    これまでの問題解決の経験に基づいて、2 次元テーブルを定義し、状態表現をdp[i][j] として定義します。p の [0, j] 間隔が [0, i] と一致するかどうかを示します。 s の間隔

  2. 状態遷移方程式
    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 です。

  3. 初期化
    前と同様に、境界を越えることを避け、初期化を容易にするために、空の文字列の概念を導入し、追加の行と列を開きます。
    ①両方とも空の文字列であり、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に初期化されます。

  4. フォームに記入する順番です。
    フォームがわからない場合は、最初の質問を参照してください。フォームに記入する順序は、上から下、各行は左から右です

  5. 戻り値
    ステータス表現に従い、戻り値は 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. 正規表現(難しい)

リンク:正規表現

  • 質問の説明
    ここに画像の説明を挿入します

  • 質問を行う手順

  1. 状態表現
    これまでの問題解決の経験に基づいて、2 次元のテーブルを定義し、状態表現をdp[i][j] として定義します。p の [0, j] 領域が [0 、i] s の領域

  2. 状態遷移方程式
    この質問のポイント: 「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] のいずれかが当てはまる限り当てはまります。

  3. 初期化 境界
    を越えることを避け、初期化を容易にするために、空の文字列の概念を導入し、もう 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 に初期化されます。

  4. フォームに記入する順番です。
    フォームがわからない場合は、最初の質問を参照してください。フォームに記入する順序は、上から下、各行は左から右です

  5. 戻り値
    ステータス表現に従い、戻り値は 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. インターリーブ文字列 (中)

リンク:インターリーブされた文字列

  • 質問の説明
    ここに画像の説明を挿入します

  • 質問を行う手順

  1. 状態表現:
    問題を解決する際のこれまでの経験に基づいて、2 次元のテーブルを定義し、状態表現をdp[i][j] として定義します。s1 の [0, i] 間隔と [0, j] は可能でしょうか。 ] s2 の間隔をインターリーブして s3 を形成する [0, i + j] 間隔

  2. 状態遷移方程式:
    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]。

  3. 初期化 境界
    を越えることを避け、初期化を容易にするために、空の文字列の概念を導入し、もう 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 が等しい)。

  4. フォームに記入する順番です。
    フォームがわからない場合は、最初の質問を参照してください。フォームに記入する順序は、上から下、各行は左から右です

  5. 戻り値
    ステータス表現に従い、戻り値は 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 ストリップ合計

  • 質問の説明
    ここに画像の説明を挿入します

  • 質問を行う手順

  1. 状態表現
    問題を解決する際のこれまでの経験に基づいて、2 次元テーブルを定義し、状態表現をdp[i][j] として定義します。つまり、s1 の [0, i] 間隔と [0, j] 間隔です。 s2 の削除消費量は同じ最小値に達する必要があります

  2. 状態遷移方程式
    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 以上である必要があります。

  3. 初期化 境界
    を越えることを避け、初期化を容易にするために、空の文字列の概念を導入し、もう 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++) となります。

  4. フォームに記入する順番です。
    フォームがわからない場合は、最初の質問を参照してください。フォームに記入する順序は、上から下、各行は左から右です

  5. 戻り値
    ステータス表現に従い、戻り値は 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. 最長反復部分配列 (中)

リンク:最長の繰り返し部分配列

  • 質問の説明
    ここに画像の説明を挿入します

  • 質問を行う手順

  1. 状態表現の
    問題は難しくありませんが、部分列ではなく部分配列であることに注意してください。前の方法で状態表現を定義すると、エラーが発生します。たとえば、状態表現を 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 位置で終わる共通の最長部分配列の長さです

  2. 状態遷移方程式
    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 です。

  3. 初期化 境界
    を越えないようにするために、追加の行と列を開きます。dp 配列の添字は 1 から始まり、追加の行と列は 0 に初期化されます。(n1 配列と n2 配列は添字 0 から始まるため、n1 配列と n2 配列の添字マッピングに注意してください)

  4. フォームに記入する順序:
    フォームに記入する順序は上から下、各行は左から右です

  5. 戻り値
    最長の部分配列の終わりを直接判断する方法はないため、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;
    }
};

おすすめ

転載: blog.csdn.net/2301_76269963/article/details/133410990