算法复习——动态规划篇之最长公共子序列问题
以下内容主要参考中国大学MOOC《算法设计与分析》,墙裂推荐希望入门算法的童鞋学习!
1. 问题背景
子序列:将给定序列中零个或多个元素(如字符)去掉后所得结果
公共子序列:
-
给定两个序列 X X X和 Y Y Y
-
公共子序列示例
问题是如何求两个给定序列的最长公共子序列?
2. 问题定义
最长公共子序列问题(Longest Common Subsequence Problem)
输入:
- 序列 X = < x 1 , x 2 , … , x n > X=<x_{1}, x_{2}, \dots, x_{n}> X=<x1,x2,…,xn>和序列 Y = < y 1 , y 2 , … , y m > Y=<y_{1}, y_{2}, \dots, y_{m}> Y=<y1,y2,…,ym>
输出:
-
求解一个功能子序列, Z = < z 1 , z 2 , … , z l > Z=<z_{1}, z_{2}, \dots, z_{l}> Z=<z1,z2,…,zl>,令 m a x ∣ Z ∣ max|Z| max∣Z∣, s . t . < z 1 , z 2 , … , z l > = < x i 1 , x i 2 , … , x i l > = < y j 1 , y j 2 , … , y j l > s.t.<z_1,z_2,\dots,z_l>=<x_{i_1},x_{i_2},\dots,x_{i_l}>=<y_{j_1},y_{j_2},\dots,y_{j_l}> s.t.<z1,z2,…,zl>=<xi1,xi2,…,xil>=<yj1,yj2,…,yjl>
( 1 ≤ i 1 < i 2 , … , i l ≤ n ; 1 ≤ j 1 < j 2 , … , j l ≤ m ) (1 \leq i_1 < i_2, \dots, i_l \leq n;1 \leq j_1 < j_2, \dots, j_l \leq m) (1≤i1<i2,…,il≤n;1≤j1<j2,…,jl≤m)
3. 枚举观察
可能存在最优子结构和重叠子问题。
4. 动态规划
4.1 问题结构分析
-
给出问题表示
- C [ i , j ] C[i,j] C[i,j]: X [ 1.. i ] X[1..i] X[1..i]和 Y [ 1.. j ] Y[1..j] Y[1..j]的最长公共子序列长度
-
明确原始问题
- C [ n , m ] C[n,m] C[n,m]: X [ 1.. n ] X[1..n] X[1..n]和 Y [ 1.. m ] Y[1..m] Y[1..m]的最长公共子序列长度
4.2 递推关系建立
4.2.1 分析最优(子)结构
考察末尾字符
-
x i ≠ y j x_i \neq y_j xi=yj:
C [ i , j ] = m a x { C [ i − 1 , j ] , C [ i , j − 1 ] } C[i,j]=max\{C[i-1,j],C[i,j-1]\} C[i,j]=max{C[i−1,j],C[i,j−1]}
-
x i = y j x_i = y_j xi=yj:
那是不是三个子问题都要去求解呢?实则不然,我们会发现:
- C [ i − 1 , j ] C[i-1,j] C[i−1,j]比 C [ i − 1 , j − 1 ] C[i-1,j-1] C[i−1,j−1]至多大1( C [ i − 1 , j ] ≤ C [ i − 1 , j − 1 ] + 1 C[i-1,j] \leq C[i-1,j-1]+1 C[i−1,j]≤C[i−1,j−1]+1 )
- C [ i , j − 1 ] C[i,j-1] C[i,j−1]比 C [ i − 1 , j − 1 ] C[i-1,j-1] C[i−1,j−1]至多大1( C [ i , j − 1 ] ≤ C [ i − 1 , j − 1 ] + 1 C[i,j-1] \leq C[i-1,j-1]+1 C[i,j−1]≤C[i−1,j−1]+1)
所以在末尾字符相同时,只需要 C [ i , j ] = C [ i − 1 , j − 1 ] + 1 C[i,j]=C[i-1,j-1]+1 C[i,j]=C[i−1,j−1]+1这个递推式即可
4.2.2 构造递推公式
C [ i , j ] = { m a x { C [ i − 1 , j ] , C [ i , j − 1 ] } , x i ≠ y j C [ i − 1 , j − 1 ] + 1 , x i = y j C[i,j]=\left\{ \begin{array}{rcl} max\{C[i-1,j],C[i,j-1]\}, & & {x_i \neq y_j}\\ C[i-1,j-1]+1, & & {x_i = y_j}\\ \end{array} \right. C[i,j]={max{C[i−1,j],C[i,j−1]},C[i−1,j−1]+1,xi=yjxi=yj
4.3 自底向上计算
4.3.1 确定计算顺序
-
初始化: C [ i , 0 ] = C [ 0 , i ] = 0 C[i,0]=C[0,i]=0 C[i,0]=C[0,i]=0(某序列长度为0时,最长公共子序列长度为0)
-
递推公式:
C [ i , j ] = { m a x { C [ i − 1 , j ] , C [ i , j − 1 ] } , x i ≠ y j C [ i − 1 , j − 1 ] + 1 , x i = y j C[i,j]=\left\{ \begin{array}{rcl} max\{C[i-1,j],C[i,j-1]\}, & & {x_i \neq y_j}\\ C[i-1,j-1]+1, & & {x_i = y_j}\\ \end{array} \right. C[i,j]={max{C[i−1,j],C[i,j−1]},C[i−1,j−1]+1,xi=yjxi=yj
4.3.2 依次求解问题
4.4 最优方案追踪
4.4.1 记录决策过程
构造追踪数组 r e c [ 1.. n , 1.. m ] rec[1..n,1..m] rec[1..n,1..m],记录子问题来源。
- 如果 r e c [ i , j ] = L U rec[i,j]=LU rec[i,j]=LU,最长公共子序列末尾为 X [ i ] = Y [ j ] X[i]=Y[j] X[i]=Y[j]
- 如果 r e c [ i , j ] = U rec[i,j]=U rec[i,j]=U,最长公共子序列在 X [ 1.. i − 1 ] X[1..i-1] X[1..i−1]和 Y [ 1.. j ] Y[1..j] Y[1..j]中
- 如果 r e c [ i , j ] = L rec[i,j]=L rec[i,j]=L,最长公共子序列在 X [ 1.. i ] X[1..i] X[1..i]和 Y [ 1.. j − 1 ] Y[1..j-1] Y[1..j−1]中
r e c [ i , j ] = { L U , i f C [ i , j ] = C [ i − 1 , j − 1 ] + 1 U , i f C [ i , j ] = C [ i − 1 , j ] L , i f C [ i , j ] = C [ i , j − 1 ] rec[i,j]=\left\{ \begin{array}{rcl} LU, & & {if\ C[i,j]=C[i-1,j-1]+1}\\ U, & & {if\ C[i,j]=C[i-1,j]}\\ L, & & {if\ C[i,j]=C[i,j-1]} \end{array} \right. rec[i,j]=⎩⎨⎧LU,U,L,if C[i,j]=C[i−1,j−1]+1if C[i,j]=C[i−1,j]if C[i,j]=C[i,j−1]
4.4.2 输出最优方案
5. 伪代码
Longest-Common-Subsequence(X, Y)
输入:两个序列X,Y
输出:X和Y的最长公共子序列
n ← length(X)
m ← length(Y)
// 初始化
新建二维数组C[0..n, 0..m]和rec[0..n, 0..m]
for i ← 0 to n do
C[i, 0] ← 0
end
for j ← 0 to m do
C[0, j] ← 0
end
// 动态规划
for i ← 1 to n do
for j ← 1 to m do
if X[i] = Y[j] then
C[i, j] ← C[i-1, j-1] + 1
rec[i, j] ← "LU"
end
else if C[i-1, j] >= C[i, j-1] then
C[i, j] ← C[i-1, j]
rec[i, j] ← "U"
end
else
C[i, j] ← C[i, j-1]
rec[i, j] ← "L"
end
end
end
return C, rec
Print-LCS(rec, X, i, j)
输入:追踪数组rec,序列X,当前位置i和j
输出:X[1…i]和Y[1…j]的最长公共子序列
if i = 0 or j = 0 then
return NULL
end
if rec[i, j] = "LU" then
Pring-LCS(rec, X, i-1, j-1)
print X[i]
end
else if rec[i, j] = "U" then
Print-LCS(rec, X, i-1, j)
end
else
Print-LCS(rec, X, i, j-1)
end
该算法的时间复杂度是 O ( n m ) O(nm) O(nm)。