求两个字符串的最长公共子序列Longest Common Sequence(LCS)

本文深入解析了求解最长公共子序列问题的两种方法:暴力穷举法和动态规划法。通过对比,阐述了动态规划法在效率上的显著优势,并提供了详细的Python代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目分析
首先需要理解题目中几个名词的含义。
(1)子序列的含义:一个序列S,任意删除若干个(可为0个)字符后得到的序列C,则C称为S的子序列
(2)最长公共子序列定义为两个序列的公共子序列中最长的一个或若干个。
(3)任意两个序列X和Y,至少存在一个公共子序列"空",即‘’。

假设已知给定的序列为X和Y,长度分别为m和n。
解法1:暴力穷举法
思路:因为要求的是两个串X和Y的最长公共子序列,因此最直观的方法是分别取出X的所有子序列与Y的所有子序列进行对比,找出相同且最长的那个。
时间复杂度分析:序列X共有:2m2^{m}2m个子序列,序列Y共有个子序列,序列Y共有Y2n2^{n}2n个子序列,因为需要两者所有的子序列进行比对,所以需要比对2m+n2^{m+n}2m+n次,所以暴力穷举法的时间复杂度为O(2m+n2^{m+n}2m+n)。
空间复杂度分析:因为暴力穷举法只是从原序列X和Y中取出子序列进行比对,所以最多只需要m+n个空间即可,因此空间复杂度为O(m+n)
结论:时间复杂度为指数级,不可接受。

解法2:动态规划+备忘录法
首先需要引入几个记号:
(1) 字符串X,长度为m,下标从1开始。
(2) 字符串Y,长度为n,下标从1开始。
(3) Xi=&lt;x1,x2,...,xi&gt;X_i = &lt;x_1,x_2,...,x_i&gt;Xi=<x1,x2,...,xi> 表示X的前i个字符组成的前缀。
(4) Yj=&lt;y1,y2,...,yj&gt;Y_j = &lt;y_1,y_2,...,y_j&gt;Yj=<y1,y2,...,yj> 表示Y的前j个字符组成的前缀。
(5) LCS(X,Y) 记为X和Y 的最长公共子序列。
思路:首先,仅分析X和Y的最后一个字符,分别为XmX_mXmYnY_nYn,则存在两种情况:
(1)若Xm=YnX_m=Y_nXm=Yn,则最长公共子序列LCS必然包含XmX_mXm,因此
LCS(Xm,Yn)=LCS(Xm−1,Yn−1)+XmLCS(X_m,Y_n) = LCS(X_{m-1},Y_{n-1})+X_mLCS(Xm,Yn)=LCS(Xm1,Yn1)+Xm
(2)若Xm&lt;&gt;YnX_m&lt;&gt;Y_nXm<>Yn,则最长公共子序列LCS等于:
LCS(Xm,Yn)=max(LCS(Xm,Yn−1),LCS(Xm−1,Yn))LCS(X_m,Y_n) = max(LCS(X_{m},Y_{n-1}),LCS(X_{m-1},Y_{n}))LCS(Xm,Yn)=maxLCS(Xm,Yn1)LCS(Xm1,Yn)
因此经上分析,可得
LCS(Xm,Yn)={LCS(Xm−1,Yn−1)+Xm,Xm=Ynmax(LCS(Xm,Yn−1),LCS(Xm−1,Yn)),Xm&lt;&gt;YnLCS(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&lt;&gt;Y_n\\ \end{array}\right. LCS(Xm,Yn)={LCS(Xm1,Yn1)+Xm,Xm=YnmaxLCS(Xm,Yn1)LCS(Xm1,Yn),Xm<>Yn
至此,可以发现上面的关系是经典的动态规划问题,也是状态转移方程。
到这一步思路已经清楚了,接下来就是coding的部分。那怎么实现这个算法呢?因为有了状态转移方程,当然可以直接上递归来编写代码,但是如果仅使用递归而不借助其它优化方法,则算法退化为了暴力方法,时间复杂度仍为O(2m+n2^{m+n}2m+n),而空间复杂度会更高。因此我们可以换种思路,可以使用长度数组来记录LCS(Xi,Yj)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=0c(i−1,j−1)+1,Xi=Yjmax(c(i,j−1),c(i−1,j)),Xi&lt;&gt;Yjc(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&lt;&gt;Y_j \end{array}\right. c(i,j)=0,i=0j=0c(i1,j1)+1,Xi=Yjmax(c(i,j1),c(i1,j)),Xi<>Yj
注:因为当i=0或者j=0时,其中一个串为空串,因此其最长公共子串也为空串,因此长度为0;
我们只需要做的是从上到下,从左到右将N中的值计算出来,最后计算出的Ni,jN_{i,j}Ni,j即为X和Y的最长公共子序列。
现在,从N中已经计算出了最长公共子序列的长度,但是我们需要的是返回最长公共子序列,是一个序列,因此还需要有一个结构来记录哪些字符组成了这个序列。我们仍然使用一个大小为(m+1)*(n+1)的二维数组H,用来记录N中每个位置的值是从哪个方向计算得到的。则H中每一个位置的值:
h(i,j)={lefttop,Xi=Yjleft,h(i,j−1)&gt;h(i−1,j)并且Xi&lt;&gt;Yjtop,h(i,j−1)&lt;h(i−1,j)并且Xi&lt;&gt;Yjh(i,j)= \left\{ \begin{array}{c} lefttop, X_i=Y_j\\ left,h(i,j-1)&gt;h(i-1,j) 并且X_i&lt;&gt;Y_j\\ top,h(i,j-1)&lt;h(i-1,j) 并且X_i&lt;&gt;Y_j \end{array}\right. h(i,j)=lefttop,Xi=Yjlefth(i,j1)>h(i1,j)Xi<>Yjtoph(i,j1)<h(i1,j)Xi<>Yj
最终,依次遍历H数组,输出所有值为lefttop位置所对应的字符,即为最长公共子序列。
**时间复杂度分析:**整个计算过程只是将N和H的值从左到右、从上到下遍历一遍即可,所以需要2m∗n2m*n2mn个时间单位,故时间复杂度为O(m∗n)O(m*n)O(mn)
**空间复杂度分析:**整个计算过程只申请了2个大小为(m+1)∗(n+1)(m+1)*(n+1)m+1(n+1)的二维数组,因此空间复杂度也为O(m∗n)O(m*n)O(mn)

接下来附上计算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()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值