线性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)\),比如最大子段和。

:部分内容来源于某只鲸鱼

posted @ 2026-07-17 21:06  _T_M_T  阅读(12)  评论(0)    收藏  举报