最长公共子序列
概念
「最长公共子序列(LCS)」 是一个在一个序列集合中(通常为两个序列)用来查找所有序列中最长子序列的问题。一个数列 ,如果分别是两个或多个已知数列的子序列,且是所有符合此条件序列中最长的,则称为已知序列的最长公共子序列。这与查找「最长公共子串」问题不同的地方是:子序列不需要在原序列中占用连续的位置。
「最长公共子序列」问题存在最优子结构:这个问题可以分解成更小,更简单的“子问题”,这个子问题可以分成更多的子问题,因此整个问题就变得简单了。最长公共子序列问题的子问题的解是可以重复使用的,也就是说,更高级别的子问题通常会重用低级子问题的解。拥有这个两个属性的问题可以使用动态规划算法来解决,这样子问题的解就可以被储存起来,而不用重复计算。这个过程需要在一个表中储存同一级别的子问题的解,因此这个解可以被更高级的子问题使用。
算法
动态规划的一个计算两个序列的最长公共子序列的方法如下: 以两个序列 X、Y 为例子: 设有二维数组f[i,j] 表示 X 的 i 位和 Y 的 j 位之前的最长公共子序列的长度,则有:
- f[1][1] = same(1,1);
- f[i,j] = max{f[i-1][j-1] + same(i,j),f[i-1,j],f[i,j-1]};
same(a,b)当 X 的第 a 位与 Y 的第 b 位相同时为“1”,否则为“0”。 此时,二维数组中最大的数便是 X 和 Y 的最长公共子序列的长度,依据该数组回溯,便可找出最长公共子序列。 该算法的空间、时间复杂度均为O(n^2),经过优化后,空间复杂度可为O(n)。
过程详解
假设有字符串wordX = 'acbaed'和wordY = 'abcadf'。i、j分别表示两个字符串的循环下标,初始化矩阵如下: 且 i == 0 || j == 0 时,如下表:
| i/j | 0 | a | b | c | a | d | f |
|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| a | 0 | ||||||
| c | 0 | ||||||
| b | 0 | ||||||
| a | 0 | ||||||
| e | 0 | ||||||
| d | 0 |
双层依次循环i、j时,分别对wordX与wordY中的元素比较:
- 首先判断wordX[i - 1] === wordY[j - 1]是否相等,如果相等则f[i-1][j-1] + 1
- 否则取max(f[i-1][j],f[i][j-1])中的最大值
最终填写矩阵如下:
| i/j | 0 | a | b | c | a | d | f |
|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| a | 0 | 「1」 | 1 | 1 | 1 | 1 | 1 |
| c | 0 | 1 | 1 | 「2」 | 2 | 2 | 2 |
| b | 0 | 1 | 2 | 2 | 2 | 2 | 2 |
| a | 0 | 1 | 2 | 2 | 「3」 | 3 | 3 |
| e | 0 | 1 | 2 | 2 | 3 | 3 | 3 |
| d | 0 | 1 | 2 | 2 | 3 | 「4」 | 4 |
辅助矩阵回溯路径如下:
| i/j | 0 | a | b | c | a | d | f |
|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| a | 0 | 「diagonal」 | left | left | diagonal | left | left |
| c | 0 | top | top | 「diagonal」 | left | left | left |
| b | 0 | top | diagonal | top | top | top | top |
| a | 0 | diagonal | top | top | 「diagonal」 | left | left |
| e | 0 | top | top | top | top | top | top |
| d | 0 | top | top | top | top | 「diagonal」 | left |
代码实现
function lcs(wordX, wordY){
var m = wordX.length,
n = wordY.length,
l = [],
i, j, a, b,
solution = []; // 记录路径
// 初始化矩阵
for(i = 0; i <= m; i++){
l[i] = []
solution[i] = []
for(j = 0; j <= n; j++){
l[i][j] = []
solution[i][j] = '0'
}
}
for(i = 0; i <= m; i++){
for(j = 0; j <= n; j++){
if(i == 0 || j == 0){
l[i][j] = 0
}else if(wordX[i - 1] === wordY[j - 1]){
l[i][j] = l[i - 1][j - 1] + 1
solution[i][j] = 'diagonal'
}else{
a = l[i - 1][j]
b = l[i][j - 1]
l[i][j] = a > b ? a : b
solution[i][j] = (l[i][j] === l[i-1][j]) ? 'top' : 'left'
}
}
}
printSolution(solution, wordX, m, n)
return l[m][n]
}
function printSolution(solution, wordX, m, n){
var a = m,
b = n,
x = solution[a][b],
answer = ''
while( x !== '0'){
if(solution[a][b] === 'diagonal'){
answer = wordX[a-1] + answer
a--
b--
}else if(solution[a][b] === 'left'){
b--
}else if(solution[a][b] === 'top'){
a--
}
x = solution[a][b]
}
console.log('lcs:',answer)
}
console.log(lcs('acbaed', 'abcadf'))