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

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

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

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

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

根据上式中 i i j j 的取值范围,我们可以知道LCS问题一共有 Θ ( m n ) Θ(mn) 个不同的子问题。并且求解规模较大的子问题依赖于规模较小的子问题。因此,可以用动态规划方法自下而上地求解LCS问题。下面给出代码。  
  这里写图片描述
  显然,该算法的运行时间为 Θ ( m n ) Θ(mn) ,因为每个表项的计算时间为 Θ ( 1 ) Θ(1) 。根据表格 b b 可以构造 X m X_m Y n Y_n 的LCS。只需从 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 及原始序列 X = &lt; x 1 , x 2 , , x m &gt; X = &lt;x_1, x_2, …, x_m&gt; Y = &lt; y 1 , y 2 , , y n &gt; Y = &lt;y_1, y_2, …, y_n&gt; 来重构LCS,要求运行时间为 O ( m + n ) O(m+n) ,不能使用表 b b
  
  这里写图片描述
15.4-3 设计LCS-LENGTH的带备忘的版本,运行时间为 O ( m n ) O(mn)
  
  这里写图片描述
15.4-4 说明如何只使用表 c c 2 × m i n ( m , n ) 2×min(m, n) 个表项及 O ( 1 ) O(1) 的额外空间来计算LCS的长度。然后说明如何只用 m i n ( m , n ) min(m, n) 个表项及 O ( 1 ) O(1) 的额外空间完成相同的工作。
  
  代码一逐行计算表 c c ,在计算 c c 的每一行时,仅需要引用上一行的数据。因此,仅需要两行,一行存储表 c c 的当前行的数据,另一行存储表 c c 的上一行的数据。在代码一中,表 c c 的一行有 n n 个元素,其实可以交换原始序列 X X Y Y 的顺序,这样表 c c 的一行有 m m 个元素。我们在创建表 c c 时,可以选取 m m n n 中的较小值作为表 c c 的一行的元素个数。因此,计算LCS的长度,可以只使用 2 × m i n ( m , n ) 2×min(m, n) 个表项及 O ( 1 ) O(1) 的额外空间。

我们再仔细看看代码一,在计算表 c c 的某一个表项 c [ i , j ] c[i, j] 时,仅需要引用 c [ i 1 , j 1 ] c[i-1, j-1] c [ i 1 , j ] c[i-1, j] c [ i , j 1 ] c[i, j-1] 。假设存储空间只有一行。我们是按照从左到右的顺序来计算一行的表项,当计算到 c [ i , j ] c[i, j] 时, c [ i , j ] c[i, j] 的位置上还保留了上一行相同位置的数据 c [ i 1 , j ] c[i-1, j] ,而 c [ i , j 1 ] c[i, j-1] c [ i , j ] c[i, j] 之前已经被计算得到。现在只剩下 c [ i 1 , j 1 ] c[i-1, j-1] ,可以额外用一个变量 t t 来保存 c [ i 1 , j 1 ] c[i-1, j-1] 。这样,计算LCS的长度,仅需要表 c c 的一行的空间及 O ( 1 ) O(1) 的额外空间。下面只给出这种做法的伪代码。
  这里写图片描述
15.4-5 设计一个 O ( n 2 ) O(n^2) 时间的算法,求一个 n n 个数的序列的最长单调递增子序列。
  
  先对数组排序,然后找出排序后的数组与原数组的LCS,就是最长单调递增子序列。简单的插入排序需要 O ( n 2 ) O(n^2) 时间,找LCS也需要花费 O ( n 2 ) O(n^2) 时间,因此该算法总的运行时间为 O ( n 2 ) O(n^2)
  
15.4-6 设计一个 O ( n lg n ) O(n\text{lg}n) 时间的算法,求一个 n n 个数的序列的最长单调递增子序列。(提示:注意到,一个长度为 i i 的候选子序列的尾元素至少不比一个长度为 i 1 i-1 的候选子序列的尾元素小。因此,可以在输入序列中将候选子序列链接起来。)
  
  假设原数列为 X X 。我们用 e [ i ] e[i] 表示以元素 X [ i ] X[i] 结尾的最长单调递增子序列(Longest Monotonically Increasing Sub-sequence,LMIS)的长度。为了计算每个元素的 e [ i ] e[i] ,我们需要维护一个有序数组 S S 。依次遍历原数组 X X 中的每个元素 X [ i ] X[i] ,在有序数组 S S 中找到不小于 X [ i ] X[i] 的最小元素,并用 X [ i ] X[i] 替换它。如果有序数组 S S 中的所有元素都比 X [ i ] X[i] 小,那么将 X [ i ] X[i] 放到有序数组 S S 中的最后一个元素的后一个位置。 X [ i ] X[i] 在有序数组 S S 中的位置就是 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; ,下表列出了每次迭代后有序数组 S S 的变化,以及每个元素有 e [ i ] e[i] 。可以看到,LMIS的长度为 4 4 ,也就是最大的 e [ i ] e[i]
