「这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战」
最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。 两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:
输入: text1 = "abcde", text2 = "ace"
输出: 3
解释: 最长公共子序列是 "ace" ,它的长度为 3 。
示例 2:
输入: text1 = "abc", text2 = "abc"
输出: 3
解释: 最长公共子序列是 "abc" ,它的长度为 3 。
示例 3:
输入: text1 = "abc", text2 = "def"
输出: 0
解释: 两个字符串没有公共子序列,返回 0 。
提示:
1 <= text1.length, text2.length <= 1000text1和text2仅由小写英文字符组成。
动态规划 巧妙处理边界
思路
这里我们要用二维数组来保存不同长度时两个字符串的最长公共子序列
dp[i][j]表示 text1.substr(0,i)和text2.substr(0,j) 两个子串之间的最长公共自序列的结果(即截取text1的前i位,和text2的前j位)
声明text1的长度位m,text2的长度位n,那么我们要遍历整个字符串就是i等于从1到m,j等于从1到n
text1的每一位就是text1[i-1](text1字符串的最后一位就是其length-1位,所以是text1[i-1] ),text2同理为text2[j-1]
-
边界处理:当i=0或者j=0时,text1.substr(0,i),text1.substr(0,j)为空字符串'',不能算作子序列,所以结果都为dp[0][j]和dp[i][0] 的结果都为0
-
状态转移:声明dp数组,用来保存dp[i][j]的最长公共子序列(text1的前i位字符串和text2的前j位字符串的最长公共子序列)
动态规划中,i 和 j 较大的值 , 依赖 i 和 j 较小的值 的结果。 所以我们在比较两个字符串的时候从最后一位来看存在两种结果,即相等或者不等
如果末尾相等:相当于将text1.substr(0,i-1)和text2.substr(0,j-1) 两个字符串末尾加上同样的字符,那么肯定会导致两个字符串的公共子序列长度增加一位,自然得出dp[i][j]等于dp[i-1][j-1]的结果+1
如果末尾不相等:那么两个末节点至少有一个不属于最长递增子序列,那么就是去掉其实一个字符串的末尾节点对最长公共子序列是没有影响的。
//举个例子:
//假设str1 = text1.substr(0,i)
//假设str2 = text2.substr(0,j)
str1 = 'abcd'
str2 = 'efghc'
//1
//经过比较,末尾不相等
//那么str1和str2的最长公共子序列与str3和str4的或者与str5和str6的结果相等
str3 = 'abc'
str4 = 'efghc'
//1
str3 = 'abcd'
str4 = 'efgh'
//0
//回到我们的推论上:去掉其实一个字符串的末尾节点对最公共长子序列是没有影响的
//很明显这里去掉str1的末尾字符对str1和str2的最长公共子序列是没有影响的
而dp[i][j] 有两种方式到达这里,一种是text1.substr(0,i-1)和text2.substr(0,j),然后text1加上一个字符串到达dp[i][j] ;或者是text1.substr(0,i)和text2.substr(0,j-1),然后 text2加上一个字符串到达dp[i][j];所以 dp[i][j]就等于两者中的较大值,这样可得递推公式
- 递推公式:
- text1[i] === text2[j] dp[i][j] = dp[i-1][j-1]+1
- text1[i] !== text2[j] dp[i][j] = Math.max(dp[i-1][j],p[i][j-1])
var longestCommonSubsequence = function (text1, text2) {
const m = text1.length, n = text2.length;
const dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
const c1 = text1[i - 1];
for (let j = 1; j <= n; j++) {
const c2 = text2[j - 1];
if (c1 === c2) {
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二维数组,我们可以把它看成一个矩形网格,i和j分别代表纵坐标和横坐标。
我们每次计算下一个位置的时候顶多会依赖上边,左边或者左上方的数据。所以我们只需要两行数据就可以了,其实就是两个和数组嘛
上面的就叫pre 当前正在计算的就叫curr
那么我们直接吧代码中的dp干掉,用我们的curr来代替dp[i] ,用我们的prev来代替dp[i-1],然后再第一层循环结束时,下次的prev=curr,curr等于空数组 这样降低空间复杂度了
最后curr还是空 prev保存着最后的结果,直接返回prev的最后一位即可
var longestCommonSubsequence = function (text1, text2) {
const m = text1.length, n = text2.length;
var pre = new Array(n + 1).fill(0)
var curr = new Array(n + 1).fill(0)
for (let i = 1; i <= m; i++) {
const c1 = text1[i - 1];
for (let j = 1; j <= n; j++) {
const c2 = text2[j - 1];
if (c1 === c2) {
curr[j] = pre[j - 1] + 1;
} else {
curr[j] = Math.max(pre[j], curr[j - 1]);
}
}
pre = curr
curr = new Array(n + 1).fill(0)
}
return pre[pre.length-1];
}