🤝

最长公共子序列 LCS

LCS = 从两个序列里找「双方都有的、顺序一致」的最长子序列:在 A 里按顺序能挑出来,在 B 里按顺序也能挑出来。这是二维 DP(第 03 课的填表)第一次上大舞台:dp[i][j] 管「A 的前 i 个 与 B 的前 j 个」。

💡 打个比方

两个朋友各写了一份「周末想去的地方」清单,顺序是他们自己的优先级。你想找一份双方都同意、且各自顺序不变的最长共同清单——可以跳过不想去的地方,但不能把顺序打乱。这就是 LCS(Longest Common Subsequence,最长公共子序列)。LIS 是「一个序列里挑上升的」,LCS 是「两个序列里挑公共的」——所以表从一排变成一张二维表:行是 A 的前缀、列是 B 的前缀,dp[i][j] = 「A 前 i 个 与 B 前 j 个」的最长公共子序列长度。转移两条规则:① 末位相同——这对字符肯定能一起用(它们就是公共子序列的最后一对),dp[i][j] = dp[i-1][j-1] + 1;② 末位不同——A 的最后一个字符(或 B 的最后一个)进不了公共子序列,两种「删法」都试一下,取大的:dp[i][j] = max(dp[i-1][j], dp[i][j-1])。边界:第 0 行、第 0 列 = 0(空序列的公共子序列长度为 0)。

术语:公共子序列(在 A 里保序能挑出、在 B 里保序也能挑出的子序列);前缀(A 的前 i 个字符);对角转移(末位相同时 dp[i-1][j-1] + 1,往左上走);取大转移(末位不同时 max(上, 左));倒推(从 dp[n][m] 沿路径回走,还原一条 LCS)。

🖼️ 大图看明白

LCS 填表:A = A B C B D A B(行),B = B D C A B(列) dp[i][j] = A 前 i 个与 B 前 j 个的最长公共子序列长度;紫色 = 倒推路径,紫色格 = 字符相同(取字符) i\j 0 B D C A B 0 0 0 0 0 0 0 A 0 0 0 0 1 1 B 0 1 1 1 1 2 C 0 1 1 2 2 2 B 0 1 1 2 2 3 D 0 1 2 2 2 3 A 0 1 2 2 3 3 B 0 1 2 2 3 4 倒推:(7,5)→(6,4)→(5,3)→(4,3)→(3,3)→(2,2)→(2,1),紫色格取字符 B、A、C、B → LCS = BCAB 两条转移规则 末位相同: dp[i][j] = dp[i-1][j-1] + 1 末位不同: dp[i][j] = max(dp[i-1][j], dp[i][j-1]) 边界:第 0 行 / 第 0 列 = 0 答案 = dp[7][5] = 4 LCS 例:BCAB(BDAB 也对) 倒推口诀 从右下角出发 字符相同 → 取字符 → 走左上 不同 → 走 dp 更大的方向 收集完再反转

🧩 子任务 1:什么叫「公共子序列」——手验两条

🎯 小目标:会用「在 A 里按位置挑、在 B 里按位置挑」验证一个串是不是公共子序列(例:A = ABCBDAB,B = BDCAB)。
// A = A B C B D A B(7 个字符,位置 1~7)
// B = B D C A B     (5 个字符,位置 1~5)
// 公共子序列:在 A 里按顺序挑得出,且在 B 里也能按顺序挑得出

验证 "BCAB":
  在 A:B(2) C(3) A(6) B(7) → 位置 2,3,6,7 递增 ✓
  在 B:B(1) C(3) A(4) B(5) → 位置 1,3,4,5 递增 ✓
  → "BCAB" 是公共子序列,长度 4

验证 "BDAB":
  在 A:B(2) D(5) A(6) B(7) → 2,5,6,7 ✓
  在 B:B(1) D(2) A(4) B(5) → 1,2,4,5 ✓
  → 也是长度 4

验证 "ABCD":
  在 A:A(1) B(2) C(3) D(5) → ✓
  在 B:A(4) B(5) C(?) → C 在位置 3,在 A(4) 前面,顺序乱了 ✗
  → "ABCD" 不是公共子序列(长度 4 存在,但这一条不行)

