文章目录
- 最长公共子序列(LCS)
 - 编辑距离(Edit Distance)
 - 总结
 - 相关题目练习
 - 583. 两个字符串的删除操作 https://leetcode.cn/problems/delete-operation-for-two-strings/
 - 712. 两个字符串的最小ASCII删除和 https://leetcode.cn/problems/minimum-ascii-delete-sum-for-two-strings/
 - 解法1:求编辑距离
 - 解法2:求最长子序列
 
- 1458. 两个子序列的最大点积 https://leetcode.cn/problems/max-dot-product-of-two-subsequences/
 - 97. 交错字符串 https://leetcode.cn/problems/interleaving-string/
 
本文记录这种两个序列之间相互比较的 dp 题目的通用模板。
 
 
最长公共子序列(LCS)
1143. 最长公共子序列
 
 
 递推公式如下
 
 为了避免出现负数下标,因此定义 dp 数组时定义成 dp[m + 1][n + 1]。
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int m = text1.length(), n = text2.length();
        int[][] dp = new int[m + 1][n + 1];
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (text1.charAt(i - 1) == text2.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1] + 1;
                else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
        return dp[m][n];
    }
}
 
可以优化成一维 dp 数组。
因为 dp[i][j] 的取值只和 左上角,上面 和 左边 这三个位置的数字有关,只需要引入一个额外的变量 pre 记录一下遍历过程中被覆盖的 dp[j]作为下一次左上角的值 就好了。
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int m = text1.length(), n = text2.length();
        int[] dp = new int[n + 1];
        for (int i = 1; i <= m; ++i) {
            int pre = dp[0];
            for (int j = 1; j <= n; ++j) {
                int tmp = dp[j];
                dp[j] = text1.charAt(i - 1) == text2.charAt(j - 1)? pre + 1: Math.max(dp[j], dp[j - 1]);
                pre = tmp;
            }
        }
        return dp[n];
    }
}
 
编辑距离(Edit Distance)
72. 编辑距离

 先定义 dp[i][j] 表示 word1 从0~i,word2 从0~j 需要的最少操作数。
递推公式如下:
 
 min() 中的三个 分别对应 删除、添加、替换 这三种操作。
class Solution {
    public int minDistance(String word1, String word2) {
        int m = word1.length(), n = word2.length();
        int[][] dp = new int[m + 1][n + 1];
        // dp数组初始化
        for (int i = 0; i <= m; ++i) dp[i][0] = i;
        for (int j = 0; j <= n; ++j) dp[0][j] = j;
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1];
                else dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i][j - 1])) + 1;
            }
        }
        return dp[m][n];
    }
}
 
继续优化成一维 dp 数组
class Solution {
    public int minDistance(String word1, String word2) {
        int m = word1.length(), n = word2.length();
        int[] dp = new int[n + 1];
        for (int j = 0; j <= n; ++j) dp[j] = j;
        for (int i = 1; i <= m; ++i) {
            dp[0] = i;				// 注意这里的dp[0]要随着i的变化更新
            int pre = dp[0] - 1;    // 引入额外的pre记录被覆盖的dp[i-1][j-1]
            for (int j = 1; j <= n; ++j) {
                int tmp = dp[j];
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) dp[j] = pre;
                else dp[j] = Math.min(pre, Math.min(dp[j], dp[j - 1])) + 1;
                pre = tmp;
            }
        }
        return dp[n];
    }
}
 
总结
这两道题目的类似点在于都是比较两个序列之间的内容,这种情况下通常定义 dp 数组为:
 dp[m + 1][n + 1] (之所以 + 1 是为了方式遍历的过程中出现负数下标)
与之对应,遍历元素的时候,使用的下标分别是 i - 1 和 j - 1。(因为原始数据的数据范围还是 0 ~ m 和 0~ n)
由于无后效性,即 dp[i][j] 的数值只与和它接近的几个数字有关,因此可以优化 dp 数组的空间。
相关题目练习
583. 两个字符串的删除操作 https://leetcode.cn/problems/delete-operation-for-two-strings/
https://leetcode.cn/problems/delete-operation-for-two-strings/
 
 和 72. 编辑距离 这道题目很像,唯一的区别在于,可以使用的操作只有删除任意一个字符串中的一个元素。
class Solution {
    public int minDistance(String word1, String word2) {
        int m = word1.length(), n = word2.length();
        int[][] dp = new int[m + 1][n + 1];
        // dp数组初始化
        for (int i = 0; i <= m; ++i) dp[i][0] = i;
        for (int j = 0; j <= n; ++j) dp[0][j] = j;
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1];
                else dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + 1;
            }
        }
        return dp[m][n];
    }
}
 
