LeetCode第 1143 题:最长公共子序列(C++)

1143. 最长公共子序列 - 力扣(LeetCode)

二维dp

其实第一反应是双指针分别移动比较,但是稍微细想就发现行不通,找不到清晰的移动策略:当i, j所指向字符不同的时候,可以移动 i ,也可以移动 j ,也可以都移动。

所以就会想到回溯法枚举,回溯法本质上可行,但是字符串太长的话复杂度太高,那能不能用动态规划呢?那就得先从回溯理一下思路:

回溯的思路,从 a[0]和 b[0]开始,依次考察两个字符串中的字符是否匹配:

  • 如果 a[i]与 b[j]匹配,最大公共子串长度加一,继续考察 a[i+1]和 b[j+1]
  • 如果 a[i]与 b[j]不匹配,最长公共子串长度不变,这时有两个的决策路线:
    1、删除 a[i],或者在 b[j]前面加上一个字符 a[i],然后继续考察 a[i+1]和 b[j];
    2、删除 b[j],或者在 a[i]前面加上一个字符 b[j],然后继续考察 a[i]和 b[j+1]。

反过来也就是说,如果要求 a[0…i] 和 b[0…j] 的最长公共长度 max_lcs(i, j),只可能通过下面三个状态转移过来:

  • (i-1, j-1, max_lcs),其中 max_lcs 表示 a[0…i-1]和 b[0…j-1]的最长公共子串长度
  • (i-1, j, max_lcs),其中 max_lcs 表示 a[0…i-1]和 b[0…j]的最长公共子串长度
  • (i, j-1, max_lcs),其中 max_lcs 表示 a[0…i]和 b[0…j-1]的最长公共子串长度

把这个转移过程,翻译为状态转移方程:

如果:a[i]==b[j],那么:max_lcs(i, j)就等于:
	max(max_lcs(i-1,j-1)+1, max_lcs(i-1, j), max_lcs(i, j-1));

如果:a[i]!=b[j],那么:max_lcs(i, j)就等于:
	max(max_lcs(i-1,j-1), max_lcs(i-1, j), max_lcs(i, j-1));

其中max表示求三数中的最大值。

翻译成代码,特殊处理第一行和第一列(访问不到i-1/j-1):

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m = text1.size(), n = text2.size();
        //f[i][j]表示text1的前i个字符与text2的前j个字符的最长公共子序列长度
        vector> f(m, vector(n, 0));
        if(text1[0] == text2[0])    f[0][0] = 1;
        for(int i = 1; i < n; ++i){//第0行,text1的第0个字符与text2的前i个字符的最长公共子序列长度
            if(text1[0] == text2[i])    f[0][i] = 1;
            else    f[0][i] = f[0][i-1];
        }  
        for(int i = 1; i < m; ++i){//第0列,text2的第0个字符与text1的前i个字符的最长公共子序列长度
            if(text2[0] == text1[i])    f[i][0] = 1;
            else    f[i][0] = f[i-1][0];
        }
        for(int i = 1; i< m; ++i){
            for(int j = 1; j < n; ++j){
                if(text1[i] == text2[j])    f[i][j] = max(max(f[i-1][j-1]+1, f[i-1][j]), f[i][j-1]);
                else    f[i][j] = max(max(f[i-1][j-1], f[i-1][j]), f[i][j-1]);
            }
        }
        return f[m-1][n-1];
    }
};

一维dp

如果要进行空间优化倒是很简单,因为只涉及到本行和前一行的数据,所以使用两行的滚动数组就可以,或者使用一维数组(这题的一维数组有点难度,因为很容易出错,不能轻易覆盖掉之前的值)也是可以。

