最長公共子序列(LCS)是經典動態規劃問題。傳統 DP 算法雖時間複雜度為 O(mn),但空間複雜度同為 O(mn),對長字串(如 DNA 序列、大型文本比對)極不友好。1975 年,D.S. Hirschberg 提出線性空間 LCS 算法,將空間降至 O(min(m,n)),且時間仍保持 O(mn),成為後續所有線性空間序列比對算法的基石。
對長度 10,000 的字串,傳統 DP 需 100M 空間(約 100 MB),在 1975 年甚至當今嵌入式環境均難以承受。Hirschberg 算法僅需約 100 KB,使大規模序列比對成為可能。
for i = 1 to m:
for j = 1 to n:
if A[i] == B[j]:
L[i][j] = L[i-1][j-1] + 1
else:
L[i][j] = max(L[i][j-1], L[i-1][j])
此算法需完整 m×n 矩陣,空間 O(mn)。
關鍵洞見:計算 L(i,j) 僅依賴前一行。若只求 LCS 長度,只需兩行滾動數組,空間 O(min(m,n))。但要重構 LCS 本身,需知道決策路徑。
Hirschberg 算法採用分治策略:
def lcs_linear_space(A, B):
if not A or not B:
return ""
if len(A) == 1:
return A if A in B else ""
if len(B) == 1:
return B if B in A else ""
mid = len(A) // 2
A_left, A_right = A[:mid], A[mid:]
# 正向 DP
def forward_dp(X, Y):
dp = [0] * (len(Y) + 1)
for x in X:
prev = 0
for j, y in enumerate(Y, 1):
temp = dp[j]
if x == y:
dp[j] = prev + 1
else:
dp[j] = max(dp[j], dp[j-1])
prev = temp
return dp
L1 = forward_dp(A_left, B)
L2 = forward_dp(A_right[::-1], B[::-1])
# 找最佳分割點
max_val, split = -1, 0
for j in range(len(B) + 1):
val = L1[j] + L2[len(B) - j]
if val > max_val:
max_val, split = val, j
return (lcs_linear_space(A_left, B[:split]) +
lcs_linear_space(A_right, B[split:]))
Hirschberg 算法展示了「以時間換空間」的經典權衡,其分治思想啟發了後續的 Myers O(ND) 差異算法、Ukkonen 截斷算法等現代序列比對技術。理解此算法,是掌握高效序列處理的關鍵一步。