直接把 72. 编辑距离 这道题目的答案复制过来,删除 dp[i - 1][j - 1] 到 dp[i][j] 的转移即可(对应着这道题没有替换元素的操作)
712. 两个字符串的最小ASCII删除和 https://leetcode.cn/problems/minimum-ascii-delete-sum-for-two-strings/
https://leetcode.cn/problems/minimum-ascii-delete-sum-for-two-strings/

解法1:求编辑距离
同样是只有删除操作的编辑距离,除此之外,每次操作需要的花费变成了字符的 ASCII 码。
class Solution {
    public int minimumDeleteSum(String s1, String s2) {
        int m = s1.length(), n = s2.length();
        int[][] dp = new int[m + 1][n + 1];
        for (int i = 1; i <= m; ++i) dp[i][0] = dp[i - 1][0] + s1.charAt(i - 1);
        for (int j = 1; j <= n; ++j) dp[0][j] = dp[0][j - 1] + s2.charAt(j - 1);
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                char a = s1.charAt(i - 1), b = s2.charAt(j - 1);
                if (a == b) dp[i][j] = dp[i - 1][j - 1];
                else dp[i][j] = Math.min(dp[i - 1][j] + a, dp[i][j - 1] + b);
            }
        }
        return dp[m][n];
    }
}
 
解法2:求最长子序列
这一题乍一看和上一题很像! 但其实应该反过来想,这道题求的也可以是最长公共子序列,这样剩下的就是需要被删除的字符了。
class Solution {
    public int minimumDeleteSum(String s1, String s2) {
        int m = s1.length(), n = s2.length(), sum = 0;
        int[][] dp = new int[m + 1][n + 1];
        for (char ch: s1.toCharArray()) sum += ch;
        for (char ch: s2.toCharArray()) sum += ch;
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                char a = s1.charAt(i - 1), b = s2.charAt(j - 1);
                if (a == b) dp[i][j] = dp[i - 1][j - 1] + a + b;
                else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
        return sum - dp[m][n];
    }
}
 
1458. 两个子序列的最大点积 https://leetcode.cn/problems/max-dot-product-of-two-subsequences/
https://leetcode.cn/problems/max-dot-product-of-two-subsequences/

 这道题目有两点需要注意,子序列要求:1.长度相同 2.非空。
这道题目一开始写成了:
class Solution {
    public int maxDotProduct(int[] nums1, int[] nums2) {
        int m = nums1.length, n = nums2.length;
        int[][] dp = new int[m + 1][n + 1];
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                int v = nums1[i - 1] * nums2[j - 1];
                dp[i][j] = Math.max(v + dp[i - 1][j - 1], dp[i][j]);
            }
        }
        return dp[m][n];
    }   
}
 
结果发现答案要求是非空的,也就是如果只有负数的话,那没办法,负数也得选进去。
正解如下:
class Solution {
    public int maxDotProduct(int[] nums1, int[] nums2) {
        int m = nums1.length, n = nums2.length;
        int[][] dp = new int[m + 1][n + 1];
        for (int i = 0; i <= m; ++i) Arrays.fill(dp[i], Integer.MIN_VALUE / 2);	// 因为负数也得选,就先都设成一个比较小的数字
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                dp[i][j] = nums1[i - 1] * nums2[j - 1]; // 至少也得选一个
                dp[i][j] = Math.max(Math.max(dp[i - 1][j], dp[i][j - 1]), dp[i][j]);
                dp[i][j] = Math.max(nums1[i - 1] * nums2[j - 1] + dp[i - 1][j - 1], dp[i][j]);
            }
        }
        return dp[m][n];
    }
}
 
这道题目做起来手感怪怪的。
97. 交错字符串 https://leetcode.cn/problems/interleaving-string/
https://leetcode.cn/problems/interleaving-string/
 
 dp[i][j] 表示 s1 的前 i 个元素 和 s2 的前 j 个元素 能否组成 s3 的前 i + j 个元素。
class Solution {
    public boolean isInterleave(String s1, String s2, String s3) {
        int m = s1.length(), n = s2.length(), t = s3.length();
        if (m + n != t) return false;
        boolean[][] dp = new boolean[m + 1][n + 1];     // 表示能组合成的序列长度
        dp[0][0] = true;
        for (int i = 0; i <= m; ++i) {
            for (int j = 0; j <= n; ++j) {
                if (i > 0 && s1.charAt(i - 1) == s3.charAt(i + j - 1)) dp[i][j] |= dp[i - 1][j];
                if (j > 0 && s2.charAt(j - 1) == s3.charAt(i + j - 1)) dp[i][j] |= dp[i][j - 1];
            }
        }
        return dp[m][n];
    }
}
 
注意这个时候 i 和 j 的 for 循环的起始点都是 0 而不是 1 了。
关于 s3 的下标为什么是 i + j - 1。举个例子,当 i = 1, j = 1,此时对应的两个下标都是0,那么这两个合起来之后对应 s3 的下标是 i + j - 2 和 i + j - 1。



