// 问题:有长度 5 的公共子序列吗?——子任务 3 用 DP 一锤定音
✅ 小检查:① 「CB」是公共子序列吗?② 「BD」是公共子序列吗?③ A 和 B 的连续最长公共段(公共子串)多长?
看看答案 ① 是——A:C(3) B(4);B:C(3) B(5),都保序。② 是——A:B(2) D(5);B:B(1) D(2)。③ 2——「AB」(A 的 1~2,B 的 4~5)或「BD」(A 的 4~5,B 的 1~2)。
易错:「公共」不是「两边都出现过这几个字符」就算——必须两边都能按顺序挑出来。A 里有 2 个 B、B 里也有 2 个 B,但挑的位置必须各自递增,要动手验。

🧩 子任务 2:两条转移规则为什么成立

🎯 小目标:理解「末位相同 → 对角 +1;末位不同 → 取 max(上, 左)」的推理,能手算 2×2 小表 LCS("AB","BA")。
// dp[i][j] = 「A 的前 i 个字符」与「B 的前 j 个字符」的 LCS 长度
// 边界:i = 0 或 j = 0(有一边是空串)→ LCS = 0

// 规则①:末位 A[i] 和 B[j] 相同
这一对字符可以作为公共子序列的最后一对:
  dp[i][j] = dp[i-1][j-1] + 1
(两边都去掉末位的答案 + 这一对字符)

// 规则②:A[i] 和 B[j] 不同
它们配不成一对,LCS 里至少没有其中一个:
  · 删掉 A 的末位 → 答案 dp[i-1][j]
  · 删掉 B 的末位 → 答案 dp[i][j-1]
  dp[i][j] = max(dp[i-1][j], dp[i][j-1])

// 小例子:LCS("AB", "BA") 全表
        B    A
  0:    0    0
  A:    0    1    A==A → dp[0][1]+1 = 1
  B:    1    1    B==B → dp[1][0]+1 = 1;
                       中心格 A≠B → max(1, 1) = 1
→ LCS("AB", "BA") = 1(「A」或「B」)
✅ 小检查:① 末位相同时为什么取对角 dp[i-1][j-1]+1,而不是 max(上, 左)?② 末位不同时,为什么两种删法都要试?③ 小例子里 LCS("AB","BA") = ?
看看答案 ① 末位相同时这对字符可以放心用:剩余部分的最优解(i-1, j-1)加上这一对,就是全局最优——对角转移既准又能支持倒推(对角一步 = 取一个字符)。② 不知道 LCS 里丢的是 A[i] 还是 B[j],两种可能都要试,取较大者才不会漏掉最优解。③ 1。
易错:倒推时「字符相同 → 走对角、取字符」是还原序列的关键。如果填表时用 max(上, 左) 而不用对角 +1,倒推时可能找不到该取哪个字符。

🧩 子任务 3:手填 7×5 全表 + 倒推 BCAB

🎯 小目标:把 A=ABCBDAB、B=BDCAB 的整张 dp 表填完(答案 4),并按口诀倒推出一条 LCS。
// 全表(边界行/列 = 0,逐行左到右填)
        B    D    C    A    B
  0:    0    0    0    0    0
  A:    0    0    0    1    1
  B:    0    1    1    1    2
  C:    0    1    1    2    2
  B:    0    1    1    2    3
  D:    0    1    2    2    3
  A:    0    1    2    2    3
  B:    0    1    2    2    3    4   ← 答案 dp[7][5]

验算 dp[7][5]:A[7]=B、B[5]=B 相同 → dp[6][4] + 1 = 3 + 1 = 4 ✓
验算 dp[5][3]:D≠C → max(dp[4][3], dp[5][2]) = max(2, 2) = 2 ✓

// 倒推一条 LCS(从右下角 (7,5) 出发):
(7,5) B==B → 取 B,走 (6,4)
(6,4) A==A → 取 A,走 (5,3)
(5,3) D≠C → dp[4][3]=2 ≥ dp[5][2]=2 → 向上走 (4,3)
(4,3) B≠C → dp[3][3]=2 ≥ dp[4][2]=1 → 向上走 (3,3)
(3,3) C==C → 取 C,走 (2,2)
(2,2) B≠D → dp[1][2]=0 < dp[2][1]=1 → 向左走 (2,1)
(2,1) B==B → 取 B,走 (1,0) 结束
倒序收集:B A C B → 反转 → LCS = BCAB ✓(长度 4)
✅ 小检查:① dp[4][5] = 3,举例一条对应的公共子序列。② 倒推时「上」和「左」的 dp 值相同,走哪边?③ 两个串没有任何公共字符时,答案是多少?
看看答案 ① 「BCB」(A:B(2)C(3)B(4);B:B(1)C(3)B(5)),「BDA」(A:B(2)D(5)A(6);B:B(1)D(2)A(4))也对。② 哪边都行——只要 dp 值不下降就不会丢掉长度,具体还原出的串可能不同,都合法。③ 0——整张表都是 0。
易错:倒推收集到的字符是倒序的(先拿到最后一个字符),输出前要反转——代码里用 ans = A[i-1] + ans(每次插到最前面)就不用手动反转了。

