【算法】动态规划 最长公共子序列(LCS)

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

CAABABCABBCBABDAB 都是公共子序列。其中 BCABBCBABDAB 的长度均为 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)

上述链条说明:较长公共子序列中包含较短公共子序列,提示问题可能具有最优子结构;而不同删除路径会反复到达相同的前缀问题,提示存在重叠子问题。

由此可以观察到:

  1. 最优子结构:原问题的最优解能够由规模更小的前缀问题的最优解构成。
  2. 重叠子问题:不同递归路径会反复求解相同的前缀组合。

因此,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 平局时的路径选择

当上方与左侧长度相同时,选择 UL 都能得到一个最优解,但可能得到不同的 LCS。LCS 的最优长度是确定的,具体序列不一定唯一。


8. 完整示例

text 复制代码
X = ABCBDAB
Y = BDCABA

动态规划表右下角为:

C76=4. C76=4. C76=4.

所以 LCS 长度为 4。按照决策表进行追踪,可以得到:

text 复制代码
BCBA

此外,BCABBDAB 也都是长度为 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 语言实现说明

  1. C 字符串采用 0 下标,状态表采用 1 下标,因此比较时使用 x[i - 1]y[j - 1]
  2. crec 使用一维连续内存模拟二维数组,能够处理运行时才能确定的序列长度。
  3. calloc 自动将长度表初始化为 0,因此第 0 行和第 0 列天然满足边界条件。
  4. LUUL 分别代表左上、上方和左侧。
  5. 递归在返回阶段输出字符,因此不需要额外反转字符串。
  6. 动态分配的内存最后必须使用 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 进行归纳证明。

  1. 边界成立:任意一个前缀为空时,LCS 长度为 0。
  2. 末尾相等 :若 xi=yjx_i=y_jxi=yj,把该元素接到两个更短前缀的 LCS 后,可以得到长度 Ci−1j−1+1Ci-1j-1+1Ci−1j−1+1 的公共子序列;任何公共子序列都不可能比它更长。
  3. 末尾不等 :公共子序列不可能同时使用两个不同的末尾元素,因此最优解必然落在"舍弃 xix_ixi"或"舍弃 yjy_jyj"的子问题中。
  4. 根据归纳假设,依赖状态已经是对应子问题的最优值,所以递推得到的 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,末尾不等时取上方和左侧的较大值;填完整张表后,再沿决策方向从右下角反向追踪,即可恢复一个最长公共子序列。

相关推荐
迷途呀6 小时前
Python:函数中的参数类型
开发语言·笔记·python·langchain·nlp
.徐十三.6 小时前
一篇文章看到最短路径——Dijkstra算法
算法
依然范特东7 小时前
动手学深度学习笔记--卷积层、微调
人工智能·笔记·深度学习
科学实验家8 小时前
二叉树的几道题
算法
金伟API10248 小时前
SQL Server基础学习笔记
笔记·学习
abcy0712138 小时前
flink state实例
大数据·算法·flink
qizayaoshuap8 小时前
# [特殊字符] 密码生成器 — 鸿蒙ArkTS安全算法与密码强度评估系统
java·算法·安全·华为·harmonyos
良木林8 小时前
滑动窗口 - LeetCode hot 100
javascript·算法·leetcode·双指针·滑动窗口