1. 问题背景
1.1 子序列
给定序列
X=⟨x1,x2,...,xn⟩, X=\langle x_1,x_2,\ldots,x_n\rangle, X=⟨x1,x2,...,xn⟩,
删除其中零个或多个元素,同时保持剩余元素在原序列中的相对次序,所得序列称为 XXX 的一个子序列。
例如,对于 ABCBDAB:
ABCB是子序列;ACBB是子序列,字符不必连续;BAC不是子序列,因为改变了字符的先后顺序。
子序列与子串的区别
子序列只要求相对顺序不变,不要求连续;子串要求元素连续出现。
1.2 公共子序列
如果序列 ZZZ 同时是 XXX 与 YYY 的子序列,则称 ZZZ 为它们的公共子序列。
使用下面这组示例:
text
X = ABCBDAB
Y = BDCABA
CA、ABA、BCAB、BCBA 和 BDAB 都是公共子序列。其中 BCAB、BCBA、BDAB 的长度均为 4,因此都是最长公共子序列。
1.3 最长公共子序列问题
输入:
X=⟨x1,x2,...,xn⟩,Y=⟨y1,y2,...,ym⟩. X=\langle x_1,x_2,\ldots,x_n\rangle, \qquad Y=\langle y_1,y_2,\ldots,y_m\rangle. X=⟨x1,x2,...,xn⟩,Y=⟨y1,y2,...,ym⟩.
输出:一个公共子序列
Z=⟨z1,z2,...,zl⟩, Z=\langle z_1,z_2,\ldots,z_l\rangle, Z=⟨z1,z2,...,zl⟩,
使 ∣Z∣|Z|∣Z∣ 最大,即
max∣Z∣, \max |Z|, max∣Z∣,
满足
Z=⟨xi1,xi2,...,xil⟩=⟨yj1,yj2,...,yjl⟩, Z=\langle x_{i_1},x_{i_2},\ldots,x_{i_l}\rangle =\langle y_{j_1},y_{j_2},\ldots,y_{j_l}\rangle, Z=⟨xi1,xi2,...,xil⟩=⟨yj1,yj2,...,yjl⟩,
其中
1≤i1<i2<⋯<il≤n,1≤j1<j2<⋯<jl≤m. 1\le i_1<i_2<\cdots<i_l\le n, \qquad 1\le j_1<j_2<\cdots<j_l\le m. 1≤i1<i2<⋯<il≤n,1≤j1<j2<⋯<jl≤m.
2. 蛮力枚举与动态规划动机
最直接的方法是枚举 XXX 的所有子序列,再检查它们是否也是 YYY 的子序列,并保留长度最大的结果。
长度为 nnn 的序列共有 2n2^n2n 个子序列,因此蛮力方法的规模至少是指数级,数据稍大就不可行。
从公共子序列 BDAB 开始继续删除字符:
text
BDAB 长度 4
DAB 长度 3
AB 长度 2
B 长度 1
text
最长公共子序列候选:BDAB(长度 4)
│ 删除首字符 B
▼
DAB(长度 3)
│ 删除首字符 D
▼
AB(长度 2)
│ 删除首字符 A
▼
B(长度 1)
上述链条说明:较长公共子序列中包含较短公共子序列,提示问题可能具有最优子结构;而不同删除路径会反复到达相同的前缀问题,提示存在重叠子问题。
由此可以观察到:
- 最优子结构:原问题的最优解能够由规模更小的前缀问题的最优解构成。
- 重叠子问题:不同递归路径会反复求解相同的前缀组合。
因此,LCS 适合使用动态规划求解。
3. 动态规划整体流程
#mermaid-svg-OKbQ7Vw7R1h8cS9D{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-OKbQ7Vw7R1h8cS9D .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .error-icon{fill:#552222;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .marker.cross{stroke:#333333;}#mermaid-svg-OKbQ7Vw7R1h8cS9D svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OKbQ7Vw7R1h8cS9D p{margin:0;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .cluster-label text{fill:#333;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .cluster-label span{color:#333;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .cluster-label span p{background-color:transparent;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .label text,#mermaid-svg-OKbQ7Vw7R1h8cS9D span{fill:#333;color:#333;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .node rect,#mermaid-svg-OKbQ7Vw7R1h8cS9D .node circle,#mermaid-svg-OKbQ7Vw7R1h8cS9D .node ellipse,#mermaid-svg-OKbQ7Vw7R1h8cS9D .node polygon,#mermaid-svg-OKbQ7Vw7R1h8cS9D .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .rough-node .label text,#mermaid-svg-OKbQ7Vw7R1h8cS9D .node .label text,#mermaid-svg-OKbQ7Vw7R1h8cS9D .image-shape .label,#mermaid-svg-OKbQ7Vw7R1h8cS9D .icon-shape .label{text-anchor:middle;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .rough-node .label,#mermaid-svg-OKbQ7Vw7R1h8cS9D .node .label,#mermaid-svg-OKbQ7Vw7R1h8cS9D .image-shape .label,#mermaid-svg-OKbQ7Vw7R1h8cS9D .icon-shape .label{text-align:center;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .node.clickable{cursor:pointer;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .arrowheadPath{fill:#333333;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-OKbQ7Vw7R1h8cS9D .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OKbQ7Vw7R1h8cS9D .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-OKbQ7Vw7R1h8cS9D .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .cluster text{fill:#333;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .cluster span{color:#333;}#mermaid-svg-OKbQ7Vw7R1h8cS9D div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OKbQ7Vw7R1h8cS9D rect.text{fill:none;stroke-width:0;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .icon-shape,#mermaid-svg-OKbQ7Vw7R1h8cS9D .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .icon-shape p,#mermaid-svg-OKbQ7Vw7R1h8cS9D .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .icon-shape .label rect,#mermaid-svg-OKbQ7Vw7R1h8cS9D .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OKbQ7Vw7R1h8cS9D .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-OKbQ7Vw7R1h8cS9D .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-OKbQ7Vw7R1h8cS9D :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 问题结构分析
递推关系建立
自底向上计算
最优方案追踪
4. 问题结构分析:定义状态
定义
Cij=LCSLen(X1..i,Y1..j), Cij=\operatorname{LCSLen}(X1..i,Y1..j), Cij=LCSLen(X1..i,Y1..j),
即 XXX 的前 iii 个元素与 YYY 的前 jjj 个元素的最长公共子序列长度。
原问题就是求:
Cnm. Cnm. Cnm.
注意:CijCijCij 只记录最优长度。如果还要输出具体的 LCS,需要额外记录每个状态来自哪个子问题。
5. 递推关系建立
考察两个前缀的末尾元素 xix_ixi 与 yjy_jyj。
5.1 情况一:xi≠yjx_i\ne y_jxi=yj
两个末尾元素不同,不可能同时成为当前公共子序列的最后一个元素。最优解至少需要舍弃其中一个末尾元素:
- 舍弃 xix_ixi,得到子问题 Ci−1jCi-1jCi−1j;
- 舍弃 yjy_jyj,得到子问题 Cij−1Cij-1Cij−1。
因此:
Cij=max{Ci−1j,Cij−1}. Cij=\max\{Ci-1j,Cij-1\}. Cij=max{Ci−1j,Cij−1}.
5.2 情况二:xi=yjx_i=y_jxi=yj
两个末尾元素相等,可以把这个元素接到两个更短前缀的 LCS 后面:
Cij=Ci−1j−1+1. Cij=Ci-1j-1+1. Cij=Ci−1j−1+1.
这里不需要再与 Ci−1jCi-1jCi−1j、Cij−1Cij-1Cij−1 比较。增加一个元素后,LCS 长度至多增加 1;当前末尾元素相等时,Ci−1j−1+1Ci-1j-1+1Ci−1j−1+1 已经能够达到最优值。
5.3 完整递推式
Cij={0,i=0 或 j=0,Ci−1j−1+1,xi=yj,max{Ci−1j,Cij−1},xi≠yj. Cij= \begin{cases} 0, & i=0\text{ 或 }j=0,\\ Ci-1j-1+1, & x_i=y_j,\\ \max\{Ci-1j,Cij-1\}, & x_i\ne y_j. \end{cases} Cij=⎩ ⎨ ⎧0,Ci−1j−1+1,max{Ci−1j,Cij−1},i=0 或 j=0,xi=yj,xi=yj.
| 末尾关系 | 当前决策 | 来源状态 | 当前值 |
|---|---|---|---|
| xi=yjx_i=y_jxi=yj | 同时保留两个相等的末尾字符 | 左上方 Ci−1j−1Ci-1j-1Ci−1j−1 | Ci−1j−1+1Ci-1j-1+1Ci−1j−1+1 |
| xi≠yjx_i\ne y_jxi=yj | 舍弃 xix_ixi | 上方 Ci−1jCi-1jCi−1j | 候选值一 |
| xi≠yjx_i\ne y_jxi=yj | 舍弃 yjy_jyj | 左侧 Cij−1Cij-1Cij−1 | 候选值二 |
| xi≠yjx_i\ne y_jxi=yj | 比较两种舍弃方案 | 上方与左侧 | 取二者最大值 |
状态依赖关系如下:
#mermaid-svg-nL7afGCdGhDfH1an{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nL7afGCdGhDfH1an .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nL7afGCdGhDfH1an .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nL7afGCdGhDfH1an .error-icon{fill:#552222;}#mermaid-svg-nL7afGCdGhDfH1an .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nL7afGCdGhDfH1an .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nL7afGCdGhDfH1an .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nL7afGCdGhDfH1an .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nL7afGCdGhDfH1an .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nL7afGCdGhDfH1an .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nL7afGCdGhDfH1an .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nL7afGCdGhDfH1an .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nL7afGCdGhDfH1an .marker.cross{stroke:#333333;}#mermaid-svg-nL7afGCdGhDfH1an svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nL7afGCdGhDfH1an p{margin:0;}#mermaid-svg-nL7afGCdGhDfH1an .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-nL7afGCdGhDfH1an .cluster-label text{fill:#333;}#mermaid-svg-nL7afGCdGhDfH1an .cluster-label span{color:#333;}#mermaid-svg-nL7afGCdGhDfH1an .cluster-label span p{background-color:transparent;}#mermaid-svg-nL7afGCdGhDfH1an .label text,#mermaid-svg-nL7afGCdGhDfH1an span{fill:#333;color:#333;}#mermaid-svg-nL7afGCdGhDfH1an .node rect,#mermaid-svg-nL7afGCdGhDfH1an .node circle,#mermaid-svg-nL7afGCdGhDfH1an .node ellipse,#mermaid-svg-nL7afGCdGhDfH1an .node polygon,#mermaid-svg-nL7afGCdGhDfH1an .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-nL7afGCdGhDfH1an .rough-node .label text,#mermaid-svg-nL7afGCdGhDfH1an .node .label text,#mermaid-svg-nL7afGCdGhDfH1an .image-shape .label,#mermaid-svg-nL7afGCdGhDfH1an .icon-shape .label{text-anchor:middle;}#mermaid-svg-nL7afGCdGhDfH1an .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-nL7afGCdGhDfH1an .rough-node .label,#mermaid-svg-nL7afGCdGhDfH1an .node .label,#mermaid-svg-nL7afGCdGhDfH1an .image-shape .label,#mermaid-svg-nL7afGCdGhDfH1an .icon-shape .label{text-align:center;}#mermaid-svg-nL7afGCdGhDfH1an .node.clickable{cursor:pointer;}#mermaid-svg-nL7afGCdGhDfH1an .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-nL7afGCdGhDfH1an .arrowheadPath{fill:#333333;}#mermaid-svg-nL7afGCdGhDfH1an .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-nL7afGCdGhDfH1an .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-nL7afGCdGhDfH1an .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nL7afGCdGhDfH1an .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-nL7afGCdGhDfH1an .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nL7afGCdGhDfH1an .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-nL7afGCdGhDfH1an .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-nL7afGCdGhDfH1an .cluster text{fill:#333;}#mermaid-svg-nL7afGCdGhDfH1an .cluster span{color:#333;}#mermaid-svg-nL7afGCdGhDfH1an div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-nL7afGCdGhDfH1an .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-nL7afGCdGhDfH1an rect.text{fill:none;stroke-width:0;}#mermaid-svg-nL7afGCdGhDfH1an .icon-shape,#mermaid-svg-nL7afGCdGhDfH1an .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nL7afGCdGhDfH1an .icon-shape p,#mermaid-svg-nL7afGCdGhDfH1an .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-nL7afGCdGhDfH1an .icon-shape .label rect,#mermaid-svg-nL7afGCdGhDfH1an .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nL7afGCdGhDfH1an .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-nL7afGCdGhDfH1an .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-nL7afGCdGhDfH1an :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 末尾相等:+1
末尾不等:取最大值
末尾不等:取最大值
Ci-1j-1
Cij
Ci-1j
Cij-1
6. 自底向上计算
6.1 初始化
当任意一个序列长度为 0 时,公共子序列只能是空序列:
Ci0=0,C0j=0. Ci0=0, \qquad C0j=0. Ci0=0,C0j=0.
6.2 填表顺序
每个 CijCijCij 只依赖:
- 左上方 Ci−1j−1Ci-1j-1Ci−1j−1;
- 上方 Ci−1jCi-1jCi−1j;
- 左侧 Cij−1Cij-1Cij−1。
因此,可以从上到下、从左到右依次填表:外层遍历 i=1...ni=1\ldots ni=1...n,内层遍历 j=1...mj=1\ldots mj=1...m。
text
j 从 0 到 m
0 1 2 3 ... m
0 0 0 0 0 ... 0
1 0 → → → ... →
i 2 0 → → → ... →
从 3 0 → → → ... →
0 ... ... ... ... ... ... ...
到 n 0 → → → ... C[n][m]
n
每个格子只读取:↖ 左上、↑ 上方、← 左侧。
因此按行从左到右填表时,当前格子的所有依赖状态都已经计算完成。
6.3 计算伪代码
text
Longest-Common-Subsequence(X, Y)
n <- length(X)
m <- length(Y)
for i <- 0 to n
C[i][0] <- 0
for j <- 0 to m
C[0][j] <- 0
for i <- 1 to n
for j <- 1 to m
if X[i] = Y[j]
C[i][j] <- C[i-1][j-1] + 1
rec[i][j] <- LU
else if C[i-1][j] >= C[i][j-1]
C[i][j] <- C[i-1][j]
rec[i][j] <- U
else
C[i][j] <- C[i][j-1]
rec[i][j] <- L
return C, rec
其中:
LU:状态来自左上方;U:状态来自上方;L:状态来自左侧。
7. 最优方案追踪
只知道 CnmCnmCnm 只能得到 LCS 的长度。为了输出一个具体的最长公共子序列,需要记录决策方向:
recij={LU,xi=yj,U,xi≠yj 且 Ci−1j≥Cij−1,L,xi≠yj 且 Ci−1j<Cij−1. recij= \begin{cases} LU, & x_i=y_j,\\ U, & x_i\ne y_j\text{ 且 }Ci-1j\ge Cij-1,\\ L, & x_i\ne y_j\text{ 且 }Ci-1j<Cij-1. \end{cases} recij=⎩ ⎨ ⎧LU,U,L,xi=yj,xi=yj 且 Ci−1j≥Cij−1,xi=yj 且 Ci−1j<Cij−1.
rec[i][j] |
移动方向 | 是否记录字符 | 含义 |
|---|---|---|---|
LU |
↖ 左上 | 是,记录 xix_ixi | xi=yjx_i=y_jxi=yj |
U |
↑ 上方 | 否 | 舍弃 xix_ixi |
L |
← 左侧 | 否 | 舍弃 yjy_jyj |
text
从 (n,m) 出发
│
├─ LU:记录 X[i],移动到 (i-1,j-1)
├─ U :不记录,移动到 (i-1,j)
└─ L :不记录,移动到 (i,j-1)
到达第 0 行或第 0 列时结束。
从右下角 (n,m)(n,m)(n,m) 开始反向移动:
- 遇到
LU:当前元素属于 LCS,记录 xix_ixi,移动到 (i−1,j−1)(i-1,j-1)(i−1,j−1); - 遇到
U:不输出元素,移动到 (i−1,j)(i-1,j)(i−1,j); - 遇到
L:不输出元素,移动到 (i,j−1)(i,j-1)(i,j−1); - 当 i=0i=0i=0 或 j=0j=0j=0 时结束。
由于追踪方向是从后向前,迭代实现需要将结果反转;递归实现可以在递归返回时输出,从而自然得到正序。
text
Print-LCS(rec, X, i, j)
if i = 0 or j = 0
return
if rec[i][j] = LU
Print-LCS(rec, X, i-1, j-1)
output X[i]
else if rec[i][j] = U
Print-LCS(rec, X, i-1, j)
else
Print-LCS(rec, X, i, j-1)
!warning 平局时的路径选择
当上方与左侧长度相同时,选择
U或L都能得到一个最优解,但可能得到不同的 LCS。LCS 的最优长度是确定的,具体序列不一定唯一。
8. 完整示例
text
X = ABCBDAB
Y = BDCABA
动态规划表右下角为:
C76=4. C76=4. C76=4.
所以 LCS 长度为 4。按照决策表进行追踪,可以得到:
text
BCBA
此外,BCAB、BDAB 也都是长度为 4 的最长公共子序列。不同结果来自决策值相等时采用了不同的方向。
完整长度表 CCC
行对应 X=extABCBDABX= ext{ABCBDAB}X=extABCBDAB,列对应 Y=extBDCABAY= ext{BDCABA}Y=extBDCABA。
| CijCijCij | 0 | B | D | C | A | B | A |
|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| A | 0 | 0 | 0 | 0 | 1 | 1 | 1 |
| B | 0 | 1 | 1 | 1 | 1 | 2 | 2 |
| C | 0 | 1 | 1 | 2 | 2 | 2 | 2 |
| B | 0 | 1 | 1 | 2 | 2 | 3 | 3 |
| D | 0 | 1 | 2 | 2 | 2 | 3 | 3 |
| A | 0 | 1 | 2 | 2 | 3 | 3 | 4 |
| B | 0 | 1 | 2 | 2 | 3 | 4 | 4 |
右下角 C76=4C76=4C76=4,所以最长公共子序列长度为 4。
一条回溯路径
下面采用"上方与左侧相等时优先向上"的规则:
text
(7,6) X[7]=B, Y[6]=A,不等,向上 ↑
(6,6) X[6]=A, Y[6]=A,相等,记录 A,左上 ↖
(5,5) X[5]=D, Y[5]=B,不等,向上 ↑
(4,5) X[4]=B, Y[5]=B,相等,记录 B,左上 ↖
(3,4) X[3]=C, Y[4]=A,不等,向左 ←
(3,3) X[3]=C, Y[3]=C,相等,记录 C,左上 ↖
(2,2) X[2]=B, Y[2]=D,不等,向左 ←
(2,1) X[2]=B, Y[1]=B,相等,记录 B,左上 ↖
(1,0) 到达边界,结束
回溯时依次记录 A、B、C、B,反转后得到:
text
BCBA
手算检查
BCBA 在两个序列中对应的下标分别可以取:
text
X = A B C B D A B
2 3 4 6
Y = B D C A B A
1 3 5 6
两组下标都严格递增,因此 BCBA 是公共子序列;动态规划表给出的最优长度为 4,所以它是一个 LCS。
9. C 语言实现
下面的实现先构造长度表 c 与方向表 rec,再使用递归输出一个 LCS。
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
enum Direction {
NONE = 0,
LU, /* 左上:当前字符相等 */
U, /* 上方 */
L /* 左侧 */
};
typedef struct {
int rows;
int cols;
int *c;
unsigned char *rec;
} LCSResult;
#define C_AT(r, i, j) ((r)->c[(i) * (r)->cols + (j)])
#define REC_AT(r, i, j) ((r)->rec[(i) * (r)->cols + (j)])
int build_lcs_tables(const char *x, const char *y, LCSResult *result) {
int n = (int)strlen(x);
int m = (int)strlen(y);
int i, j;
result->rows = n + 1;
result->cols = m + 1;
result->c = (int *)calloc((size_t)(n + 1) * (m + 1), sizeof(int));
result->rec = (unsigned char *)calloc(
(size_t)(n + 1) * (m + 1), sizeof(unsigned char));
if (result->c == NULL || result->rec == NULL) {
free(result->c);
free(result->rec);
result->c = NULL;
result->rec = NULL;
return 0;
}
/* calloc 已将第 0 行和第 0 列初始化为 0 */
for (i = 1; i <= n; ++i) {
for (j = 1; j <= m; ++j) {
if (x[i - 1] == y[j - 1]) {
C_AT(result, i, j) = C_AT(result, i - 1, j - 1) + 1;
REC_AT(result, i, j) = LU;
} else if (C_AT(result, i - 1, j) >=
C_AT(result, i, j - 1)) {
C_AT(result, i, j) = C_AT(result, i - 1, j);
REC_AT(result, i, j) = U;
} else {
C_AT(result, i, j) = C_AT(result, i, j - 1);
REC_AT(result, i, j) = L;
}
}
}
return 1;
}
void print_lcs_recursive(const LCSResult *result,
const char *x,
int i,
int j) {
if (i == 0 || j == 0) {
return;
}
if (REC_AT(result, i, j) == LU) {
print_lcs_recursive(result, x, i - 1, j - 1);
putchar(x[i - 1]);
} else if (REC_AT(result, i, j) == U) {
print_lcs_recursive(result, x, i - 1, j);
} else {
print_lcs_recursive(result, x, i, j - 1);
}
}
void free_lcs_result(LCSResult *result) {
free(result->c);
free(result->rec);
result->c = NULL;
result->rec = NULL;
}
int main(void) {
const char *x = "ABCBDAB";
const char *y = "BDCABA";
LCSResult result = {0, 0, NULL, NULL};
int n = (int)strlen(x);
int m = (int)strlen(y);
if (!build_lcs_tables(x, y, &result)) {
fprintf(stderr, "内存分配失败\n");
return EXIT_FAILURE;
}
printf("X = %s\n", x);
printf("Y = %s\n", y);
printf("LCS length = %d\n", C_AT(&result, n, m));
printf("LCS = ");
print_lcs_recursive(&result, x, n, m);
putchar('\n');
free_lcs_result(&result);
return EXIT_SUCCESS;
}
可能输出:
text
X = ABCBDAB
Y = BDCABA
LCS length = 4
LCS = BCBA
C 语言实现说明
- C 字符串采用 0 下标,状态表采用 1 下标,因此比较时使用
x[i - 1]和y[j - 1]。 c和rec使用一维连续内存模拟二维数组,能够处理运行时才能确定的序列长度。calloc自动将长度表初始化为 0,因此第 0 行和第 0 列天然满足边界条件。LU、U、L分别代表左上、上方和左侧。- 递归在返回阶段输出字符,因此不需要额外反转字符串。
- 动态分配的内存最后必须使用
free释放。
10. C++ 实现
cpp
#include <algorithm>
#include <iostream>
#include <string>
#include <utility>
#include <vector>
using namespace std;
pair<int, string> lcs(const string& x, const string& y) {
const int n = static_cast<int>(x.size());
const int m = static_cast<int>(y.size());
vector<vector<int>> c(n + 1, vector<int>(m + 1, 0));
vector<vector<char>> rec(n + 1, vector<char>(m + 1, 0));
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (x[i - 1] == y[j - 1]) {
c[i][j] = c[i - 1][j - 1] + 1;
rec[i][j] = 'D'; // diagonal,即 LU
} else if (c[i - 1][j] >= c[i][j - 1]) {
c[i][j] = c[i - 1][j];
rec[i][j] = 'U';
} else {
c[i][j] = c[i][j - 1];
rec[i][j] = 'L';
}
}
}
string answer;
int i = n;
int j = m;
while (i > 0 && j > 0) {
if (rec[i][j] == 'D') {
answer.push_back(x[i - 1]);
--i;
--j;
} else if (rec[i][j] == 'U') {
--i;
} else {
--j;
}
}
reverse(answer.begin(), answer.end());
return {c[n][m], answer};
}
int main() {
auto [length, sequence] = lcs("ABCBDAB", "BDCABA");
cout << "LCS length = " << length << '\n';
cout << "LCS = " << sequence << '\n';
return 0;
}
11. 正确性
可以按照前缀长度 i+ji+ji+j 进行归纳证明。
- 边界成立:任意一个前缀为空时,LCS 长度为 0。
- 末尾相等 :若 xi=yjx_i=y_jxi=yj,把该元素接到两个更短前缀的 LCS 后,可以得到长度 Ci−1j−1+1Ci-1j-1+1Ci−1j−1+1 的公共子序列;任何公共子序列都不可能比它更长。
- 末尾不等 :公共子序列不可能同时使用两个不同的末尾元素,因此最优解必然落在"舍弃 xix_ixi"或"舍弃 yjy_jyj"的子问题中。
- 根据归纳假设,依赖状态已经是对应子问题的最优值,所以递推得到的 CijCijCij 也是最优值。
12. 复杂度分析
设 ∣X∣=n|X|=n∣X∣=n,∣Y∣=m|Y|=m∣Y∣=m。
- 状态数量:(n+1)(m+1)(n+1)(m+1)(n+1)(m+1);
- 每个状态的计算时间:O(1)O(1)O(1);
- 时间复杂度 :O(nm)O(nm)O(nm);
- 保存完整长度表和方向表的空间复杂度 :O(nm)O(nm)O(nm);
- 方案追踪最多移动 n+mn+mn+m 步,时间复杂度为 O(n+m)O(n+m)O(n+m)。
12.1 只求长度时的空间优化
若只需要 LCS 长度,不需要恢复具体序列,则只需保留当前行和上一行,甚至可以压缩成一维数组,空间复杂度降为:
O(min{n,m}). O(\min\{n,m\}). O(min{n,m}).
C 语言滚动数组版本
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int max_int(int a, int b) {
return a > b ? a : b;
}
int lcs_length(const char *x, const char *y) {
int n = (int)strlen(x);
int m = (int)strlen(y);
int i, j;
int *dp;
/* 让 y 是较短的序列,以降低空间占用 */
if (n < m) {
const char *temp_s = x;
int temp_n = n;
x = y;
y = temp_s;
n = m;
m = temp_n;
}
dp = (int *)calloc((size_t)m + 1, sizeof(int));
if (dp == NULL) {
return -1;
}
for (i = 1; i <= n; ++i) {
int left_up = 0;
for (j = 1; j <= m; ++j) {
int old_up = dp[j];
if (x[i - 1] == y[j - 1]) {
dp[j] = left_up + 1;
} else {
dp[j] = max_int(dp[j], dp[j - 1]);
}
left_up = old_up;
}
}
i = dp[m];
free(dp);
return i;
}
int main(void) {
printf("%d\n", lcs_length("ABCBDAB", "BDCABA"));
return 0;
}
C++ 滚动数组版本
cpp
#include <algorithm>
#include <string>
#include <vector>
using namespace std;
int lcsLength(const string& x, const string& y) {
const string *a = &x;
const string *b = &y;
if (a->size() < b->size()) {
swap(a, b);
}
vector<int> dp(b->size() + 1, 0);
for (char ch : *a) {
int leftUp = 0;
for (size_t j = 1; j <= b->size(); ++j) {
int oldUp = dp[j];
if (ch == (*b)[j - 1]) {
dp[j] = leftUp + 1;
} else {
dp[j] = max(dp[j], dp[j - 1]);
}
leftUp = oldUp;
}
}
return dp[b->size()];
}
13.总结
使用 CijCijCij 表示两个前缀的最优长度:末尾相等时取左上方加 1,末尾不等时取上方和左侧的较大值;填完整张表后,再沿决策方向从右下角反向追踪,即可恢复一个最长公共子序列。