两字符串最值问题->动态规划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];
}

 

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