| 迭代次数 i i | 元素 X [ i ] X[i] | 数组S | e [ i ] 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 4 。按如下步骤来寻找:
  1) 先找到 e [ i ] e[i] 4 4 的元素,有两个,元素值分别为 5 5 6 6 。以其中一个作为LMIS的尾元素。假设我们选择 5 5 作为尾元素,即原数组的第 7 7 个元素。
  2) 接下来我们寻找一个长度为 3 3 的单调递增子序列的尾元素。寻找范围是在原数组第 1 1 ~ 6 6 个元素之内(因为原数组第 7 7 个元素在步骤 1 1 )中已被找出,故第 7 7 个元素及其后的元素不再考虑),并且找到的元素必须小于步骤1)中找到的元素。满足这些条件的元素有两个,元素值分别为 3 3 4 4 。假设我们选择 3 3 作为我们找到的元素,即原数组的第 6 6 个元素。
  3) 接下来我们寻找一个长度为 2 2 的单调递增子序列的尾元素,寻找方法与步骤2)一样。我们找到的元素值为 2 2 ,即原数组的第 3 3 个元素。
  4) 最后我们要寻找一个长度为 1 1 的单调递增子序列的尾元素。我们找到的元素为 1 1 ,即原数组的第 2 2 个元素。
  按以上迭代步骤,我们找到一个完整的LMIS为 &lt; 1 , 2 , 3 , 5 &gt; &lt;1, 2, 3, 5&gt;
  如果我们在迭代过程中要寻找一个长度为 k k 的单调递增子序列的尾元素,可以采用一种简单的方法。从后向前遍历 e [ i ] e[i] ,首先遇到的满足 e [ i k ] = = k e[i_k] == k 的元素作为我们找到的元素 X [ i k ] X[i_k] 。这个元素必定小于上一步迭代过程中找到的长度为 k + 1 k+1 的单调递增子序列的尾元素 X [ i k + 1 ] X[i_{k+1}] 。因为如果不满足 X [ i k ] &lt; X [ i k + 1 ] X[i_k] &lt; X[i_{k+1}] ,那么在将 X [ i k + 1 ] X[i_{k+1}] 插入到有序数组 S S 中时,由于 X [ i k + 1 ] X [ i k ] X[i_{k+1}] ≤ X[i_k] X [ i k + 1 ] X[i_{k+1}] 会取代 X [ i k ] X[i_k] ,或者取代 X [ i k ] X[i_k] 之前的某个元素,那么 X [ i k + 1 ] X[i_{k+1}] 就不是一个长度为 k + 1 k+1 的单调递增子序列的尾元素,这推导出了矛盾。因此 X [ i k ] &lt; X [ i k + 1 ] X[i_k] &lt; X[i_{k+1}] 必然成立。
  综上所述,我们可以形成一个算法,下面给出了伪代码。
  这里写图片描述
  下面分析该算法的时间复杂度。LMIS包含一重循环,每次循环迭代会调用一次 FIND-POS \text{FIND-POS} 。而 FIND-POS \text{FIND-POS} 是一个二分查找,它的时间复杂度为 O ( lg n ) O(\text{lg}n) 。故LMIS的时间复杂度为 O ( n lg n ) O(n\text{lg}n) PRINT-LMIS \text{PRINT-LMIS} 也只包含一重循环,每次循环迭代为常数时间,故 PRINT-LMIS \text{PRINT-LMIS} 的时间复杂度为 O ( n ) O(n) 。综上,整个算法的时间复杂度为 O ( n lg n ) O(n\text{lg}n)

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

猜你喜欢

转载自blog.csdn.net/yangtzhou/article/details/81713303