《算法导论》读书笔记与习题解答 — 15.4 最长公共子序列
笔记
最长公共子序列(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