Longest Common Subsequence

題目描述

給定兩個字串 text1text2,回傳它們的最長公共子序列(LCS)的長度。子序列不需要連續,但順序要保持。

輸入:text1 = "abcde", text2 = "ace"
輸出:3
解釋:LCS 是 "ace"

輸入:text1 = "abc", text2 = "def"
輸出:0

解題思路

1

定義狀態

dp[i][j] = text1 前 i 個字元和 text2 前 j 個字元的 LCS 長度。dp[0][j] = dp[i][0] = 0(空字串的 LCS 為 0)

2

字元相同

text1[i-1] == text2[j-1] 時,這對字元加入 LCS:dp[i][j] = dp[i-1][j-1] + 1。例如 text1=ace, text2=ace → dp[1][1] = dp[0][0]+1 = 1

3

字元不同

text1[i-1] != text2[j-1] 時,這對字元無法同時使用:dp[i][j] = max(dp[i-1][j], dp[i][j-1]),取跳過 text1 或跳過 text2 的較大值

4

填表方向

從左上到右下逐格填表,保證計算 dp[i][j] 時其依賴的三個格子都已填好

5

答案

dp[m][n] 即為 text1 和 text2 的完整 LCS 長度

程式碼

C#

csharp
public class Solution {
    public int LongestCommonSubsequence(string text1, string text2) {
        int m = text1.Length, n = text2.Length;
        // 空間優化:只用一維陣列
        int[] dp = new int[n + 1];
        
        for (int i = 1; i <= m; i++) {
            int prev = 0; // dp[i-1][j-1]
            for (int j = 1; j <= n; j++) {
                int temp = dp[j]; // 暫存 dp[i-1][j]
                if (text1[i - 1] == text2[j - 1]) {
                    dp[j] = prev + 1;
                } else {
                    dp[j] = Math.Max(dp[j], dp[j - 1]);
                }
                prev = temp;
            }
        }
        return dp[n];
    }
}

TypeScript

typescript
function longestCommonSubsequence(text1: string, text2: string): number {
    const m = text1.length, n = text2.length;
    const dp: number[][] = Array.from({length: m + 1}, () => new Array(n + 1).fill(0));
    
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            if (text1[i - 1] === text2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }
    return dp[m][n];
}

Python

python
def longestCommonSubsequence(text1: str, text2: str) -> int:
    m, n = len(text1), len(text2)
    # 空間優化版本
    dp = [0] * (n + 1)
    
    for i in range(1, m + 1):
        prev = 0
        for j in range(1, n + 1):
            temp = dp[j]
            if text1[i - 1] == text2[j - 1]:
                dp[j] = prev + 1
            else:
                dp[j] = max(dp[j], dp[j - 1])
            prev = temp
    
    return dp[n]

複雜度分析

| | Time | Space | |--|------|-------| | 本解法 | O(m × n) | O(n)(空間優化後) |

圖解

dp 表格 (m+1)×(n+1)
text1[i]==text2[j]
字元相同 → 左上+1
填表
字元不同 → max(上,左)
填表
dp[m][n] = LCS 長度

圖表展示了 DP 的兩條轉移路徑:字元匹配走「左上角 +1」,不匹配走「上方或左方取 max」。 每格只依賴左上、上方、左方三個值,所以可以用一維陣列加一個 prev 暫存變數來空間優化。

進階觀點

💡面試追問

  • 如何輸出實際的 LCS 字串? 保留完整的 2D dp 表格,從 dp[m][n] 開始反向追溯:字元相同時加入結果並走左上,不同時走上方或左方中值較大的。最後反轉字串得到 LCS。
  • 和 583. Delete Operation 的關係? 兩個字串的最少刪除次數 = m + n - 2 × LCS。因為 LCS 是兩者共有的部分,其餘字元都要刪掉。
  • 和 72. Edit Distance 的關係? Edit Distance 在 LCS 基礎上增加「替換」操作。LCS 只考慮保留和跳過,Edit Distance 多了一種「把 text1[i] 改成 text2[j]」的選項。
  • 實際應用? diff 工具(比較兩個檔案的差異)、Git 版本控制(merge conflict 解析)、DNA 序列比對(Bioinformatics)都以 LCS 為核心演算法。

邊界情況

理解測驗

🤔 為什麼字元相同時是 dp[i-1][j-1]+1 而不是 dp[i-1][j]+1?

🤔 LCS 的「子序列」和「子字串」有什麼區別?

你可能也想看

62. Unique Paths714. Best Time to Buy and Sell Stock with Transaction Fee

按 ← → 鍵切換課程