线性 DP
💡 核心思想
线性 DP 是最基础的动态规划形式,状态沿着线性序列递推。经典问题包括:最长递增子序列(LIS)、最长公共子序列(LCS)、数字三角形等。状态转移只在相邻的有限个位置之间发生。
线性 DP 的核心是定义好"状态"。以 LIS 为例,$dp[i]$ 表示以 $a[i]$ 结尾的 LIS 长度,那么 $dp[i] = \max_{j < i, a[j] < a[i]}(dp[j]) + 1$。这个定义"以 i 结尾"非常关键——它确保了子结构性质。很多同学状态定义错了,后面怎么推都不对。
🎯 直觉理解
线性 DP 就像在一条路上走,每走一步你都记住"走到这里的最优结果"。你只需要看前面几步的结果就能决定这一步怎么走,不需要记住全部历史。
📝 算法流程
- 定义状态:明确 dp[i] 的含义
- 写出状态转移方程
- 确定初始条件和遍历顺序
- LIS 优化:用二分维护递增序列,$O(n\log n)$
$$LIS: dp[i] = \max_{j < i, a[j] < a[i]}(dp[j]) + 1$$
📊 复杂度分析
| 指标 | 复杂度 |
|---|---|
| 时间 | 朴素 $O(n^2)$,LIS 优化 $O(n\log n)$ |
| 空间 | $O(n)$ |
💻 参考实现(C++)
C++ (C++17)
#include <bits/stdc++.h>
using namespace std;
// LIS O(n log n) 优化
int main() {
int n; cin >> n;
vector<int> a(n);
for (auto& x : a) cin >> x;
vector<int> tails; // tails[i] = 长度为 i+1 的 LIS 的最小末尾
for (int x : a) {
auto it = lower_bound(tails.begin(), tails.end(), x);
if (it == tails.end()) tails.push_back(x);
else *it = x;
}
cout << tails.size() << endl;
return 0;
}⚠️ 常见坑点
LIS 的 tails 数组内容不是实际的 LIS 序列
LCS 的二维 DP 空间可能很大
状态定义不清晰导致转移方程写错
LIS 用 upper_bound 还是 lower_bound 取决于是否严格递增
📚 相关题目
| 题目 | 来源 | 难度 | 备注 |
|---|---|---|---|
| P1020 导弹拦截 | 洛谷 | CSP-S | LIS经典 |
| P1439 最长公共子序列 | 洛谷 | CSP-S | LCS模板 |