最长公共子序列

446 阅读3分钟

最长公共子序列

概念

最长公共子序列(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'))

参考资料

JavaScript数据结构与算法