本文转载自:进击的大前端《前端也能学算法:由浅入深讲解动态规划》–蒋鹏飞 Dennis的部分内容
一、分析
上述问题也可以用暴力穷举来求解,先列举出X字符串所有的子串,假设他的长度为m,则总共有种情况,因为对于X字符串中的每个字符都有留着和不留两种状态,m个字符的全排列种类就是种。那对应的Y字符串就有种子串, n为Y的长度。然后再遍历找出最长的公共子序列,这个复杂度非常高,我这里就不写了。
我们观察两个字符串,如果他们最后一个字符相同,则他们的LCS(最长公共子序列简写)就是两个字符串都去掉最后一个字符的LCS再加一。因为最后一个字符相同,所以最后一个字符是他们的子序列,把他去掉,子序列就少了一个,所以他们的LCS是他们去掉最后一个字符的字符串的LCS再加一。如果他们最后一个字符不相同,那他们的LCS就是X去掉最后一个字符与Y的LCS,或者是X与Y去掉最后一个字符的LCS,是他们两个中较长的那一个。写成数学公式就是:
看着这个公式,一个规模为(i, j)的问题转化为了规模为(i-1, j-1)的问题,这不就又可以用递归求解了吗?
二、代码
动态规划
递归虽然能实现我们的需求,但是复杂度实在太高,长一点的字符串需要的时间是指数级增长的。我们还是要用动态规划来求解,根据我们前面讲的动态规划原理,我们需要从小的往大的算,每算出一个值都要记下来。因为c(i, j)里面有两个变量,我们需要一个二维数组才能存下来。注意这个二维数组的行数是X的长度加一,列数是Y的长度加一,因为第一行和第一列表示X或者Y为空串的情况。代码如下:
function lcs2(str1, str2) {
let length1 = str1.length;
let length2 = str2.length;
// 构建一个二维数组
// i表示行号,对应length1 + 1
// j表示列号, 对应length2 + 1
// 第一行和第一列全部为0
let result = [];
for(let i = 0; i < length1 + 1; i++){
result.push([]); //初始化每行为空数组
for(let j = 0; j < length2 + 1; j++){
if(i === 0) {
result[i][j] = 0; // 第一行全部为0
} else if(j === 0) {
result[i][j] = 0; // 第一列全部为0
} else if(str1[i - 1] === str2[j - 1]){
// 最后一个字符相同
result[i][j] = result[i - 1][j - 1] + 1;
} else{
// 最后一个字符不同
result[i][j] = result[i][j - 1] > result[i - 1][j] ? result[i][j - 1] : result[i - 1][j];
}
}
}
console.log(result);
return result[length1][length2]
}
let result = lcs2('ABCBDAB', 'BDCABA');
console.log(result); // 4
上面的result就是我们构造出来的二维数组,对应的表格如下,每一格的值就是c(i, j),如果,则它的值就是他斜上方的值加一,如果,则它的值是上方或者左方较大的那一个。
输出最长公共子序列
要输出LCS,思路还是跟前面切钢条的类似,把每一步操作都记录下来,然后再回溯。为了记录操作我们需要一个跟result二维数组一样大的二维数组,每个格子里面的值是当前值是从哪里来的,当然,第一行和第一列仍然是0。每个格子的值要么从斜上方来,要么上方,要么左方,所以:
1. 我们用1来表示当前值从斜上方来
2. 我们用2表示当前值从左方来
3. 我们用3表示当前值从上方来
function lcs3(str1, str2) {
let length1 = str1.length;
let length2 = str2.length;
// 构建一个二维数组
// i表示行号,对应length1 + 1
// j表示列号, 对应length2 + 1
// 第一行和第一列全部为0
let result = [];
let comeFrom = []; // 保存来历的数组
for(let i = 0; i < length1 + 1; i++){
result.push([]); //初始化每行为空数组
comeFrom.push([]);
for(let j = 0; j < length2 + 1; j++){
if(i === 0) {
result[i][j] = 0; // 第一行全部为0
comeFrom[i][j] = 0;
} else if(j === 0) {
result[i][j] = 0; // 第一列全部为0
comeFrom[i][j] = 0;
} else if(str1[i - 1] === str2[j - 1]){
// 最后一个字符相同
result[i][j] = result[i - 1][j - 1] + 1;
comeFrom[i][j] = 1; // 值从斜上方来
} else if(result[i][j - 1] > result[i - 1][j]){
// 最后一个字符不同,值是左边的大
result[i][j] = result[i][j - 1];
comeFrom[i][j] = 2;
} else {
// 最后一个字符不同,值是上边的大
result[i][j] = result[i - 1][j];
comeFrom[i][j] = 3;
}
}
}
console.log(result);
console.log(comeFrom);
// 回溯comeFrom数组,找出LCS
let pointerI = length1;
let pointerJ = length2;
let lcsArr = []; // 一个数组保存LCS结果
while(pointerI > 0 && pointerJ > 0) {
console.log(pointerI, pointerJ);
if(comeFrom[pointerI][pointerJ] === 1) {
lcsArr.push(str1[pointerI - 1]);
pointerI--;
pointerJ--;
} else if(comeFrom[pointerI][pointerJ] === 2) {
pointerI--;
} else if(comeFrom[pointerI][pointerJ] === 3) {
pointerJ--;
}
}
console.log(lcsArr); // ["B", "A", "D", "B"]
//现在lcsArr顺序是反的
lcsArr = lcsArr.reverse();
return {
length: result[length1][length2],
lcs: lcsArr.join('')
}
}
let result = lcs3('ABCBDAB', 'BDCABA');
console.log(result); // {length: 4, lcs: "BDAB"}