题目分析:
首先需要理解题目中几个名词的含义。
(1)子序列的含义:一个序列S,任意删除若干个(可为0个)字符后得到的序列C,则C称为S的子序列
(2)最长公共子序列定义为两个序列的公共子序列中最长的一个或若干个。
(3)任意两个序列X和Y,至少存在一个公共子序列"空",即‘’。
假设已知给定的序列为X和Y,长度分别为m和n。
解法1:暴力穷举法
思路:因为要求的是两个串X和Y的最长公共子序列,因此最直观的方法是分别取出X的所有子序列与Y的所有子序列进行对比,找出相同且最长的那个。
时间复杂度分析:序列X共有: 2 m 2^{m} 2m 个 子 序 列 , 序 列 Y 共 有 个子序列,序列Y共有 个子序列,序列Y共有 2 n 2^{n} 2n个子序列,因为需要两者所有的子序列进行比对,所以需要比对 2 m + n 2^{m+n} 2m+n次,所以暴力穷举法的时间复杂度为O( 2 m + n 2^{m+n} 2m+n)。
空间复杂度分析:因为暴力穷举法只是从原序列X和Y中取出子序列进行比对,所以最多只需要m+n个空间即可,因此空间复杂度为O(m+n)
结论:时间复杂度为指数级,不可接受。
解法2:动态规划+备忘录法
首先需要引入几个记号:
(1) 字符串X,长度为m,下标从1开始。
(2) 字符串Y,长度为n,下标从1开始。
(3) X i = < x 1 , x 2 , . . . , x i > X_i = <x_1,x_2,...,x_i> Xi=<x1,x2,...,xi> 表示X的前i个字符组成的前缀。
(4) Y j = < y 1 , y 2 , . . . , y j > Y_j = <y_1,y_2,...,y_j> Yj=<y1,y2,...,yj> 表示Y的前j个字符组成的前缀。
(5) LCS(X,Y) 记为X和Y 的最长公共子序列。
思路:首先,仅分析X和Y的最后一个字符,分别为 X m X_m Xm和 Y n Y_n Yn,则存在两种情况:
(1)若 X m = Y n X_m=Y_n Xm=Yn,则最长公共子序列LCS必然包含 X m X_m Xm,因此
L C S ( X m , Y n ) = L C S ( X m − 1 , Y n − 1 ) + X m LCS(X_m,Y_n) = LCS(X_{m-1},Y_{n-1})+X_m LCS(Xm,Yn)=LCS(Xm−1,Yn−1)+Xm
(2)若 X m < > Y n X_m<>Y_n Xm<>Yn,则最长公共子序列LCS等于:
L C S ( X m , Y n ) = m a x ( L C S ( X m , Y n − 1 ) , L C S ( X m − 1 , Y n ) ) LCS(X_m,Y_n) = max(LCS(X_{m},Y_{n-1}),LCS(X_{m-1},Y_{n})) LCS(Xm,Yn)=max(LCS(Xm,Yn−1),LCS(Xm−1,Yn))
因此经上分析,可得
L C S ( X m , Y n ) = { L C S ( X m − 1 , Y n − 1 ) + X m , X m = Y n m a x ( L C S ( X m , Y n − 1 ) , L C S ( X m − 1 , Y n ) ) , X m < > Y n LCS(X_m,Y_n) = \left\{ \begin{array}{c} LCS(X_{m-1},Y_{n-1})+X_m , X_m=Y_n \\ max(LCS(X_{m},Y_{n-1}),LCS(X_{m-1},Y_{n})), X_m<>Y_n\\ \end{array}\right. LCS(Xm,Yn)={LCS(Xm−1,Yn−1)+Xm,Xm=Ynmax(LCS(Xm,Yn−1),LCS(Xm−1,Yn)),Xm<>Yn
至此,可以发现上面的关系是经典的动态规划问题,也是状态转移方程。
到这一步思路已经清楚了,接下来就是coding的部分。那怎么实现这个算法呢?因为有了状态转移方程,当然可以直接上递归来编写代码,但是如果仅使用递归而不借助其它优化方法,则算法退化为了暴力方法,时间复杂度仍为O( 2 m + n 2^{m+n} 2m+n),而空间复杂度会更高。因此我们可以换种思路,可以使用长度数组来记录 L C S ( X i , Y j ) LCS(X_i,Y_j) LCS(Xi,Yj),从前到后依次计算,具体步骤如下。
(1)申请一个二维数组N,大小为 ( m + 1 ) ∗ ( n + 1 ) (m+1)*(n+1) (m+1)∗(n+1)。
(2)对于二维数组N,则N中任意位置的值
c ( i , j ) = { 0 , i = 0 或 j = 0 c ( i − 1 , j − 1 ) + 1 , X i = Y j m a x ( c ( i , j − 1 ) , c ( i − 1 , j ) ) , X i < > Y j c(i,j)= \left\{ \begin{array}{c} 0 , i=0 或 j=0 \\ c(i-1,j-1)+1, X_i=Y_j\\ max(c(i,j-1),c(i-1,j)), X_i<>Y_j \end{array}\right. c(i,j)=⎩⎨⎧0,i=0或j=0c(i−1,j−1)+1,Xi=Yjmax(c(i,j−1),c(i−1,j)),Xi<>Yj
注:因为当i=0或者j=0时,其中一个串为空串,因此其最长公共子串也为空串,因此长度为0;
我们只需要做的是从上到下,从左到右将N中的值计算出来,最后计算出的 N i , j N_{i,j} Ni,j即为X和Y的最长公共子序列。
现在,从N中已经计算出了最长公共子序列的长度,但是我们需要的是返回最长公共子序列,是一个序列,因此还需要有一个结构来记录哪些字符组成了这个序列。我们仍然使用一个大小为(m+1)*(n+1)的二维数组H,用来记录N中每个位置的值是从哪个方向计算得到的。则H中每一个位置的值:
h ( i , j ) = { l e f t t o p , X i = Y j l e f t , h ( i , j − 1 ) > h ( i − 1 , j ) 并 且 X i < > Y j t o p , h ( i , j − 1 ) < h ( i − 1 , j ) 并 且 X i < > Y j h(i,j)= \left\{ \begin{array}{c} lefttop, X_i=Y_j\\ left,h(i,j-1)>h(i-1,j) 并且X_i<>Y_j\\ top,h(i,j-1)<h(i-1,j) 并且X_i<>Y_j \end{array}\right. h(i,j)=⎩⎨⎧lefttop,Xi=Yjleft,h(i,j−1)>h(i−1,j)并且Xi<>Yjtop,h(i,j−1)<h(i−1,j)并且Xi<>Yj
最终,依次遍历H数组,输出所有值为lefttop位置所对应的字符,即为最长公共子序列。
**时间复杂度分析:**整个计算过程只是将N和H的值从左到右、从上到下遍历一遍即可,所以需要 2 m ∗ n 2m*n 2m∗n个时间单位,故时间复杂度为 O ( m ∗ n ) O(m*n) O(m∗n)
**空间复杂度分析:**整个计算过程只申请了2个大小为 ( m + 1 ) ∗ ( n + 1 ) (m+1)*(n+1) (m+1)∗(n+1)的二维数组,因此空间复杂度也为 O ( m ∗ n ) O(m*n) O(m∗n)。
接下来附上计算N和H的python代码:
// An highlighted block
author: shuaifeng
"""
def LCS(strA,strB):
lenA = len(strA)
lenB = len(strB)
N = [[0]*(lenB+1) for i in range(lenA+1)]
H = [[0]*(lenB+1) for i in range(lenA+1)]
for i in range(1,lenA+1):
for j in range(1,lenB+1):
if strA[i-1] == strB[j-1]:
N[i][j] = N[i-1][j-1]+1
H[i][j] = 'leftTop'
elif strA[i-1] !=strB[j-1] and N[i][j-1] >= N[i-1][j]:
N[i][j] = N[i][j-1]
H[i][j] = 'left'
else:
N[i][j] = N[i-1][j]
H[i][j] = 'top'
print(N)
print(H)
def main():
strA = 'ABDFWEGLASDFV'
strB = 'ABSFEV'
print(LCS(strA,strB))
if __name__=='__main__':
main()