动态规划之LCS

来源:互联网 发布:淘宝鹊桥app 编辑:程序博客网 时间:2024/04/29 20:59

最长公共子序列的意思就是两个序列,有公共的部分,公共部分在这两个序列的位置上不一定相等,但序列的逻辑顺序是相等的

例如给定两个序列x[1..m]y[1..n],找出一个(注,这里说的是一个而不是这个,也就是说可能有很多个)最长的公共序列,其中

 x: A B C B D A B          y: B D C A B A

LCS(x,y) = BCBA   .........此处LCS(x,y)是一种函数标识形式,但不是函数

枚举算法:

x[1..m]中每一个子序列判断是否也是y[1..n]中的一个子序列

这个算法对于x的每一个子序列y需要On)来比较,而x2m个子序列(注,可以当成一个长度为mbit-vector即位向量来找出不同的子序列)

这个算法的最坏运行时间是On2m)

递归算法:

策略是考虑xy的前缀

定义:c[i,j] = | LCS(x[1..i],y[1..j])| c[m,n] = | LCS(x,y)|

上面c[i,j]表示x[1..i]y[1..j]的最长公共序列的长度,c[m,n]表示x[1..m]y[1..n]的最长公共序列的长度

则递归公式:

证明:在x[i] = y[j]情况下,如下图:

设z[1..k] = LCS(x[1..i],y[1..j])x,y的一个最长公共序列是zc[i,j] = k那么z[k] = x[i] = y[j]

显然z[1..k-1]x[1..i-1]y[1..j-1]的一个公共序列则令z[1..k-1] = LCS(x[1..i-1],y[1..j])为了推出矛盾,假设w是一个比c[i,j]-1更长的关于 x[1..i-1]y[1..j-1]的一个公共序列,即|w| > k-1

然后利用cut and paste(剪切粘贴)方法 w||z[k]w concatenated with z[k])得到 x[1..i]y[1..j]的一个公共子序列,然而|w||z[k]| >k,推出矛盾,所以c[i-1,j-1] = k-1意味着c[i,j] = c[i-1,j-1] +1

另一个方面即x[i] 与 y[j] 不相等,证明过程相同,此处省略。

这样就得到了Dynamic-programming的一个特征:

1一个问题的最优解包含了子问题的最优解

即如果z = LCS(x,y)则任何z的前缀必是x的前缀和y的前缀的一个最长公共子序列

代码如下:

下面有一个递归树来说明问题:

这个树的高度是m+n,观察这个树我们发现重复计算了很多子问题这样就引出了Dynamic-programming的另一个特征:

2一个递归的过程包含少数独立问题被反复计算多次

求上面给出的两个长度分别为m,n的字符串的LCS的子问题数为mn

不是严格的分析:以上图为例树中节点包含了所有情况且有重复,m=3有一个集合{123}n = 4有一个集合{1234},则从集合m和集合n中各取一个元素即{x,y/x∈m或者x∈ny∈m或者y∈n},我们把重复的节点去掉就变成了{x,y/x∈my∈n}即子问题个数为mn

为了避免重复计算我们把每一个子问题的结果都存到一张表里,当有一个新的子问题的时候如果前面已经计算过了就直接到表里取就可以了

代码如下:

时间复杂度和空间复杂度都为Omn

为了更清楚的理解这个问题,以开始举的例子为例用这种方法来计算LCS

图标中的第一行和第一列是LCS的初始值全为0y中第一个子序列即B开始在x的所有子序列中如果有B就置1这样就完成了第二行,第三行即从y中一个子序列BD开始在x所有子序列中如果有D就在上一行对应的位置下面加一表示LCS的长度,以此类推可以完成整个表格的填写。

完成表格以后就要找到一条路径即为LCS这条路径的找法分为两种:

1从上往下:从第一行开始找到一条路径使这个方格的后续值是相同的且随行数递增

2从下往上:从最后一行开始通过行减1或者列减1得到一个依次递减的序列

利用上面两种方法都可以找到LCS

Python实现的代码如下:

def LCS(x,y):    d = [ [ None for i in x ] for j in y ]     m = [ [ 0 for k in x] for v in y]    for p1 in range(len(y)):         for p2 in range(len(x)):             if y[p1] == x[p2]:                if p1 == 0 or p2 == 0:                     m[p1][p2] = 1                 else:                     m[p1][p2] = m[p1-1][p2-1]+1                 d[p1][p2] = 1            elif m[p1-1][p2] < m[p1][p2-1]:                 m[p1][p2] = m[p1][p2-1]                 d[p1][p2] = 2            else:                             # m[p1][p2-1] < m[p1-1][p2]                 m[p1][p2] = m[p1-1][p2]                 d[p1][p2] = 3        s = []    (p1, p2) = (len(y)-1, len(x)-1)     while p1 >=0 and p2>=0 and m[p1][p2]>0:        print(p1,p2)        c = d[p1][p2]        if c == 1:             s.append(y[p1])            p1 -=1            p2 -=1         if c == 2: p2 -= 1         if c == 3: p1 -= 1     s.reverse()     return s x = ['A','B','C','B','D','A','B']y = ['B','D','C','A','B','A']print(LCS(x,y))

转自http://zsp.iteye.com/blog/379632

关于代码实现我看了好长时间才看懂了过程,不是别的原因主要是自己太菜了,为了防止以后忘记我将过程详细表述如下:

M用来存放lcs长度,d用来存放回溯“指针”,他们大小都是xy,下面用y中每一个元素和x中的所有元素进行比较,如果两个元素相等且其中一个元素是所在序列的第一个则m[p1][p2] = 1 ,有前面讲的前缀法,则m[p1][p2] = m[p1-1][p2-1]+1 

在两个元素相等的情况下令d[p1][p2] = 1表示这个元素属于公共序列,然后当两个元素不相等又分为m[p1-1][p2] < m[p1][p2-1]m[p1-1][p2]>= m[p1][p2-1],第一种情况列减1大于行减1m[p1][p2] = m[p1][p2-1],且让d[p1][p2] = 2,其实d的数值可以任意设定,只不过要注意在输出lcs时在列减1处大的d设定对应的回溯是列减1,第二种情况就和第一种情况相反,感觉没有说清楚,好吧,画一张表格对应着看:

其中黄色表示d中数值,黑色表示m中数值,红色块表示回溯过程(自底向上)


 

原创粉丝点击