线性 DP 学习总结
Tiger_Rory · · 算法·理论
线性 DP 是一种基础的 DP,但它常常以一种令人意想不到的方式出现在比赛中。本蒟蒻被它弄得措不及防,于是痛定思痛,写下了这篇文章。
动态规划简介
定义
动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
由于动态规划并不是某种具体的算法,而是一种解决特定问题的方法,因此它会出现在各式各样的数据结构中,与之相关的题目种类也更为繁杂。
基本性质
-
最优子结构。
-
无后效性(重中之重):已经求解的子问题,不会再受到后续决策的影响。
-
子问题重叠。
如果有大量的重叠子问题,我们可以用空间将这些子问题的解存储下来,避免重复求解相同的子问题,从而提升效率。这是大部分 DP 优化的常用方法。
一般解法
对于一个能用动态规划解决的问题,一般采用如下思路解决:
- 将原问题划分为若干阶段,每个阶段对应若干个子问题,提取这些子问题的特征(称之为状态);
- 寻找每一个状态的可能决策,或者说是各状态间的相互转移方式(用数学的语言描述就是状态转移方程)。
- 按顺序求解每一个阶段的问题。
(以上均来自 oi-wiki)。
线性 DP 常被归类于“基础动态规划”,但事实上它相当灵活,不比树形 DP 和状压 DP 这些简单多少。
接下来进入正文。
线性 DP
递推是动态规划的引入,我们从这里开始。
递推
首先,我们来看一个经典案例:求解斐波那契数列第
斐波那契数列规律:
之前大家都学过递归的解法,即如下代码:
int F(int n){
if(n==1||n==2)return 1;
return F(n-1)+F(n-2);
}
这很好理解,但是时间复杂度是
于是我们考虑优化,于是有了线性递推解法。由于它的执行过程是线性的,所以这也是最初步的线性 DP。它的状态转移方程就是
代码是简短的,循环一遍求每个
这个问题后来又有更优秀的递推方法(矩阵),可以优化至
那么,数楼梯、过河卒等题目的实现就可以大大降低复杂度了。
但是,还有很多东西也是“线性 DP”。序列上的问题自然就出现了。
最长上升子序列(LIS)
模板传送门(简单版)。
给定一个长为
子序列和子串不一样,它允许不连续,所以不能贪心。
我们会发现,一个位置的最长上升子序列长度可以由上一个数值比它小的位置的最长上升子序列长度“转移”而来,所以我们关心上一个比它小的位置的数值大小。
那就规定
转移复杂度显然
那如果
考虑优化,我们注意到
我们考虑维护这个
- 当遇到比它末尾大的数时,直接插入。
- 否则,找到第一个大于等于它的元素,替换掉。由于序列单调递增,使用二分可以做到
O(\log{n}) 的查找次数。
答案即为序列长度。但是切记
总复杂度
这个优化好好理解下,下面要用。
最长公共子序列(LCS)
模板传送门(要加优化)。
求序列
两个序列作比较,容易想到设二维状态
我们会发现,这个状态只和
别急,来看看答案要怎么转移上去。
代码直接依照上面来实现就好了。
这个问题的启示:转移决策可能需要分类讨论。这就很考验心思的细腻了。
但是,这居然也能优化!
当然,这道模板题有一个性质:两个序列长度相同且都是
我们考虑记录
我们对应到
于是,我们求
相当神奇,建议自己去试一试。
正确性证明(可能不是很严谨):
最长上升公共子序列(LCIS)
这其实是一道习题。
求长度为
综合以上所述,可以想到设
这里转移时忽视
容易发现复杂度是
考虑优化,不难发现因为
求解前缀最大值使用递推即可,减掉一重循环,复杂度降为
0/1 背包问题
题目传送门。
题意简述:有
容易设状态
那么,当
可以过,但我们有更优的方式。
我们去掉第一维状态,那转移方程就变为当
但是这时有一个问题需要注意,那就是此时内层循环如果还是正着来,就会出现一个物品多次放入的情况(手玩试试)。所以,内层循环要倒着来。
这样空间复杂度就变为
完全背包问题
完全背包正解就是优化后的
后记与补充
希望能给大家带来帮助。
这里有一道补充题可以试试。