动态规划(DP)---LCS(the Longest Common Subsequence)

0 暴力求解两个序列的最长公共子序列(LCS)

stringx : B D C A B A
stringy : A B C B D A B

字符串 xy 的几个最长公共子序列是这个样子的:

BDABBCBABCAB

第一次遇到这种题时,我会不假思索地将字符串 xy 所有的子序列找出来,然后一个个地比较来找出它们的最长子序列(LCS)。那么问题来了,一个长度为 m 的字符串到底有多少个子序列呢?

1C1m

2C2m

3C3m

.....

mCmm

子序列的个数为: C1m+C2m+C3m+...+Cmm

这个跟二项展开式很像,二项展开式是这个样子的:

(a+b)m=C0mam+C1mam1b1+C2mam2b2+...+Crmamrbr+...+Cmmbm

所以,一个长度为 m 的字符串一共有 2m1 个子序列。

那么,使用暴力的方式,找出两个长度分别为 m,n 的字符串的 LCS 的时间复杂度是多少呢?

1st,:=>2m1+2n1=Θ(2m+2n)

2nd,:=>(2m1)×(2n1)=Θ(2m+n)

由此,暴力破解的时间复杂度就是:
Θ(2m+2n)+Θ(2m+n)=Θ(2m+n)

如何?暴力破解所消耗的时间是 指数规模(exponential time)的,这样的速度就是龟速!


1 动态规划(Dynamic Programming)

动态规划通常用来求解最优化问题(optimization problem)。这类问题可以有很多可行解,每个解都有一个值,我们希望寻找具有最优值(最小值或最大值)的解。我们称这样的解为问题的一个最优解(an optimal solution),而不是最优解(the optimal solution),因为可能有多个解达到最优值。

我们通常按如下4个步骤来设计一个动态规划算法:

1,刻画一个最优解的结构特征。
2,递归地定义最优解的值。
3,计算最优解的值,通常采用自底向上的方法。
4,利用计算出的信息构造一个最优解。

————自《算法导论》机械工业出版社

并不是所有的最优问题都可以使用动态规划来求解,使用动态规划必须满足两个问题:

1. 最优子问题(optimal subproblems)。
2. 重叠子问题(overlapping subproblems)。


2 使用动态规划求解LCS

按照上面提到的4个步骤来设计一个求解LCS的动态规划算法。

2.1 刻画一个最优解的结构特征

定义:

c[i,j]=|LCS(x[1...i],y[1...j])|......(1)

c[i,j] 为字符串 x[1...i] y[1...j] 的 LCS 的长度。

那么:

c[m,n]=|LCS(x,y)|m,nx,y

因此,LCS 最优解的结构特征就是 c[m,n]

2.2 递归定义最优解的值

根据2.1定义的最优解的结构特征,写出 c[i,j] 的归纳表达式如下:

c[i,j]={c[i1,j1]+1max{c[i1,j],c[i,j1]}ifx[i]==y[j]otherwise}......(2)

下面证明式(2)的正确性。

      ifx[i]==y[j]

z[1...k]LCS(x[1...i],y[1...j])c[i,j]=kz[k]=x[i]=y[j]

z[1...k1]=LCS(x[1...i1],y[1...j1])c[i1,j1]=k1

w=CS(x[1...i1],y[1...j1]),|w|>k1使wz[k]c[i,j]=|w,z[k]|>kc[i,j]=kCutCopy

      otherx[i]!=y[j] 证明略。

2.3 计算最优解的值,通常采用自底向上

2.3.1 自顶向下

参照归纳表达式(2),写出LCS递归算法如下:

LCS(x,y,i,j)
if x[i] == x[j]
    c[i,j]=LCS(x,y,i-1,j-1)+1
else
    c[i,j]=max(LCS(x,y,i-1,j),LCS(x,y,i,j-1))
return c[i,j]

在最坏情况下,即 x[i]!=y[j] ,参数 stringx,y 的部分递归树,如下:

递归树部分

