最长公共子序列 LCS
排列 LCS→LIS·计数
本课摘要
最长公共子序列 LCS课程回答“两个序列的公共结构如何通过二维状态刻画”。内容以排列 LCS→LIS·计数为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。
- 判断最长公共子序列 LCS的适用条件与状态边界
- 围绕“排列 LCS→LIS·计数”推导转移与计算顺序
- 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节
正在整理目录…
什么是「公共子序列」
上一节的子序列是从一串里挑数、保持原次序。这一节有两串, 要找一条同时是它们各自子序列的序列——它就是一条公共子序列;其中最长的那条,长度就是 LCS (Longest Common Subsequence)。注意是「子序列」不是「子串」:字符不必相邻,只要在两串里都能按原次序依次找到。
拿一个小例子:、。 在两串里都出现且次序一致,是公共子序列(长 2); 也是——它在 A 里是第 2、3、6、7 位,在 B 里是第 1、3、4、5 位,两边都递增。能不能更长?试遍所有挑法,最长就是 4。
为什么不能贪心地「从头扫,遇到相同字符就配一对」?看 、:贪心先把两个 配上,之后 在 A 里已经没了往后的位置——只得长度 1; 可正解是先放 再放 ,同样长度 1,这里恰好不亏,但把串拉长就会出岔:此刻配哪一对最好,取决于后面还能配出多少——又是需要 DP 的信号。
穷举呢?A 的子序列有 条,逐条去 B 里验证,指数级,串一长就无从枚举。下面用一张二维表把它压成 。
状态与转移:只看两串的「末位」
两串一起处理,抓手是各自的前缀。设 表示:A 的前 个字符与 B 的前 个字符的最长公共子序列长度。 要算它,只需盯住两串当前的最后一个字符 与 ——它俩相不相等,决定了两条截然不同的路。
末位相等():这对字符可以且值得配成公共子序列的最后一对。把它配上后,剩下的问题变成「A 前 与 B 前 的 LCS」,长度在它基础上 :
末位不等():这两个末位配不成同一对,那么最优解里 与 至少有一个不会被用到。于是要么丢掉 (转成 ),要么丢掉 (转成 ),谁大取谁:
边界:(任一串为空,公共子序列长度为 0)。答案:。
本质
两串的 LCS 被「各自前缀 + 只看末位」拆成了一张 的表:每格只依赖左上、上、左三个已算好的邻居,一步 。于是 的枚举被 个格子装下。相等走对角、不等走上/左——这条「对角 vs 直行」的分野是全表的灵魂。
跟着算一遍
用一对更短的串 、 走几格(下标从 1 记),把两条规则跑起来:
看它一格一格长出来,再回溯出答案
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
不止长度:回溯重构一条 LCS
只给出长度。想要那条子序列本身,就从右下角沿转移的来路往回走: 在格 ,若当初是「相等」填的(),就斜着走到 ,并摘下这个字符; 否则朝当初更大的那个来源(上或左)走一格、不摘字符。走到边界为止,把摘到的字符逆序拼起来,就是一条 LCS。
要留意 LCS 可能不唯一:当上、左来源一样大时,往哪边走都合法,会回溯出不同但等长的 LCS。想统计「到底有多少条」,就得给方案数也开一张表——这正是本页例题 P2516 要处理的(相等时方案继承左上,不等时把达标来源并起来、再用容斥减去重复计入的左上)。
深化:当两串是「排列」——降到 O(n log n)
标准 LCS 是 。当 都到 , 必然超时。但有一类特殊情形能大幅提速: 两串是同一集合的两个排列(各值恰好出现一次,如都是 的重排)。此时有一个漂亮的转化——LCS 可以变成 LIS。
关键观察:既然 A 是排列,每个值在 A 里有唯一的位置。把 B 里的每个值,都替换成「它在 A 中的位置」,得到一串位置序列。那么——
为什么位置序列的 LIS 就是 LCS? 一条公共子序列,等价于在 A 里选一批位置、在 B 里选同样一批值,且两边次序一致。 映射后,B 中被选值的相对次序就是它们出现的先后(沿 B 从左到右,即位置序列的下标递增);而「它们在 A 里也保持同样次序」翻译过来,正是这些位置数值递增——两个「递增」合起来,恰是位置序列的一条上升子序列。于是最长公共子序列 = 位置序列的最长上升子序列。
而 LIS 有 的贪心 + 二分解法(维护 、 替换)。绕这一圈,排列 LCS 就从 降到了 。
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
边界 · 只对「排列 / 无重复」直接成立
「LCS→LIS」的降维前提是「一串里每个值唯一」(映射才是单值函数)。若值有界重复(如每种恰好出现 次),需把一个值展开成它的多个位置、且按位置降序铺开再求 LIS——见例题 P4303。若是普通带重复的两串,则老老实实用 的二维 DP,别硬套。另有一类叫 LCIS(最长公共上升子序列),要求公共子序列同时严格上升——它是「LCS 的匹配 + LIS 的上升」两个约束的复合,需设二维状态 「用到 、且以 结尾」并配合前缀最优优化到 ;洛谷原生 P/B 题库暂无纯 LCIS 模板,此处只作为概念点点到,不强凑题号。
例题
#include <algorithm>
#include <iostream>
using namespace std;
#define MX 100005
int n, len;
int a[MX], p[MX]; // p[值] = 该值在 a 中的位置
int b[MX], g[MX]; // g[k] = 长度 k 的上升子序列的最小结尾(单调递增)
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
p[a[i]] = i; // 记下 a 中每个值的位置
}
for (int i = 1; i <= n; i++)
{
int x;
cin >> x;
b[i] = p[x]; // ★把 b 的值换成它在 a 中的位置
}
// 排列 LCS = 位置序列 b[] 的 LIS,二分 O(n log n)
for (int i = 1; i <= n; i++)
{
if (len == 0 || b[i] > g[len])
{
g[++len] = b[i]; // 比末尾大,接到最长后面
}
else
{
int l = 1, r = len;
while (l <= r) // lower_bound:第一个 >= b[i] 的位置
{
int mid = (l + r) >> 1;
g[mid] >= b[i] ? r = mid - 1 : l = mid + 1;
}
g[l] = b[i];
}
}
cout << len << endl;
return 0;
}
// TAG: 线性DP LCS 排列 LIS 二分 O(nlogn)#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
#define MX 100005
int n, len;
vector<int> pos[MX]; // pos[值] = 该值在 a 中出现的所有位置(升序)
int b[5 * MX], g[5 * MX];
int main()
{
cin >> n;
int tot = 5 * n; // 每种基因恰好出现 5 次
for (int i = 1; i <= tot; i++)
{
int x;
cin >> x;
pos[x].push_back(i); // a 中位置,天然升序
}
int cnt = 0;
for (int i = 1; i <= tot; i++)
{
int x;
cin >> x;
// ★把 b 里的 x 展开成它在 a 中的位置,且按【降序】铺开,
// 这样同一个值的 5 个位置在 LIS 里最多被选中一个,等价 LCS 的匹配约束。
for (int k = (int)pos[x].size() - 1; k >= 0; k--)
{
b[++cnt] = pos[x][k];
}
}
// 对展开后的位置序列求 LIS(严格上升),二分 O(N log N),N = 5n
for (int i = 1; i <= cnt; i++)
{
if (len == 0 || b[i] > g[len])
{
g[++len] = b[i];
}
else
{
int l = 1, r = len;
while (l <= r) // 第一个 >= b[i] 的位置
{
int mid = (l + r) >> 1;
g[mid] >= b[i] ? r = mid - 1 : l = mid + 1;
}
g[l] = b[i];
}
}
cout << len << endl;
return 0;
}
// TAG: 线性DP LCS 有界重复 展开 LIS 二分#include <algorithm>
#include <iostream>
#include <cstring>
using namespace std;
#define MX 5005
const int MOD = 100000000; // 答案对 10^8 取模
char sa[MX], sb[MX];
int la, lb;
int f[MX][MX]; // f[i][j]:LCS 长度
int c[MX][MX]; // c[i][j]:取得该长度的方案数
int main()
{
cin >> (sa + 1) >> (sb + 1);
la = strlen(sa + 1) - 1; // 题目串尾带一个多余字符,去掉
lb = strlen(sb + 1) - 1;
for (int i = 0; i <= la; i++) // 与空串比:长度 0,「什么都不选」算 1 种
{
c[i][0] = 1;
}
for (int j = 0; j <= lb; j++)
{
c[0][j] = 1;
}
for (int i = 1; i <= la; i++)
{
for (int j = 1; j <= lb; j++)
{
if (sa[i] == sb[j])
{
f[i][j] = f[i - 1][j - 1] + 1;
c[i][j] = c[i - 1][j - 1]; // 末位配对,方案继承左上
}
else
{
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
if (f[i - 1][j] == f[i][j]) // 谁的长度达标就并进来
{
c[i][j] = (c[i][j] + c[i - 1][j]) % MOD;
}
if (f[i][j - 1] == f[i][j])
{
c[i][j] = (c[i][j] + c[i][j - 1]) % MOD;
}
if (f[i - 1][j - 1] == f[i][j]) // ★容斥:左上被重复计入,减掉
{
c[i][j] = ((c[i][j] - c[i - 1][j - 1]) % MOD + MOD) % MOD;
}
}
}
}
cout << f[la][lb] << endl;
cout << c[la][lb] % MOD << endl;
return 0;
}
// TAG: 线性DP LCS 计数 容斥练习
说明:纯 LCIS(最长公共上升子序列)在洛谷原生 P/B 题库暂无对应模板题(仅有 U 前缀的用户自建题)。它的正解是「LCS 匹配 + LIS 上升」的复合二维状态,已在上方深化的「常见陷阱」框里作为概念点讲解,这里不强凑题号。下面两题分别从「子序列思想」与「加权 LCS / 对齐」两侧巩固。

