###笔记
最长公共子序列(Longest-Common-Subsequence, LCS)问题:给定两个序列
X
m
=
<
x
1
,
x
2
,
…
,
x
m
>
X_m = <x_1, x_2, …, x_m>
Xm=<x1,x2,…,xm>和
Y
n
=
<
y
1
,
y
2
,
…
,
y
n
>
Y_n = <y_1, y_2, …, y_n>
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
=
<
z
1
,
z
2
,
…
,
z
k
>
Z = <z_1, z_2, …, z_k>
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
=
<
z
1
,
z
2
,
…
,
z
k
−
1
>
Z_{k-1} = <z_1, z_2, …, z_{k-1}>
Zk−1=<z1,z2,…,zk−1>是
X
m
−
1
=
<
x
1
,
x
2
,
…
,
x
m
−
1
>
X_{m-1} = <x_1, x_2, …, x_{m-1}>
Xm−1=<x1,x2,…,xm−1>和
Y
n
−
1
=
<
y
1
,
y
2
,
…
,
y
n
−
1
>
Y_{n-1} = <y_1, y_2, …, y_{n-1}>
Yn−1=<y1,y2,…,yn−1>的一个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}
Xm−1和
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}
Yn−1的一个LCS。
根据以上事实,可以递归地求解LCS问题。如果 x m = y n x_m = y_n xm=yn,递归求解 X m − 1 X_{m-1} Xm−1和 Y n − 1 Y_{n-1} Yn−1的LCS,将 x m x_m xm加到 X m − 1 X_{m-1} Xm−1和 Y n − 1 Y_{n-1} Yn−1的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} Xm−1和 Y n Y_n Yn的LCS、 X m X_m Xm和 Y n − 1 Y_{n-1} Yn−1的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
>
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}
c[i,j]=⎩⎪⎨⎪⎧0c[i−1,j−1]+1max(c[i,j−1],c[i−1,j])i=0或j=0i,j>0且xi=yji,j>0且xi̸=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
=
<
x
1
,
x
2
,
…
,
x
m
>
X = <x_1, x_2, …, x_m>
X=<x1,x2,…,xm>和
Y
=
<
y
1
,
y
2
,
…
,
y
n
>
Y = <y_1, y_2, …, y_n>
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[i−1,j−1]、
c
[
i
−
1
,
j
]
c[i-1, j]
c[i−1,j]和
c
[
i
,
j
−
1
]
c[i, j-1]
c[i,j−1]。假设存储空间只有一行。我们是按照从左到右的顺序来计算一行的表项,当计算到
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[i−1,j],而
c
[
i
,
j
−
1
]
c[i, j-1]
c[i,j−1]在
c
[
i
,
j
]
c[i, j]
c[i,j]之前已经被计算得到。现在只剩下
c
[
i
−
1
,
j
−
1
]
c[i-1, j-1]
c[i−1,j−1],可以额外用一个变量
t
t
t来保存
c
[
i
−
1
,
j
−
1
]
c[i-1, j-1]
c[i−1,j−1]。这样,计算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
i−1的候选子序列的尾元素小。因此,可以在输入序列中将候选子序列链接起来。)
解
假设原数列为
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
=
<
9
,
1
,
2
,
4
,
6
,
3
,
5
,
0
>
X = <9, 1, 2, 4, 6, 3, 5, 0>
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] |
---|---|---|---|
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
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为
<
1
,
2
,
3
,
5
>
<1, 2, 3, 5>
<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
]
<
X
[
i
k
+
1
]
X[i_k] < 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
]
<
X
[
i
k
+
1
]
X[i_k] < 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