leetCode 1143.最长公共子序列 动态规划 + 滚动数组-CSDN博客https://blog.csdn.net/weixin_41987016/article/details/133689692?spm=1001.2014.3001.5501大家可以在我上期的文章中看此题目,接下来具体来详细表述如何一步步思考动态规划,以及优化空间复杂度🤔
一、递归搜索 + 保存计算结果 = 记忆化搜索
把这两个字符串分别叫做 s 和 t ,长度设为 n 和 m ,和背包问题一样。子序列也相当于是考虑每个字母, 你是选还是不选 ,从最后一个字母 开始考虑的话,两两组合就有这四种情况了 ,比如都不选,那么问题就从n个字母 + m个字母的原问题变成n-1个字母+m-1个字母的子问题了。那再一般化,考虑 s[i] 和 t[j] 选或不选 ,这样就确定了递归参数中的 i 和 j 表示的子问题 。就是 s 的前 i 个字母和 t 的前 j 个字母的LCS长度,那根据选或不选,就可以得到这样的一些子问题了。注意都选和都不选,它们的子问题是一样的 ,只有**s[i] = t[j]**的时候才能都选,当然这种情况下都选肯定是比都不选要好的,这样就可以得到两个式子了。
>>思考两个问题(O_O)?:
- 第一个问题:在s[i] = t[j]的时候,我们需要考虑只选其中一个的情况吗?
- 第二个问题:在s[i] ≠ t[j]的时候,我们需要考虑都不选的情况吗?
第一个问题,比如s=abcdc和t=abc,此时s和t的c是相等的 ,那么都选就变成它们的LCS长度+1了,把它的LCS长度记作x,即x = dfs(i-1,j-1)。如果不选s中的c的话,就变成 abcd, abc这种情况,如果它更优,就表示dfs(i-1,j)比x+1还要大,即dfs(i-1,j) > x+1,那么这两个c匹配的话,再把他两去掉,就变成了abd和ab。根据前面的假设,abd和ab的LCS长度是大于x的,同时由于这两个又是abcd和ab的子序列,所以它们两的LCS长度又是小于等于x的,这样就矛盾了。所以,dfs(i-1,j)它是小于等于x+1的,对于dfs(i,j-1)也同理。所以在s[i]=t[j]的时候,我们只需要考虑都选的情况就好了。
第二个问题,在s[i] ≠ t[j]的时候,需不需要调用dfs(i-1,j-1)呢?**其实是不需要的,因为在dfs(i-1,j)里面,如果它不选t[j]的话,那就回递归到dfs(i-1,j-1)了,那相当于dfs(i-1,j-1)的结果,已经在dfs(i-1,j)里面了,**或者说dfs(i-1,j)是大于等于dfs(i-1,j-1)的。那对于dfs(i,j-1)也是同理的,所以我们不需要递归到dfs(i-1,j-1)这里,那最终就可简化成这个式子了。
cpp
class Solution {
public:
int longestCommonSubsequence(string s, string t) {
int n = s.length(), m = t.length(), cache[n][m];
memset(cache, -1, sizeof(cache)); // -1 表示没有访问过
function<int(int, int)> dfs = [&](int i, int j) -> int {
if (i < 0 || j < 0) return 0;
int &res = cache[i][j];
if (res != -1) return res;
if (s[i] == t[j]) return res = dfs(i - 1, j - 1) + 1;
return res = max(dfs(i - 1, j), dfs(i, j - 1));
};
return dfs(n - 1, m - 1);
}
};
作者:灵茶山艾府
链接:https://leetcode.cn/problems/longest-common-subsequence/solutions/2133188/jiao-ni-yi-bu-bu-si-kao-dong-tai-gui-hua-lbz5/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
- 时间复杂度:O(nm)
- 空间复杂度:O(nm)
二、翻译成递推
cpp
class Solution {
public:
int longestCommonSubsequence(string s, string t) {
int n = s.length(), m = t.length(), f[n + 1][m + 1];
memset(f, 0, sizeof(f));
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j)
f[i + 1][j + 1] = s[i] == t[j] ? f[i][j] + 1 : max(f[i][j + 1], f[i + 1][j]);
return f[n][m];
}
};
作者:灵茶山艾府
链接:https://leetcode.cn/problems/longest-common-subsequence/solutions/2133188/jiao-ni-yi-bu-bu-si-kao-dong-tai-gui-hua-lbz5/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
- 时间复杂度:O(nm)
- 空间复杂度:O(m)
三、空间优化:两个数组(滚动数组)
cpp
class Solution {
public:
int longestCommonSubsequence(string s,string t) {
int n = s.length(),m = t.length(),f[2][m+1];
memset(f,0,sizeof(f));
for(int i=0;i<n;++i) {
for(int j=0;j<m;++j) {
if(s[i] == t[j]) f[(i+1)%2][j+1] = f[i%2][j] + 1;
else f[(i+1)%2][j+1] = max(f[i%2][j+1],f[(i+1)%2][j]);
}
}
return f[n%2][m];
}
};
四、空间优化:一个数组(滚动数组,优化空间复杂度)
从这个递推式可看出,对于每个状态,只需要知道它左边,上面和左上,这三个相邻方向的状态 。那么为了在递推的过程中,这三个方向都是经过计算的数值 ,所以要 从前向后 , 从上到下 来遍历这个矩阵。
如果只有一个一维数组,那么再算当前行的时候,它的左上这个状态就会被之前的计算给覆盖掉,那如何解决这个问题呢?我们可以用一个临时变量**pre(previos)**把它记录起来。
cpp
class Solution {
public:
// 滚动数组
int longestCommonSubsequence(string s,string t) {
int m = t.length(),f[m+1];
memset(f,0,sizeof(f));
for(char x:s){
for(int j=0,pre=0;j<m;++j) {
int tmp = f[j+1];
if(x == t[j]) f[j+1] = pre+1;
else f[j+1] = max(f[j+1],f[j]);
pre = tmp;
}
}
return f[m];
}
};
- 时间复杂度:O(nm)
- 空间复杂度:O(m)
参考文章和视频: