《算法导论》读书笔记与习题解答 — 15.4 最长公共子序列

笔记

  最长公共子序列(Longest-Common-Subsequence, LCS)问题:给定两个序列Xm=<x1,x2,,xm>Yn=<y1,y2,,yn>,求解长度最长的公共子序列。

  如果用暴力搜索法求解LCS问题,就要穷举Xm的所有子序列,对每个子序列检查它是否也是Yn的子序列,再从中找到最长子序列。而Xm一共有2m个子序列(Xm的每个元素都可选择在或者不在子序列中,因此子序列有2m个),因此暴力搜索法的运行时间为指数级。
  
  然而,LCS问题具有最优子结构,可以用动态规划方法来求解。令Z=<z1,z2,,zk>XY的任意LCS,以下分3种情况:  
  (1) 如果xm=yn,则zk=xm=yn,且Zk1=<z1,z2,,zk1>Xm1=<x1,x2,,xm1>Yn1=<y1,y2,,yn1>的一个LCS;  
  (2) 如果xmyn,那么zkxm意味着ZkXm1Yn的一个LCS;
  (3) 如果xmyn,那么zkyn意味着ZkXmYn1的一个LCS。

  根据以上事实,可以递归地求解LCS问题。如果xm=yn,递归求解Xm1Yn1的LCS,将xm加到Xm1Yn1的LCS末尾,就得到XmYn的LCS。如果xmyn,则必须求解两个子问题:Xm1Yn的LCS、XmYn1的LCS。二者中较长者即为XmYn的LCS。

  我们用c[i,j]表示XiYj的LCS长度。根据以上分析,可以得到下面的递归式。

c[i,j]={0i=0j=0c[i1,j1]+1i,j>0xi=yjmax(c[i,j1],c[i1,j])i,j>0xiyj

  根据上式中ij的取值范围,我们可以知道LCS问题一共有Θ(mn)个不同的子问题。并且求解规模较大的子问题依赖于规模较小的子问题。因此,可以用动态规划方法自下而上地求解LCS问题。下面给出代码。  
  《算法导论》读书笔记与习题解答 — 15.4 最长公共子序列
  显然,该算法的运行时间为Θ(mn),因为每个表项的计算时间为Θ(1)。根据表格b可以构造XmYn的LCS。只需从b[m,n]开始,并按箭头方向追踪下去即可。代码如下所示。
  《算法导论》读书笔记与习题解答 — 15.4 最长公共子序列

习题

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及原始序列X=<x1,x2,,xm>Y=<y1,y2,,yn>来重构LCS,要求运行时间为O(m+n),不能使用表b
  
  《算法导论》读书笔记与习题解答 — 15.4 最长公共子序列
15.4-3 设计LCS-LENGTH的带备忘的版本,运行时间为O(mn)
  
  《算法导论》读书笔记与习题解答 — 15.4 最长公共子序列
15.4-4 说明如何只使用表c2×min(m,n)个表项及O(1)的额外空间来计算LCS的长度。然后说明如何只用min(m,n)个表项及O(1)的额外空间完成相同的工作。
  
  代码一逐行计算表c,在计算c的每一行时,仅需要引用上一行的数据。因此,仅需要两行,一行存储表c的当前行的数据,另一行存储表c的上一行的数据。在代码一中,表c的一行有n个元素,其实可以交换原始序列XY的顺序,这样表c的一行有m个元素。我们在创建表c时,可以选取mn中的较小值作为表c的一行的元素个数。因此,计算LCS的长度,可以只使用2×min(m,n)个表项及O(1)的额外空间。

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

迭代次数i 元素X[i] 数组S e[i]
1 9 9 / / / / / / / 1
2 1 1 / / / / / / / 1
3 2 1 2 / / / / / / 2
4 4 1 2 4 / / / / / 3
5 6 1 2 4 6 / / / / 4
6 3 1 2 3 6 / / / / 3
7 5 1 2 3 5 / / / / 4
8 0 0 2 3 5 / / / / 1

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

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