首先要理解公共子串和公共子序列
公共子串和公共子序列是两个不相同的概念, 公共子串要求字符连续, 公共子序列不要求字符连续。
比如: 字符串"ascde", “axcxdde”
最长公共子串是 “de” , 因为 "de"在两个字符串中都是连续的
最长公共子序列是 “acde”, 不要求连续, 只要满足两个字符串中子序列前后顺序的一致即可
动态规划求最长公共子序列
动态规划的核心是
- base case
- 状态转移方程
- 自下而上
我们可以通过状态转移表来帮助我们得出状态转移方程
这里我直接说结论:
我们思考一个问题, 如果我们求 字符串 s1的前 i 位子串和 s2的前 j 位子串的最长公共子序列 f(i,j), 跟我们求 s1的前 i-1 位子串和 s2的前 j-1 位子串的最长公共子序列 f(i-1,j-1) 两者之间有什么关系呢?
或者换一种说法, 如果我们知道的 f(i-1,j-1) 的结果 x, 我们如何根据 x 推得 f(i,j) 呢?
其结论就是, 我们判断 s1.charAt(i) 和 s2.charAt(j) 是否相等, 如果相等, 则 f(i,j) = f(i-1, j-1) + 1;
如果不相等, f(i,j) = max(f(i-1, j), f(i, j-1));
这个结论我就不证明了, 这不需要我们开发工程师深入探索。
如果你理解了上面的公式, 我们就可以进入编码阶段。
代码实现有两种思路, 一是回溯, 二是动态规划
回溯就是所谓的自上而下, 遇到分支的时候去探索每一个分支, 当遇到末尾的时候回溯到上一分支。动态规划是所谓的自下而上, 先解决子问题, 再根据子问题的结果去解决父问题。动态规划的优势在于, 通过备忘录避免对子问题的重复求解, 缺点和很显然, 用空间换时间, 占用内存。
这里我们用动态规划实现
首先我们需要解决 base case, 我的理解就是边界条件, 也就是 i=0 或者 j=0时的情况
很显然, i=0时, 我们遍历j, 当s1.charAt(i) == s2.charAt(j) 时, f(0,j)=1, j=0时同理
for (int i = 0; i < s1.length(); i++) {
if (s1.charAt(i) == s2.charAt(0)) {
arr[i][0] = 1;
max = 1;
}
}
for (int j = 0; j < s2.length(); j++) {
if (s1.charAt(0) == s2.charAt(j)) {
arr[0][j] = 1;
max = 1;
}
}
我们把子问题的解缓存到 int[i][j] 的数组arr中, 避免重复求解
整体的题解如下
public class Test2 {
static String s1 =