算法导论 — 15.4 最长公共子序列

###笔记
  最长公共子序列(Longest-Common-Subsequence, LCS)问题:给定两个序列 X m = &lt; x 1 , x 2 , … , x m &gt; X_m = &lt;x_1, x_2, …, x_m&gt; Xm=<x1,x2,,xm> Y n = &lt; y 1 , y 2 , … , y n &gt; Y_n = &lt;y_1, y_2, …, y_n&gt; Yn=<y1,y2,,yn>,求解长度最长的公共子序列。

如果用暴力搜索法求解LCS问题,就要穷举 X m X_m Xm的所有子序列,对每个子序列检查它是否也是 Y n Y_n Yn的子序列,再从中找到最长子序列。而 X m X_m Xm一共有 2 m 2^m 2m个子序列( X m X_m Xm的每个元素都可选择在或者不在子序列中,因此子序列有 2 m 2^m 2m个),因此暴力搜索法的运行时间为指数级。
  
  然而,LCS问题具有最优子结构,可以用动态规划方法来求解。令 Z = &lt; z 1 , z 2 , … , z k &gt; Z = &lt;z_1, z_2, …, z_k&gt; Z=<z1,z2,,zk> X X X Y Y Y的任意LCS,以下分3种情况:  
  (1) 如果 x m = y n x_m = y_n xm=yn,则 z k = x m = y n z_k = x_m = y_n zk=xm=yn,且 Z k − 1 = &lt; z 1 , z 2 , … , z k − 1 &gt; Z_{k-1} = &lt;z_1, z_2, …, z_{k-1}&gt; Zk1=<z1,z2,,zk1> X m − 1 = &lt; x 1 , x 2 , … , x m − 1 &gt; X_{m-1} = &lt;x_1, x_2, …, x_{m-1}&gt; Xm1=<x1,x2,,xm1> Y n − 1 = &lt; y 1 , y 2 , … , y n − 1 &gt; Y_{n-1} = &lt;y_1, y_2, …, y_{n-1}&gt; Yn1=<y1,y2,,yn1>的一个LCS;  
  (2) 如果 x m ≠ y n x_m ≠ y_n xm̸=yn,那么 z k ≠ x m z_k ≠ x_m zk̸=xm意味着 Z k Z_k Zk X m − 1 X_{m-1} Xm1 Y n Y_n Yn的一个LCS;
  (3) 如果 x m ≠ y n x_m ≠ y_n xm̸=yn,那么 z k ≠ y n z_k ≠ y_n zk̸=yn意味着 Z k Z_k Zk X m X_m Xm Y n − 1 Y_{n-1} Yn1的一个LCS。

根据以上事实,可以递归地求解LCS问题。如果 x m = y n x_m = y_n xm=yn,递归求解 X m − 1 X_{m-1} Xm1 Y n − 1 Y_{n-1} Yn1的LCS,将 x m x_m xm加到 X m − 1 X_{m-1} Xm1 Y n − 1 Y_{n-1} Yn1的LCS末尾,就得到 X m X_m Xm Y n Y_n Yn的LCS。如果 x m ≠ y n x_m ≠ y_n xm̸=yn,则必须求解两个子问题: X m − 1 X_{m-1} Xm1 Y n Y_n Yn的LCS、 X m X_m Xm Y n − 1 Y_{n-1} Yn1的LCS。二者中较长者即为 X m X_m Xm Y n Y_n Yn的LCS。