注释有点多不好懂,空间占用不是很多的话其实不是那么必要。

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m = text1.size(), n = text2.size();
        vector f(n, 0);
        if(text1[0] == text2[0])    f[0] = 1;
        for(int i = 1; i < n; ++i){//第0行,text1的第0个字符与text2的前i个字符的最长公共子序列长度
            if(text1[0] == text2[i])    f[i] = 1;
            else    f[i] = f[i-1];
        }  
        //for(auto c : f) cout << c << " "; 
        //cout << endl;
        for(int i = 1; i< m; ++i){
            int pre = text1[i] == text2[0] ? 1 : f[0];//第一个值
            int cur = 0;
            for(int j = 1; j < n; ++j){
                if(text1[i] == text2[j])    cur = max(max(f[j-1]+1, f[j]), pre);
                else    cur = max(max(f[j-1], f[j]), pre);
                f[j-1] = pre;//上次的值可以填充了,因为下一轮循环上次就变为上上次,状态转移不需要上上次的值
                pre = cur;//当前值需要记录下来,不能进行填充,因为下一轮循环会用到这个位置的值,填充的话就给覆盖掉了
            }
            //填充最后一个值
            f[n-1] = pre;//这儿要用pre,不能用cur,因为可能循环没有进去。
        }
        return f[n-1];
    }
};

哨兵优化

为了让代码更加简洁,我们可以对之前二维数组的代码进行优化:

之前我们需要特殊处理第一行和第一列,但是如果使用哨兵处理,就可以将第一行/列的处理并入到后续的通用代码块。哨兵类似链表里面常用的哑巴节点,添加哨兵的目的是为了使边界条件更加容易处理:

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m = text1.size(), n = text2.size();
        //f[i][j]表示text1的前i-1个字符与text2的前j-1个字符的最长公共子序列长度
        vector> f(m+1, vector(n+1, 0));//行列均多申请一个位置
        for(int i = 1; i< m+1; ++i){
            for(int j = 1; j < n+1; ++j){
            	//注意这儿比较的是i-1和j-1的位置
                if(text1[i-1] == text2[j-1])    f[i][j] = max(max(f[i-1][j-1]+1, f[i-1][j]), f[i][j-1]);
                else    f[i][j] = max(max(f[i-1][j-1], f[i-1][j]), f[i][j-1]);
            }
        }
        return f[m][n];
    }
};

状态转移方程优化

其实上面的那个转移方程:

如果:a[i]==b[j],那么:max_lcs(i, j)就等于:
	max(max_lcs(i-1,j-1)+1, max_lcs(i-1, j), max_lcs(i, j-1));

如果:a[i]!=b[j],那么:max_lcs(i, j)就等于:
	max(max_lcs(i-1,j-1), max_lcs(i-1, j), max_lcs(i, j-1));

虽然是对的,但是不够简单,更直接的转移方程应该是这样的:

如果:a[i]==b[j],那么:max_lcs(i, j)就等于:
	max_lcs(i-1,j-1)+1;

如果:a[i]!=b[j],那么:max_lcs(i, j)就等于:
	max(max_lcs(i-1, j), max_lcs(i, j-1));

其实仔细想想就明白了,可以看动态规划之最长公共子序列(LCS) - 最长公共子序列 - 力扣(LeetCode)这位大佬的解释。

所以上面的代码这样写就可以:

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m = text1.size(), n = text2.size();
        //f[i][j]表示text1的前i-1个字符与text2的前j-1个字符的最长公共子序列长度
        vector> f(m+1, vector(n+1, 0));//行列均多申请一个位置
        for(int i = 1; i< m+1; ++i){
            for(int j = 1; j < n+1; ++j){
                if(text1[i-1] == text2[j-1])    f[i][j] = f[i-1][j-1]+1;
                else    f[i][j] = max(f[i-1][j], f[i][j-1]);
            }
        }
        return f[m][n];
    }
};

当然使用一维数组优化空间也是一样的思路:

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m = text1.size(), n = text2.size();
        vector f(n+1, 0);
        for(int i = 1; i< m+1; ++i){
            int pre = 0, cur = 0;
            for(int j = 1; j < n+1; ++j){
                if(text1[i-1] == text2[j-1])    cur = f[j-1]+1;
                else    cur = max(f[j], pre);
                f[j-1] = pre;//下轮循环上次变为上上次,状态转移不需要上上次的值(就代表可以进行更新了)
                pre = cur;//当前值需要记录,不能更新,因为下轮循环会用到
            }
            f[n] = pre;//更新每一行的最后一个值
        }
        return f[n];
    }
};

你可能感兴趣的:(leetcode)