递归树的高度为: m+n=13 m,n为两个字符串的长度。根据满二叉树的性质,知道高度 h ,就可以算出二叉树节点的总数为2h1,从上面的递归树可以知道,一个节点就是一个子问题,那么算法的时间复杂度有一个渐进紧确上界为: O(2m+n)

由蓝色虚线框出的部分可以看出,递归算法存在重复运算,这也验证了动态规划的第二个特征:重叠子问题
改进算法,将子问题的解存储起来(备忘法),下次求解相同子问题时直接取出解:

LCS(x,y,i,j)
if c[i,j]!=nil
    return c[i,j]
if x[i] == x[j]
    c[i,j]=LCS(x,y,i-1,j-1)+1
else
    c[i,j]=max(LCS(x,y,i-1,j),LCS(x,y,i,j-1))
return c[i,j]

使用备忘法后,独立子问题的规模就只有 m×n ,相应的时间复杂度为 Θ(m×n)
那么,独立子问题的规模是怎么知道的呢?答案就在算法中的数组 c[i,j] ,整个算法就是在填充二维数组 c ,所以独立子问题的规模就等于二维数组c的大小 m×n

2.3.2 自底向上(bottom-up)

从前面的自顶向下可以看出,算法有很多的重复计算,虽然采用备忘法可以去掉重复,但是程序极为不清晰。一般来说,真正的动态规划更多的是采用自底向上的方法来去重复。

参照式(2)归纳式,可以很容易地写出自底向上的伪代码,其求解方法就是自底向上填充数组 c

LCS(x,y,m,n)
for i=0 to m-1
    for j=0 to n-1
        if x[i]=y[j]
            c[i,j]=c[i-1,j-1]+1
        else 
            c[i,j]=max(c[i-1,j],c[i,j-1])
return c[m,n]
//代码没有考虑 c[-1,-1],由于 c[-1,-1]没有任何前缀字符,所以 c[-1,-1]=0, c[-1,*]=c[*,-1]=0

根据代码来填充数组 c,如下图所示:
这里写图片描述

如图所示,蓝色边框就是计算后的数组 c ,在 c 中,红色元素为上面代码中满足 x[i]=y[j] 的情形,蓝色情况为不满足情形。

2.4 利用计算出的信息构造一个最优解

那么利用数组 c <script type="math/tex" id="MathJax-Element-50">c</script> 的信息如何找出真正满足LCS的条件呢?
这里写图片描述这里写图片描述这里写图片描述这里写图片描述这里写图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
最长公共子序列问题(Longest Common Subsequence,简称LCS)是指在两个序列中找到一个最长的公共子序列,其中一个序列的所有元素按原序列中出现的顺序排列,而另一个序列中的元素则不要求按原序列中出现的顺序排列。 动态规划方法可以很好地解决LCS问题。设A和B是两个序列,LCS(A,B)表示A和B的最长公共子序列。则可以设计如下的状态转移方程: 当A和B的末尾元素相同时,LCS(A,B) = LCS(A-1,B-1) + 1。 当A和B的末尾元素不同时,LCS(A,B) = max(LCS(A-1,B), LCS(A,B-1))。 其中,LCS(A-1,B-1)表示A和B的末尾元素相同时的情况,LCS(A-1,B)表示A的最后一个元素不在最长公共子序列中,而B中的最后一个元素在最长公共子序列中的情况,LCS(A,B-1)表示B的最后一个元素不在最长公共子序列中,而A中的最后一个元素在最长公共子序列中的情况。 根据这个状态转移方程,可以使用动态规划算法来求解LCS问题。具体方法是,构建一个二维数组dp,其中dp[i][j]表示A前i个元素和B前j个元素的LCS。初始化dp[0][j]和dp[i][0]为0,然后按照上述状态转移方程进行递推,最终得到dp[lenA][lenB],其中lenA和lenB分别表示A和B的长度。dp[lenA][lenB]即为A和B的最长公共子序列的长度。要找到具体的最长公共子序列,可以从dp[lenA][lenB]开始,按照状态转移方程反向推导出每个元素,即可得到最长公共子序列。 LCS问题是动态规划算法的经典应用之一,时间复杂度为O(n*m),其中n和m分别为A和B的长度。
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值