线性dp
合集
是什么
线性dp是一维的。
状态是一维的,或者能压成一维的,递推方向只有一个——从左往右,或者从前往后,不回头。
算 \(dp_i\) 的时候,只用得着 \(i\) 之前的信息,不会突然跳到后面去。这是它跟区间 DP、树形 DP 最根本的区别。
常见的那些题:LIS、LCS、最大子段和、编辑距离、数字三角形、斐波那契、爬楼梯、错排、卡特兰数,全归这一类。
注:
1.LIS:最长上升子序列 (Longest Increasing Subsequence)。在一个序列里找出一个子序列(不要求连续),使得它严格递增,且长度尽可能长。
2.最长公共子序列 (LCS:Longest Common Subsequence)。给定两个序列,找出一个同时出现在两个序列中的子序列(不要求连续),且长度尽可能长。
怎么实现
先开数组。一维就 \(dp_n\),双序列就 \(dp_{n,m}\)。
再写转移。线性 DP 的转移来源不外乎这几种:
- 从 \(dp_{i-1}\) 或 \(dp_{i-2}\) 过来(相邻位置)
- 从 \(dp_{i-k}\) 过来(前 k 个)
- 从前面任意一个 \(dp_j\) 过来,\(j < i\),但要加条件筛选(比如 LIS 要 \(a_j < a_i\))
- 双序列的话,就是 \(dp_{{i-1},j}\)、\(dp_{i,{j-1 }}\)、\(dp_{{i-1}{j-1}}\) 这三者之一或组合
然后是初始化。把最小子问题先填好(初始化),比如 \(dp_0\)、\(dp_1\) 该是什么就是什么。求方案数用 0 或 1,具体看题目怎么定义“空状态”。
最后定循环顺序。一维就正着来,双序列就两层循环都正着来。原则只有一个:算到 \(dp_i\) 的时候,它依赖的所有状态必须已经有值了。
骨架大概长这样:
初始化 dp[边界]
for(int i=1;i<=n;i++):
dp[i] = 由前置状态推出来
输出 dp[n] 或者 max(dp)
TIPS
TIP 1
\(dp_i\) 到底表示“前 \(i\) 个”还是“以 \(i\) 结尾”?这两个都能做题,但写法不一样。“以 \(i\) 结尾”的好处是转移方便,坏处是最后答案要扫一遍取 \(max\);“前 \(i\) 个”的好处是答案直接拿 \(dp[n]\),坏处是转移的时候得多想一步“第 \(i\) 个选不选”。
双序列的边界,比如 LCS,\(dp_{i,0}\) 和 \(dp_{0,j}\) 全是 0,因为空序列跟谁匹配都是 0。循环从 1 开始,就不用单独处理边界了。
答案在哪看你状态怎么定义的:
- 前 \(i\) 个 \(\rightarrow\) \(dp_n\)
- 以 \(i\) 结尾 \(\rightarrow\) \(max(dp_{1..n})\)
- 从 \(i\) 开始 \(\rightarrow\) \(dp_1\)
TIP 2
如果题目性质允许贪心,就别硬上 DP。贪心 \(O(n)\) 跑完的事,DP \(O(n²)\) 纯属浪费时间。DP 是万能解法,但不一定是最优解(别问我怎么知道的)。
空间上,如果转移只依赖前一行,就压到一维,别开二维 (也别问) 。01 背包那种倒序枚举就是典型。
注意子串和子序列的区别:子串是连续的,转移通常只看 \(i-1\);子序列可以不连续,转移可能要从前面任意 \(j\) 跳过来,复杂度容易变成 \(O(n²)\)。
数据范围会告诉你该用什么复杂度 (实践出真知) :
- \(n \le 10^4\),\(O(n²)\) 还能扛。
- \(n \le 10^5\),\(O(n²)\) 必炸,要想 \(O(n log n)\) 的写法(比如 LIS 的贪心+二分)。
- 双序列长度 \(\le 5000\),\(O(nm)\) 能过。
- \(n \le 10^6\),只能 \(O(n)\),比如最大子段和。
注:部分内容来源于某只鲸鱼

浙公网安备 33010602011771号