我们用 c [ i , j ] c[i, j] c[i,j]表示 X i X_i Xi Y j Y_j Yj的LCS长度。根据以上分析,可以得到下面的递归式。
c [ i , j ] = { 0 i = 0 或 j = 0 c [ i − 1 , j − 1 ] + 1 i , j &gt; 0 且 x i = y j m a x ( c [ i , j − 1 ] , c [ i − 1 , j ] ) i , j &gt; 0 且 x i ≠ y j c[i, j] = \begin{cases} 0 &amp;&amp; {i=0或j=0} \\ c[i-1, j-1]+1 &amp;&amp; {i, j &gt; 0且x_i = y_j} \\ max(c[i, j-1], c[i-1, j]) &amp;&amp; {i, j &gt; 0且x_i ≠ y_j} \\ \end{cases} c[i,j]=0c[i1,j1]+1max(c[i,j1],c[i1,j])i=0j=0i,j>0xi=yji,j>0xi̸=yj

根据上式中 i i i j j j的取值范围,我们可以知道LCS问题一共有 Θ ( m n ) Θ(mn) Θ(mn)个不同的子问题。并且求解规模较大的子问题依赖于规模较小的子问题。因此,可以用动态规划方法自下而上地求解LCS问题。下面给出代码。  
  这里写图片描述
  显然,该算法的运行时间为 Θ ( m n ) Θ(mn) Θ(mn),因为每个表项的计算时间为 Θ ( 1 ) Θ(1) Θ(1)。根据表格 b b b可以构造 X m X_m Xm Y n Y_n Yn的LCS。只需从 b [ m , n ] b[m, n] b[m,n]开始,并按箭头方向追踪下去即可。代码如下所示。
  这里写图片描述

###习题
15.4-1 求<1, 0, 0, 1, 0, 1, 0, 1>和<0, 1, 0, 1, 1, 0, 1, 1, 0>的一个LCS。
  
  LCS为<1, 0, 0, 1, 1, 0>。

15.4-2 设计伪代码,利用完整的表 c c c及原始序列 X = &lt; x 1 , x 2 , … , x m &gt; X = &lt;x_1, x_2, …, x_m&gt; X=<x1,x2,,xm> Y = &lt; y 1 , y 2 , … , y n &gt; Y = &lt;y_1, y_2, …, y_n&gt; Y=<y1,y2,,yn>来重构LCS,要求运行时间为 O ( m + n ) O(m+n) O(m+n),不能使用表 b b b
  
  这里写图片描述
15.4-3 设计LCS-LENGTH的带备忘的版本,运行时间为 O ( m n ) O(mn) O(mn)
  
  这里写图片描述
15.4-4 说明如何只使用表 c c c 2 × m i n ( m , n ) 2×min(m, n) 2×min(m,n)个表项及 O ( 1 ) O(1) O(1)的额外空间来计算LCS的长度。然后说明如何只用 m i n ( m , n ) min(m, n) min(m,n)个表项及 O ( 1 ) O(1) O(1)的额外空间完成相同的工作。
  
  代码一逐行计算表 c c c,在计算 c c c的每一行时,仅需要引用上一行的数据。因此,仅需要两行,一行存储表 c c c的当前行的数据,另一行存储表 c c c的上一行的数据。在代码一中,表 c c c的一行有 n n n个元素,其实可以交换原始序列 X X X Y Y Y的顺序,这样表 c c c的一行有 m m m个元素。我们在创建表 c c c时,可以选取 m m m n n n中的较小值作为表 c c c的一行的元素个数。因此,计算LCS的长度,可以只使用 2 × m i n ( m , n ) 2×min(m, n) 2×min(m,n)个表项及 O ( 1 ) O(1) O(1)的额外空间。

我们再仔细看看代码一,在计算表 c c c的某一个表项 c [ i , j ] c[i, j] c[i,j]时,仅需要引用 c [ i − 1 , j − 1 ] c[i-1, j-1] c[i1,j1] c [ i − 1 , j ] c[i-1, j] c[i1,j] c [ i , j − 1 ] c[i, j-1] c[i,j1]。假设存储空间只有一行。我们是按照从左到右的顺序来计算一行的表项,当计算到 c [ i , j ] c[i, j] c[i,j]时, c [ i , j ] c[i, j] c[i,j]的位置上还保留了上一行相同位置的数据 c [ i − 1 , j ] c[i-1, j] c[i1,j],而 c [ i , j − 1 ] c[i, j-1] c[i,j1] c [ i , j ] c[i, j] c[i,j]之前已经被计算得到。现在只剩下 c [ i − 1 , j − 1 ] c[i-1, j-1] c[i1,j1],可以额外用一个变量 t t t来保存 c [ i − 1 , j − 1 ] c[i-1, j-1] c[i1,j1]。这样,计算LCS的长度,仅需要表 c c c的一行的空间及 O ( 1 ) O(1) O(1)的额外空间。下面只给出这种做法的伪代码。
  这里写图片描述
15.4-5 设计一个 O ( n 2 ) O(n^2) O(n2)时间的算法,求一个 n n n个数的序列的最长单调递增子序列。
  
  先对数组排序,然后找出排序后的数组与原数组的LCS,就是最长单调递增子序列。简单的插入排序需要 O ( n 2 ) O(n^2) O(n2)时间,找LCS也需要花费 O ( n 2 ) O(n^2) O(n2)时间,因此该算法总的运行时间为 O ( n 2 ) O(n^2) O(n2)
  
15.4-6 设计一个 O ( n lg n ) O(n\text{lg}n) O(nlgn)时间的算法,求一个 n n n个数的序列的最长单调递增子序列。(提示:注意到,一个长度为 i i i的候选子序列的尾元素至少不比一个长度为 i − 1 i-1 i1的候选子序列的尾元素小。因此,可以在输入序列中将候选子序列链接起来。)
  
  假设原数列为 X X X。我们用 e [ i ] e[i] e[i]表示以元素 X [ i ] X[i] X[i]结尾的最长单调递增子序列(Longest Monotonically Increasing Sub-sequence,LMIS)的长度。为了计算每个元素的 e [ i ] e[i] e[i],我们需要维护一个有序数组 S S S。依次遍历原数组 X X X中的每个元素 X [ i ] X[i] X[i],在有序数组 S S S中找到不小于 X [ i ] X[i] X[i]的最小元素,并用 X [ i ] X[i] X[i]替换它。如果有序数组 S S S中的所有元素都比 X [ i ] X[i] X[i]小,那么将 X [ i ] X[i] X[i]放到有序数组 S S S中的最后一个元素的后一个位置。 X [ i ] X[i] X[i]在有序数组 S S S中的位置就是 e [ i ] e[i] e[i]。下面用一个例子来说明。  
  有一个数组 X = &lt; 9 , 1 , 2 , 4 , 6 , 3 , 5 , 0 &gt; X = &lt;9, 1, 2, 4, 6, 3, 5, 0&gt; X=<9,1,2,4,6,3,5,0>,下表列出了每次迭代后有序数组 S S S的变化,以及每个元素有 e [ i ] e[i] e[i]。可以看到,LMIS的长度为 4 4 4,也就是最大的 e [ i ] e[i] e[i]

迭代次数 i i i元素 X [ i ] X[i] X[i]数组S e [ i ] e[i] e[i]
199 / / / / / / /1
211 / / / / / / /1
321 2 / / / / / /2
441 2 4 / / / / /3
561 2 4 6 / / / /4
631 2 3 6 / / / /3
751 2 3 5 / / / /4
800 2 3 5 / / / /1

我们现在来寻找整个数组的LMIS。我们已经知道,LMIS的长度为 4 4 4。按如下步骤来寻找:
  1) 先找到 e [ i ] e[i] e[i] 4 4 4的元素,有两个,元素值分别为 5 5 5 6 6 6。以其中一个作为LMIS的尾元素。假设我们选择 5 5 5作为尾元素,即原数组的第 7 7 7个元素。
  2) 接下来我们寻找一个长度为 3 3 3的单调递增子序列的尾元素。寻找范围是在原数组第 1 1 1~ 6 6 6个元素之内(因为原数组第 7 7 7个元素在步骤 1 1 1)中已被找出,故第 7 7 7个元素及其后的元素不再考虑),并且找到的元素必须小于步骤1)中找到的元素。满足这些条件的元素有两个,元素值分别为 3 3 3 4 4 4。假设我们选择 3 3 3作为我们找到的元素,即原数组的第 6 6 6个元素。
  3) 接下来我们寻找一个长度为 2 2 2的单调递增子序列的尾元素,寻找方法与步骤2)一样。我们找到的元素值为 2 2 2,即原数组的第 3 3 3个元素。
  4) 最后我们要寻找一个长度为 1 1 1的单调递增子序列的尾元素。我们找到的元素为 1 1 1,即原数组的第 2 2 2个元素。
  按以上迭代步骤,我们找到一个完整的LMIS为 &lt; 1 , 2 , 3 , 5 &gt; &lt;1, 2, 3, 5&gt; <1,2,3,5>
  如果我们在迭代过程中要寻找一个长度为 k k k的单调递增子序列的尾元素,可以采用一种简单的方法。从后向前遍历 e [ i ] e[i] e[i],首先遇到的满足 e [ i k ] = = k e[i_k] == k e[ik]==k的元素作为我们找到的元素 X [ i k ] X[i_k] X[ik]。这个元素必定小于上一步迭代过程中找到的长度为 k + 1 k+1 k+1的单调递增子序列的尾元素 X [ i k + 1 ] X[i_{k+1}] X[ik+1]。因为如果不满足 X [ i k ] &lt; X [ i k + 1 ] X[i_k] &lt; X[i_{k+1}] X[ik]<X[ik+1],那么在将 X [ i k + 1 ] X[i_{k+1}] X[ik+1]插入到有序数组 S S S中时,由于 X [ i k + 1 ] ≤ X [ i k ] X[i_{k+1}] ≤ X[i_k] X[ik+1]X[ik] X [ i k + 1 ] X[i_{k+1}] X[ik+1]会取代 X [ i k ] X[i_k] X[ik],或者取代 X [ i k ] X[i_k] X[ik]之前的某个元素,那么 X [ i k + 1 ] X[i_{k+1}] X[ik+1]就不是一个长度为 k + 1 k+1 k+1的单调递增子序列的尾元素,这推导出了矛盾。因此 X [ i k ] &lt; X [ i k + 1 ] X[i_k] &lt; X[i_{k+1}] X[ik]<X[ik+1]必然成立。
  综上所述,我们可以形成一个算法,下面给出了伪代码。
  这里写图片描述
  下面分析该算法的时间复杂度。LMIS包含一重循环,每次循环迭代会调用一次 FIND-POS \text{FIND-POS} FIND-POS。而 FIND-POS \text{FIND-POS} FIND-POS是一个二分查找,它的时间复杂度为 O ( lg n ) O(\text{lg}n) O(lgn)。故LMIS的时间复杂度为 O ( n lg n ) O(n\text{lg}n) O(nlgn) PRINT-LMIS \text{PRINT-LMIS} PRINT-LMIS也只包含一重循环,每次循环迭代为常数时间,故 PRINT-LMIS \text{PRINT-LMIS} PRINT-LMIS的时间复杂度为 O ( n ) O(n) O(n)。综上,整个算法的时间复杂度为 O ( n lg n ) O(n\text{lg}n) O(nlgn)

本节相关的code链接。 
  https://github.com/yangtzhou2012/Introduction_to_Algorithms_3rd/tree/master/Chapter15/Section_15.4

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值