🧩 子任务 4:代码要点——二维数组 + 倒推循环

🎯 小目标:记住三个坑:字符串下标从 0、dp 表从 1(多一行一列存边界);倒推循环的终止条件与方向选择。
// 要点 1:字符串下标从 0 开始,dp 表从 1 开始
for (int i = 1; i <= n; i++)
    for (int j = 1; j <= m; j++)
        if (A[i-1] == B[j-1])  dp[i][j] = dp[i-1][j-1] + 1;   // 对角 +1
        else                   dp[i][j] = max(dp[i-1][j], dp[i][j-1]);

// 要点 2:dp 开 [n+1][m+1]——第 0 行 / 第 0 列就是边界(全 0,int 默认 0)

// 要点 3:倒推循环
while (i > 0 && j > 0) {
    if (A[i-1] == B[j-1]) { ans = A[i-1] + ans; i--; j--; }  // 取字符走对角
    else if (dp[i-1][j] >= dp[i][j-1]) i--;                   // 平手走上
    else j--;
}
✅ 小检查:① 为什么字符串用 A[i-1] 而不是 A[i]?② dp 为什么要开 [n+1][m+1]?③ 倒推循环为什么要 i > 0 && j > 0 才继续?
看看答案 ① C++ 字符串下标从 0 开始,而 dp 表从 1 开始(给边界留位置)——「第 i 个字符」在字符串里是 A[i-1]。② 第 0 行 / 第 0 列要存「空串」的边界值,数组必须比 n、m 各多 1。③ i 或 j 到 0 就走到边界(空串),再走就数组越界了。

💻 完整代码:LCS 长度 + 倒推一条 LCS

// 题目:输入两个字符串 A、B,输出 LCS 长度和其中一条 LCS。
// 示例:输入 ABCBDAB / BDCAB → 长度 4,输出 BCAB
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;

int main() {
    string A, B;
    cin >> A >> B;
    int n = A.size(), m = B.size();
    int dp[105][105] = {};
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (A[i-1] == B[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
            else dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
        }
    }
    cout << "LCS 长度 = " << dp[n][m] << endl;

    // 倒推一条 LCS(每次把字符插到最前面,天然正序)
    string ans = "";
    int i = n, j = m;
    while (i > 0 && j > 0) {
        if (A[i-1] == B[j-1]) { ans = A[i-1] + ans; i--; j--; }
        else if (dp[i-1][j] >= dp[i][j-1]) i--;
        else j--;
    }
    cout << "一条 LCS: " << ans << endl;
    return 0;
}
// 运行:输入 ABCBDAB / BDCAB
// 输出:LCS 长度 = 4
//       一条 LCS: BCAB
// 注意:LCS 可能有多条(BCAB、BDAB),程序输出其中一条即可。

✋ 你来试试

第 1 题:LCS("ABAB", "BABA") = ?举例一条。 3,例如「BAB」(在 ABAB 里位置 2,3,4;在 BABA 里位置 1,3,4)。dp 表最后一格 = 3。
第 2 题:LCS("12345", "246") = ? 2——「24」。2 和 4 在两个串里都保序出现;6 在 "12345" 里没有。
第 3 题:A 和 B 完全相同(如都是 "HELLO"),LCS 长度是多少? = 串长(5)——整个串就是公共子序列。此时每一格都走对角 +1,表填出来是「阶梯」。
第 4 题(加餐):"ABCBDAB" 和 "BDCAB" 的公共子串(必须连续)最长多长? 2——「AB」或「BD」。LCS 允许跳(4),公共子串必须连续(2)——同一个「公共」,连续与否差很多。
🎯 一句话记住:LCS = 二维表——末位相同走对角 +1,不同取 max(上, 左);边界行/列 = 0;答案在右下角 dp[n][m];倒推「相同取字符走对角、不同走 dp 大的方向」,收集后反转。