兩字符串最值問題->動態規劃dp[i][j]:編輯距離+最長公共子序列;一字符串最值問題->動態規劃dp[i][j]:最長迴文子序列

 https://labuladong.github.io/ebook/動態規劃系列/

  1. 最值+最優子結構+重疊子問題:
    1. 兩個數組/字符串
      1. dp[i][j]:SelArray1[0~i] SelArray2[0~j]
        1. 一般與dp[i-1][j-1],dp[i-1][j],dp[i][j-1]有關
    2. 一個數組/字符串
      1. dp[i]:以i爲尾的...
      2. dp[i][j]:SelArray[i,i+1,,,j-1,j]
        1. 一般與dp[i+1][j-1],dp[i][j-1],dp[i+1][j]有關
一、兩種思路
1、第一種思路模板是一個一維的 dp 數組:
int n = array.length;
int[] dp = new int[n];

for (int i = 1; i < n; i++) {
    for (int j = 0; j < i; j++) {
        dp[i] = 最值(dp[i], dp[j] + ...)
    }
}
舉個我們寫過的例子「最長遞增子序列」,在這個思路中 dp 數組的定義是:
在子數組 array[0..i] 中,我們要求的子序列(最長遞增子序列)的長度是 dp[i]。
爲啥最長遞增子序列需要這種思路呢?前文說得很清楚了,因爲這樣符合歸納法,可以找到狀態轉移的關係,這裏就不具體展開了。
2、第二種思路模板是一個二維的 dp 數組:
int n = arr.length;
int[][] dp = new dp[n][n];

for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
        if (arr[i] == arr[j]) 
            dp[i][j] = dp[i][j] + ...
        else
            dp[i][j] = 最值(...)
    }
}
這種思路運用相對更多一些,尤其是涉及兩個字符串/數組的子序列,比如前文講的「最長公共子序列」。本思路中 dp 數組含義又分爲「只涉及一個字符串」和「涉及兩個字符串」兩種情況。
2.1 涉及兩個字符串/數組時(比如最長公共子序列),dp 數組的含義如下:
在子數組 arr1[0..i] 和子數組 arr2[0..j] 中,我們要求的子序列(最長公共子序列)長度爲 dp[i][j]。
2.2 只涉及一個字符串/數組時(比如本文要講的最長迴文子序列),dp 數組的含義如下:
在子數組 array[i..j] 中,我們要求的子序列(最長迴文子序列)的長度爲 dp[i][j]。

解決兩個字符串的動態規劃問題,一般都是用兩個指針 i,j 分別指向兩個字符串的最後,然後一步步往前走,縮小問題的規模

int minDistance(String s1, String s2) {//動態規劃 二維
    int m = s1.length(), n = s2.length();
    int[][] dp = new int[m + 1][n + 1];//空串算1個
    // base case 
    for (int i = 1; i <= m; i++)
        dp[i][0] = i;
    for (int j = 1; j <= n; j++)
        dp[0][j] = j;
    // 自底向上求解
    for (int i = 1; i <= m; i++)
        for (int j = 1; j <= n; j++)
            if (s1.charAt(i-1) == s2.charAt(j-1))
                dp[i][j] = dp[i - 1][j - 1];
            else               
                dp[i][j] = min(
                    dp[i - 1][j] + 1,//刪除
                    dp[i][j - 1] + 1,//插入
                    dp[i-1][j-1] + 1//替換
                );
    // 儲存着整個 s1 和 s2 的最小編輯距離
    return dp[m][n];
}

 

int minDistance(String s1, String s2) {//動態規劃 一維
    int m = s1.length(), n = s2.length();
    int[] dp = new int[n + 1];
    // base case 
    for (int j = 0; j <= n; j++)
        dp[j] = j;
    // 自底向上求解
    for (int i = 1; i <= m; i++)
        for (int j = 1,temp; j <= n; j++){
            temp=dp[j];
            if(j==1) dp[j]=i;
            else if (s1.charAt(i-1) == s2.charAt(j-1))
                dp[j] = dpij;
            else            
                dp[j] = min(
                    dp[j] + 1,//刪除
                    dp[j - 1] + 1,//插入
                    dpij+ 1);//替換
            dpij=temp;
        }
    // 儲存着整個 s1 和 s2 的最小編輯距離
    return dp[n];
}

最長公共子序列(Longest Common Subsequence,簡稱 LCS)是一道非常經典的面試題目,因爲它的解法是典型的二維動態規劃,大部分比較困難的字符串問題都和這個問題一個套路,比如說編輯距離。而且,這個算法稍加改造就可以用於解決其他問題,所以說 LCS 算法是值得掌握的。

題目就是讓我們求兩個字符串的 LCS 長度:

輸入: str1 = "abcde", str2 = "ace" 
輸出: 3  
解釋: 最長公共子序列是 "ace",它的長度是 3

 

 用兩個指針 ij 從後往前遍歷 s1s2,如果 s1[i]==s2[j],那麼這個字符一定在 lcs;否則的話,s1[i]s2[j] 這兩個字符至少有一個不在 lcs,需要丟棄一個。

對於第一種情況,找到一個 lcs 中的字符,同時將 i j 向前移動一位,並給 lcs 的長度加一;對於後者,則嘗試兩種情況,取更大的結果。

def longestCommonSubsequence(str1, str2) -> int:
    m, n = len(str1), len(str2)
    # 構建 DP table 和 base case
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    # 進行狀態轉移
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if str1[i - 1] == str2[j - 1]:
                # 找到一個 lcs 中的字符
                dp[i][j] = 1 + dp[i-1][j-1]
            else:
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])#dp[i-1][j-1]一定是最小,所以不用比較

    return dp[-1][-1]

dp[i][j]和dp[i-1][j],dp[i][j-1],dp[i-1][j-1]有關的狀態轉移方程都可以最終優化成一維dp

 

 

int longestPalindromeSubseq(string s) {//動態規劃 二維(可優化爲一維)
    int n = s.size();
    // dp 數組全部初始化爲 0
    vector<vector<int>> dp(n, vector<int>(n, 0));
    // base case
    for (int i = 0; i < n; i++)
        dp[i][i] = 1;
    // 反着遍歷保證正確的狀態轉移
    for (int i = n - 1; i >= 0; i--) {
        for (int j = i + 1; j < n; j++) {
            // 狀態轉移方程
            if (s[i] == s[j])
                dp[i][j] = dp[i + 1][j - 1] + 2;
            else
                dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
        }
    }
    // 整個 s 的最長迴文子串長度
    return dp[0][n - 1];
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章