#5753. 2026/3/24/XMH笔记(LIS+LCS)
2026/3/24/XMH笔记(LIS+LCS)
LIS与LCS专题复习笔记
动态规划经典模型:最长上升子序列(LIS)与最长公共子序列(LCS)
包含13道经典题目,涵盖基础模板到变种应用
目录
一、知识点概览
题目分类
| 类型 | 题目编号 | 核心考点 |
|---|---|---|
| LIS基础 | P2856 | 模板题,O(n²) |
| LIS优化 | P5366 | 大数据,O(nlogn) |
| LIS变种 | P5367, P5423, P2838 | 求和、修改次数、变形 |
| LIS双向 | P2858, P2861, P3774 | 登山、滑翔翼、合唱队形 |
| LIS应用 | P2835 | 拦截导弹(非递增) |
| LCS基础 | P1770 | 模板题,O(n²) |
| LCS优化 | P1771, P4598 | 大数据、字符串LCS |
| 编辑距离 | P2851 | DP变形 |
二、LIS 专题
1. 基础模板:最长上升子序列
题目:P2856 【模板】最长上升子序列
题目简述:给定序列,求最长严格上升子序列长度。n ≤ 1000。
解法一:O(n²) DP
状态定义:
dp[i]表示以第 i 个元素结尾的最长上升子序列长度
状态转移:
dp[i] = max(dp[j] + 1) 其中 j < i 且 a[j] < a[i]
关键代码:
int lis = 1;
for (int i = 1; i <= n; i++) {
dp[i] = 1; // 初始化:自己就是一个长度为1的子序列
for (int j = 1; j < i; j++) {
if (a[j] < a[i]) { // 严格上升
dp[i] = max(dp[i], dp[j] + 1);
}
}
lis = max(lis, dp[i]); // 更新全局最大值
}
复杂度:时间 O(n²),空间 O(n)
2. 优化模板:O(nlogn) 解法
题目:P5366 【模板】最长上升子序列2
题目简述:n ≤ 10⁶,必须用 O(nlogn) 解法。
解法:贪心 + 二分
核心思想:
- 维护一个数组
d[],d[len]表示长度为 len 的上升子序列的最小末尾元素 - 对于每个元素,用二分找到它能替换的位置
为什么这样是对的?
- 末尾元素越小,后面能接的元素越多
- 我们贪心地让每个长度的末尾尽可能小
关键代码:
int d[MAXN]; // d[i] 表示长度为 i 的上升子序列的最小末尾
int len = 0; // 当前最长长度
for (int i = 1; i <= n; i++) {
if (len == 0 || a[i] > d[len]) {
// 可以接在最后,长度+1
d[++len] = a[i];
} else {
// 二分找到第一个 >= a[i] 的位置,替换它
int pos = lower_bound(d + 1, d + len + 1, a[i]) - d;
d[pos] = a[i];
}
}
// 最终 len 就是答案
注意:
lower_bound找的是第一个大于等于的位置- 如果是严格上升,用
lower_bound - 如果是非严格上升(可以相等),用
upper_bound
复杂度:时间 O(nlogn),空间 O(n)
3. LIS 变种题目
(1) 最大上升子序列和
题目:P5367 最大上升子序列和
题目简述:求上升子序列的最大元素和,而非最大长度。
分析:
- 状态定义改变:
dp[i]表示以 i 结尾的上升子序列的最大和 - 转移方程:
dp[i] = max(dp[j] + a[i])wherej < i && a[j] < a[i]
关键代码:
for (int i = 1; i <= n; i++) {
dp[i] = a[i]; // 初始化:只选自己
for (int j = 1; j < i; j++) {
if (a[j] < a[i]) {
dp[i] = max(dp[i], dp[j] + a[i]);
}
}
ans = max(ans, dp[i]);
}
注意:最长的不一定和最大!例:[100, 1, 2, 3],最长是 [1,2,3],但最大和是 100。
(2) 最少修改次数
题目:P5423 最少修改次数
题目简述:修改最少的数字,使数列严格递增。
分析:
- 保留尽量多的元素不动 = 找最长上升子序列
- 答案 = n - LIS长度
考虑序列 [1, 2, 2],LIS长度是 2,但无法通过修改一个数使其严格递增。
正确做法:
- 需要满足
a[i] - i严格递增(或非严格) - 转化为:求
a[i] - i的最长非递减子序列
(3) 友好城市
题目:P2838 友好城市
题目简述:南北两岸各 N 个城市,有 N 对友好关系。要在河上建航道,航道不能交叉,最多建多少条?
分析:
- 航道不交叉 = 选择的城市对在南岸和北岸的顺序一致
- 固定一岸的顺序(如按南岸坐标排序),另一岸的坐标必须上升
- 问题转化为:北岸坐标的最长上升子序列
关键代码:
// 按南岸坐标排序
sort(p + 1, p + n + 1, [](const Pair& a, const Pair& b) {
return a.south < b.south;
});
// 对北岸坐标求LIS
for (int i = 1; i <= n; i++) {
b[i] = p[i].north;
}
// 然后对 b[] 求LIS
4. 双向 LIS 问题
这类问题的特点:先上升后下降(或反过来)。
(1) 登山问题
题目:P2858 登山
题目简述:按照编号顺序浏览景点,海拔先严格上升再严格下降,最多浏览多少个?
分析:
- 找一个"峰点" i
- 左边是到 i 的上升序列,右边是从 i 开始的下降序列
- 分别求从左到右和从右到左的 LIS
关键代码:
// dp1[i]: 从左到右,以 i 结尾的最长上升子序列
for (int i = 1; i <= n; i++) {
dp1[i] = 1;
for (int j = 1; j < i; j++) {
if (a[j] < a[i]) dp1[i] = max(dp1[i], dp1[j] + 1);
}
}
// dp2[i]: 从右到左,以 i 开头的最长下降子序列
for (int i = n; i >= 1; i--) {
dp2[i] = 1;
for (int j = i + 1; j <= n; j++) {
if (a[j] < a[i]) dp2[i] = max(dp2[i], dp2[j] + 1);
}
}
// 枚举峰点
int ans = 0;
for (int i = 1; i <= n; i++) {
ans = max(ans, dp1[i] + dp2[i] - 1); // i 被算两次,减1
}
(2) 怪盗基德的滑翔翼
题目:P2861 怪盗基德的滑翔翼
题目简述:可以从任意建筑出发,选一个方向滑翔(只能往低飞),最多经过多少建筑?
分析:
- 两个方向:向左或向右
- 向右滑 = 从左到右的 LIS(用高度)
- 向左滑 = 从右到左的 LIS
- 答案 = max(正向LIS, 反向LIS)
(3) 合唱队形
题目:P3774 合唱队形
题目简述:n 位同学,要让剩下的排成先上升后下降的队形,最少出列几人?
分析:
- 和登山问题一样
- 答案 = n - max(dp1[i] + dp2[i] - 1)
5. 拦截导弹
题目:P2835 拦截导弹
题目简述:
- 一套系统最多拦截多少导弹?
- 拦截所有导弹最少需要几套系统?
分析:
第一问:非递增子序列的最大长度
- 因为"每一发都不能高于前一发"
- 就是求最长非递增子序列
第二问:Dilworth 定理
- 最少需要的不上升子序列个数 = 最长上升子序列长度
- 贪心模拟:每次尽可能延长当前系统
关键代码:
// 第一问:最长非递增子序列
// 把"上升"改成"下降或相等"
for (int i = 1; i <= n; i++) {
dp[i] = 1;
for (int j = 1; j < i; j++) {
if (a[j] >= a[i]) { // 非递增
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
// 第二问:贪心
int systems = 0;
int minHeight[1005]; // 每个系统当前能拦截的最高高度
for (int i = 1; i <= n; i++) {
bool found = false;
for (int j = 1; j <= systems; j++) {
if (minHeight[j] >= a[i]) { // 可以用这个系统
minHeight[j] = a[i];
found = true;
break;
}
}
if (!found) { // 需要新系统
systems++;
minHeight[systems] = a[i];
}
}
三、LCS 专题
1. 基础模板
题目:P1770 【模板】最长公共子序列1
题目简述:求两个排列的最长公共子序列。n ≤ 1000。
解法:O(n²) DP
状态定义:
dp[i][j]表示 A 的前 i 个和 B 的前 j 个的 LCS 长度
状态转移:
如果 A[i] == B[j]:
dp[i][j] = dp[i-1][j-1] + 1
否则:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
关键代码:
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (a[i] == b[j]) {
dp[i][j] = dp[i-1][j-1] + 1;
} else {
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
}
// 答案是 dp[n][n]
复杂度:时间 O(n²),空间 O(n²)
2. 优化:排列的 LCS
题目:P1771 最长公共子序列2
题目简述:n ≤ 100000,排列的 LCS。
分析:
- 排列的特点:元素不重复,都是 1~n
- 可以把问题转化为 LIS!
转化方法:
- 设 A 是
[3, 1, 2],B 是[1, 2, 3] - 记录 A 中每个值的位置:
pos[3]=1, pos[1]=2, pos[2]=3 - 把 B 转换成 A 中位置序列:
[pos[1], pos[2], pos[3]] = [2, 3, 1] - 求
[2, 3, 1]的 LIS = 2
为什么正确?
- LCS 保持相对顺序
- 在 A 中的位置递增 = 在 A 中相对顺序正确
- 在 B 中出现 = 在 B 中相对顺序正确
- 所以位置序列的 LIS = LCS
关键代码:
// 记录 A 中每个元素的位置
int pos[MAXN];
for (int i = 1; i <= n; i++) {
pos[a[i]] = i;
}
// 把 B 转换成位置序列
for (int i = 1; i <= n; i++) {
b[i] = pos[b[i]];
}
// 对 b[] 求 LIS(用 O(nlogn) 方法)
3. 字符串 LCS
题目:P4598 挑选糖果
题目简述:两个字符串,求最长公共子序列。长度 ≤ 100。
分析:
- 就是标准 LCS 问题
- 但如果是排列(字符各不相同),可以用上面的转化方法
四、编辑距离
题目:P2851 编辑距离
题目简述:三种操作(插入、删除、替换),将 A 变成 B 的最少操作次数。
状态定义:
dp[i][j]表示 A 的前 i 个字符变成 B 的前 j 个字符的最少操作
状态转移:
如果 A[i] == B[j]:
dp[i][j] = dp[i-1][j-1] // 不用操作
否则:
dp[i][j] = min(
dp[i-1][j] + 1, // 删除 A[i]
dp[i][j-1] + 1, // 在 A[i] 后插入 B[j]
dp[i-1][j-1] + 1 // 替换 A[i] 为 B[j]
)
关键代码:
// 初始化:空串变成长度 i 的串需要 i 次插入
for (int i = 0; i <= n; i++) dp[i][0] = i; // 删除
for (int j = 0; j <= m; j++) dp[0][j] = j; // 插入
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (a[i] == b[j]) {
dp[i][j] = dp[i-1][j-1];
} else {
dp[i][j] = min({dp[i-1][j], dp[i][j-1], dp[i-1][j-1]}) + 1;
}
}
}
五、综合练习
题目总结表
| 题号 | 题目名 | 类型 | 难度 | 关键技巧 |
|---|---|---|---|---|
| P2856 | LIS模板 | LIS基础 | ⭐ | O(n²) DP |
| P5366 | LIS模板2 | LIS优化 | ⭐⭐⭐ | 贪心+二分 |
| P5367 | 最大上升子序列和 | LIS变种 | ⭐⭐ | 求和而非长度 |
| P5423 | 最少修改次数 | ⭐⭐⭐ | 转化为 a[i]-i | |
| P2838 | 友好城市 | LIS变形 | ⭐⭐ | 排序+转化 |
| P2835 | 拦截导弹 | LIS应用 | ⭐⭐⭐ | 非递增+Dilworth |
| P2858 | 登山 | 双向LIS | ⭐⭐ | 正反两次DP |
| P2861 | 滑翔翼 | 选方向的最大值 | ||
| P3774 | 合唱队形 | n - max | ||
| P1770 | LCS模板 | LCS基础 | ⭐ | O(n²) DP |
| P1771 | LCS2 | LCS优化 | ⭐⭐⭐ | 转化为LIS |
| P4598 | 挑选糖果 | LCS | ⭐⭐ | 标准LCS |
| P2851 | 编辑距离 | DP | 三种操作 |
通用模板
LIS O(n²) 模板
int lis(int a[], int n) {
int ans = 0;
for (int i = 1; i <= n; i++) {
dp[i] = 1;
for (int j = 1; j < i; j++) {
if (a[j] < a[i]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
ans = max(ans, dp[i]);
}
return ans;
}
LIS O(nlogn) 模板
int lis(int a[], int n) {
int d[MAXN], len = 0;
for (int i = 1; i <= n; i++) {
if (len == 0 || a[i] > d[len]) {
d[++len] = a[i];
} else {
int pos = lower_bound(d + 1, d + len + 1, a[i]) - d;
d[pos] = a[i];
}
}
return len;
}
LCS O(n²) 模板
int lcs(int a[], int b[], int n) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (a[i] == b[j]) {
dp[i][j] = dp[i-1][j-1] + 1;
} else {
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[n][n];
}
常见错误
-
LIS的"严格"问题
- 题目说"严格上升":用
<,二分用lower_bound - 题目说"非严格上升":用
<=,二分用upper_bound
- 题目说"严格上升":用
-
双向LIS的重复计算
- 峰点 i 在正反两个方向都被算了一次
- 答案要减 1:
dp1[i] + dp2[i] - 1
-
LCS的空间优化
- 可以用滚动数组优化到 O(n) 空间
- 注意内层循环要从后往前更新
-
编辑距离的初始化
dp[0][j] = j:空串变成 B 的前 j 个字符,需要 j 次插入dp[i][0] = i:A 的前 i 个字符变成空串,需要 i 次删除
思维导图
LIS & LCS 专题
├── LIS (最长上升子序列)
│ ├── O(n²) 解法
│ │ └── dp[i] = 以 i 结尾的最长长度
│ ├── O(nlogn) 解法
│ │ └── 贪心 + 二分,维护每个长度的最小末尾
│ └── 变种
│ ├── 最大和:dp[i] = max(dp[j] + a[i])
│ ├── 最少修改:n - LIS
│ ├── 双向:正反两次 DP,找峰点
│ └── 非递增:改判断条件
├── LCS (最长公共子序列)
│ ├── O(n²) 解法
│ │ └── dp[i][j] = A前i个和B前j个的LCS
│ └── 排列优化 O(nlogn)
│ └── 转化为位置序列的 LIS
└── 编辑距离
└── dp[i][j] = min(删除, 插入, 替换) + 1