###笔记
最长公共子序列(Longest-Common-Subsequence, LCS)问题:给定两个序列
和
,求解长度最长的公共子序列。
如果用暴力搜索法求解LCS问题,就要穷举
的所有子序列,对每个子序列检查它是否也是
的子序列,再从中找到最长子序列。而
一共有
个子序列(
的每个元素都可选择在或者不在子序列中,因此子序列有
个),因此暴力搜索法的运行时间为指数级。
然而,LCS问题具有最优子结构,可以用动态规划方法来求解。令
为
和
的任意LCS,以下分3种情况:
(1) 如果
,则
,且
是
和
的一个LCS;
(2) 如果
,那么
意味着
是
和
的一个LCS;
(3) 如果
,那么
意味着
是
和
的一个LCS。
根据以上事实,可以递归地求解LCS问题。如果 ,递归求解 和 的LCS,将 加到 和 的LCS末尾,就得到 和 的LCS。如果 ,则必须求解两个子问题: 和 的LCS、 和 的LCS。二者中较长者即为 和 的LCS。
我们用
表示
和
的LCS长度。根据以上分析,可以得到下面的递归式。
根据上式中
和
的取值范围,我们可以知道LCS问题一共有
个不同的子问题。并且求解规模较大的子问题依赖于规模较小的子问题。因此,可以用动态规划方法自下而上地求解LCS问题。下面给出代码。
显然,该算法的运行时间为
,因为每个表项的计算时间为
。根据表格
可以构造
和
的LCS。只需从
开始,并按箭头方向追踪下去即可。代码如下所示。
###习题
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 设计伪代码,利用完整的表
及原始序列
和
来重构LCS,要求运行时间为
,不能使用表
。
解
15.4-3 设计LCS-LENGTH的带备忘的版本,运行时间为
。
解
15.4-4 说明如何只使用表
中
个表项及
的额外空间来计算LCS的长度。然后说明如何只用
个表项及
的额外空间完成相同的工作。
解
代码一逐行计算表
,在计算
的每一行时,仅需要引用上一行的数据。因此,仅需要两行,一行存储表
的当前行的数据,另一行存储表
的上一行的数据。在代码一中,表
的一行有
个元素,其实可以交换原始序列
和
的顺序,这样表
的一行有
个元素。我们在创建表
时,可以选取
和
中的较小值作为表
的一行的元素个数。因此,计算LCS的长度,可以只使用
个表项及
的额外空间。
我们再仔细看看代码一,在计算表
的某一个表项
时,仅需要引用
、
和
。假设存储空间只有一行。我们是按照从左到右的顺序来计算一行的表项,当计算到
时,
的位置上还保留了上一行相同位置的数据
,而
在
之前已经被计算得到。现在只剩下
,可以额外用一个变量
来保存
。这样,计算LCS的长度,仅需要表
的一行的空间及
的额外空间。下面只给出这种做法的伪代码。
15.4-5 设计一个
时间的算法,求一个
个数的序列的最长单调递增子序列。
解
先对数组排序,然后找出排序后的数组与原数组的LCS,就是最长单调递增子序列。简单的插入排序需要
时间,找LCS也需要花费
时间,因此该算法总的运行时间为
。
15.4-6 设计一个
时间的算法,求一个
个数的序列的最长单调递增子序列。(提示:注意到,一个长度为
的候选子序列的尾元素至少不比一个长度为
的候选子序列的尾元素小。因此,可以在输入序列中将候选子序列链接起来。)
解
假设原数列为
。我们用
表示以元素
结尾的最长单调递增子序列(Longest Monotonically Increasing Sub-sequence,LMIS)的长度。为了计算每个元素的
,我们需要维护一个有序数组
。依次遍历原数组
中的每个元素
,在有序数组
中找到不小于
的最小元素,并用
替换它。如果有序数组
中的所有元素都比
小,那么将
放到有序数组
中的最后一个元素的后一个位置。
在有序数组
中的位置就是
。下面用一个例子来说明。
有一个数组
,下表列出了每次迭代后有序数组
的变化,以及每个元素有
。可以看到,LMIS的长度为
,也就是最大的
。
| 迭代次数
| 元素
| 数组S |
|
| :--------: | :-------: | :–: | :— : |
| 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的长度为
。按如下步骤来寻找:
1) 先找到
为
的元素,有两个,元素值分别为
和
。以其中一个作为LMIS的尾元素。假设我们选择
作为尾元素,即原数组的第
个元素。
2) 接下来我们寻找一个长度为
的单调递增子序列的尾元素。寻找范围是在原数组第
~
个元素之内(因为原数组第
个元素在步骤
)中已被找出,故第
个元素及其后的元素不再考虑),并且找到的元素必须小于步骤1)中找到的元素。满足这些条件的元素有两个,元素值分别为
和
。假设我们选择
作为我们找到的元素,即原数组的第
个元素。
3) 接下来我们寻找一个长度为
的单调递增子序列的尾元素,寻找方法与步骤2)一样。我们找到的元素值为
,即原数组的第
个元素。
4) 最后我们要寻找一个长度为
的单调递增子序列的尾元素。我们找到的元素为
,即原数组的第
个元素。
按以上迭代步骤,我们找到一个完整的LMIS为
。
如果我们在迭代过程中要寻找一个长度为
的单调递增子序列的尾元素,可以采用一种简单的方法。从后向前遍历
,首先遇到的满足
的元素作为我们找到的元素
。这个元素必定小于上一步迭代过程中找到的长度为
的单调递增子序列的尾元素
。因为如果不满足
,那么在将
插入到有序数组
中时,由于
,
会取代
,或者取代
之前的某个元素,那么
就不是一个长度为
的单调递增子序列的尾元素,这推导出了矛盾。因此
必然成立。
综上所述,我们可以形成一个算法,下面给出了伪代码。
下面分析该算法的时间复杂度。LMIS包含一重循环,每次循环迭代会调用一次
。而
是一个二分查找,它的时间复杂度为
。故LMIS的时间复杂度为
。
也只包含一重循环,每次循环迭代为常数时间,故
的时间复杂度为
。综上,整个算法的时间复杂度为
。
本节相关的code链接。
https://github.com/yangtzhou2012/Introduction_to_Algorithms_3rd/tree/master/Chapter15/Section_15.4