動態規劃(DP)是解決最優化問題的利器,核心在於最優子結構與重疊子問題。本文以微軟面試經典第56題——最長公共子序列(LCS)為例,完整演示DP四步法:描述最優解結構、遞歸定義、自底向上計算、構造最優解。
給定兩字符串X = x₁x₂...xₘ、Y = y₁y₂...yₙ。若序列Z同時是X與Y的子序列(不要求連續),稱Z為公共子序列。求長度最大的Z及其長度。
例:X=BDCABA、Y=ABCBDAB,LCS長度為4,如BCBA或BDAB。
設Z = z₁z₂...zₖ為X與Y的一個LCS。三條性質:
xₘ = yₙ,則zₖ = xₘ = yₙ,且Zₖ₋₁是Xₘ₋₁與Yₙ₋₁的LCS。xₘ ≠ yₙ且zₖ ≠ xₘ,則Z是Xₘ₋₁與Y的LCS。xₘ ≠ yₙ且zₖ ≠ yₙ,則Z是X與Yₙ₋₁的LCS。均可用反證法證明:假設不成立,則能構造更長的公共子序列,與LCS定義矛盾。
定義c[i][j]為Xᵢ與Yⱼ的LCS長度(Xᵢ為前i字符)。
c[i][j] = 0 if i=0 or j=0
c[i][j] = c[i-1][j-1] + 1 if i,j>0 and xᵢ = yⱼ
c[i][j] = max(c[i-1][j], c[i][j-1]) if i,j>0 and xᵢ ≠ yⱼ
為構造具體LCS,需記錄決策方向b[i][j]:
↖:來自c[i-1][j-1](字符匹配,該字符屬於LCS)。↑:來自c[i-1][j]。←:來自c[i][j-1]。
LCS-LENGTH(X, Y)
m ← length[X], n ← length[Y]
for i ← 1 to m: c[i][0] ← 0
for j ← 0 to n: c[0][j] ← 0
for i ← 1 to m:
for j ← 1 to n:
if xᵢ = yⱼ:
c[i][j] ← c[i-1][j-1] + 1
b[i][j] ← "↖"
else if c[i-1][j] ≥ c[i][j-1]:
c[i][j] ← c[i-1][j]
b[i][j] ← "↑"
else:
c[i][j] ← c[i][j-1]
b[i][j] ← "←"
return c, b
時間O(mn),空間O(mn)(可優化至O(min(m,n)))。
PRINT-LCS(b, X, i, j)
if i = 0 or j = 0: return
if b[i][j] = "↖":
PRINT-LCS(b, X, i-1, j-1)
print xᵢ
else if b[i][j] = "↑":
PRINT-LCS(b, X, i-1, j)
else:
PRINT-LCS(b, X, i, j-1)
從b[m][n]遞歸回溯至邊界,遇↖輸出字符,即得一個LCS(可能不唯一)。
#include <stdio.h>
#include <string.h>
#define MAX 100
char X[MAX], Y[MAX];
int c[MAX][MAX];
char b[MAX][MAX];
void LCS_LENGTH() {
int m = strlen(X), n = strlen(Y);
for (int i = 0; i <= m; i++) c[i][0] = 0;
for (int j = 0; j <= n; j++) c[0][j] = 0;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (X[i-1] == Y[j-1]) {
c[i][j] = c[i-1][j-1] + 1;
b[i][j] = '\\'; // ↖
} else if (c[i-1][j] >= c[i][j-1]) {
c[i][j] = c[i-1][j];
b[i][j] = '|'; // ↑
} else {
c[i][j] = c[i][j-1];
b[i][j] = '-'; // ←
}
}
}
printf("LCS長度: %d\n", c[m][n]);
}
void PRINT_LCS(int i, int j) {
if (i == 0 || j == 0) return;
if (b[i][j] == '\\') {
PRINT_LCS(i-1, j-1);
putchar(X[i-1]);
} else if (b[i][j] == '|') {
PRINT_LCS(i-1, j);
} else {
PRINT_LCS(i, j-1);
}
}
int main() {
strcpy(X, "BDCABA");
strcpy(Y, "ABCBDAB");
LCS_LENGTH();
printf("一個LCS: ");
PRINT_LCS(strlen(X), strlen(Y));
putchar('\n');
return 0;
}
LCS長度: 4 一個LCS: BCBA
觀察遞歸式,c[i][*]僅依賴c[i-1][*]與c[i][*]自身左側。可用兩行數組滾動覆蓋,空間降至O(min(m,n));若需輸出具體LCS,需保留完整b[][]或採用Hirschberg算法(分治+正反向DP)在線性空間構造解。
掌握LCS的DP四步法,即掌握了解決所有「序列對齊/編輯距離/最長公共子串」類問題的通用框架。