动态规划(Ⅰ)
1|0前言
复健 DP 了。
参考资料:
-
《算法竞赛进阶指南》- 李煜东
-
OI-Wiki
动态规划是什么?动态规划把原问题视作若干个重叠子问题的逐层递进,每个子问题的求解过程都构成一个阶段。在完成前一个阶段的计算后,动态规划才会执行下一阶段的计算。
为了保证这些计算能够按顺序、不重复地进行,动态规划要求已经求解的子问题不受后续阶段的影响。这个条件也叫做无后效性。从图上来说,每个节点对应着一个状态,图上的边对应状态之间的转移,转移的选取就是动态规划中的决策。且动态规划对状态空间的遍历构成一张 DAG,遍历顺序就是该 DAG 的一个拓扑序。
在很多情况下,动态规划用于求解最优化问题。此时,下一阶段的最优解应该能够由前面各阶段子问题的最优解到处。这个条件被称为最优子结构性质。更广泛来说,动态规划在阶段计算完成时,只会在每个状态上保留与最终解集相关的部分代表信息,这些代表信息应该具有可重复的求解过程,并且能够导出后续阶段的代表信息。
状态,阶段和决策是构成动态规划算法的三要素,而子问题重叠性,无后效性和最优子结构性质是问题能用动态规划求解的三个基本条件。
动态规划算法把相同的计算过程作用于各阶段的同类子问题,就好像把一个固定的公式在格式相同的若干输入数据上运行。因此,我们一般只需要定义出 DP 的计算过程,就可以写代码了。这个计算过程就被称为状态转移方程。
动态规划的艺术在于状态的设计和子结构的挖掘。而动态规划的优化就在于两个部分:状态转移方程中的状态和转移,我们通过观察性质,优化状态空间,减去冗余信息;通过数据结构等,来加快转移的速度。
一般用
-
:最长上升子序列。 -
:最长公共子序列,普通背包问题。 -
:多源最短路 Floyd。
下面整理一下不同类型的 DP
2|0线性 DP
最基本的动态规划,它的定义是具有线性阶段划分的动态规划算法统称为线性 DP。常见的 LIS,LCS,数字三角形等经典 DP 入门问题都属于线性 DP 的范畴。
在线性 DP 问题中,每个状态的求解直接构成一个阶段,这使得 DP 的状态表示自然就是阶段的表示。因此,我们只需要在每个维度上各取一个坐标值作为 DP 的状态,自然就可以描绘出“已求解部分”在状态空间中的轮廓特征,该轮廓的进展就是阶段的推移。
下面以几个例题讲一些常用的优化,最基本的我直接给出了,重点在于优化:
2|1从决策集合的角度优化
给定两个数列,求两个数列的最长公共上升子序列
在此只考虑弱化版,不需要求一个可行解。
设
-
当
时,有 ; -
当
时,有
这样以后你就有了一个
在转移过程中,我们把
上面的状态转移便只需要双重循环,
这道题告诉我们,在实现状态转移方程时,要注意观察决策集合的范围随着状态的变化情况。对于“决策集合中的元素只增加不减少”的情景,就可以像这个题一样记录决策集合的当前信息,避免重复扫描,把转移的复杂度降低一个量级。
相似题目:P2893 [USACO08FEB] Making the Grade G
2|2从状态表示的角度优化
考虑
再考虑
然后这个题目还要求同一位置不能出现两个员工,所以还要判断一下转移的合法性。
可以看出,这个转移的复杂度是
仔细观察后发现,在第
因此,我们用
设
可以看出,这样转移复杂度是
2|3总结
通过上述两个例题,我们可以得到一些提示性的东西:
-
设计 DP 的状态转移方程时,不一定要以“如何计算出一个状态”的形式给出,也可以考虑“一个已知状态应该更新哪些后续阶段的未知状态”。前者称之为填表法,后者称之为刷表法。
-
求解线性 DP 问题,一般先确定阶段。若阶段不足以表示一个状态,则可以把所需的附加信息也作为状态的维度,如第二个题把三个员工的位置也加入到状态里。
在转移时,若总是从一个阶段转移到另一个阶段(如这两个题的从
到 ),则没有必要关心附加信息维度的大小变化情况,因为无后效性已经由阶段保证。 -
在实现状态转移方程时,要注意观察决策集合的范围随着状态的变化情况。对于“决策集合中的元素只增加不减少”的情景,就可以像这个题一样记录决策集合的当前信息,避免重复扫描,把转移的复杂度降低一个量级。(第一种优化)
-
在确定 DP 状态时,要选择最小的能够覆盖整个状态空间的维度集合。
若 DP 状态由多个维度构成,则应检查这些维度之间能否相互导出,用尽量少的维度覆盖整个状态空间,排除冗余维度,如第二个例题。
-
有时候,我们可以在状态空间中运用等效手法对状态进行缩放,来达到简化复杂度的目的。
3|0背包
背包本质上是线性 DP,但因为它非常重要而特殊,所以单独拎出来了。
3|101 背包
最基本的背包问题,它的模型是这样的:
有
个物品和一个容量为 的背包,每个物品有重量 和价值 两种属性,要求选若干物品放入背包使背包中物品的总价值最大且背包中物品的总重量不超过背包的容量。
在上面这个模型中,由于每个物体只有取或者不取两种状态,对应二进制中的
我们考虑这个问题的阶段,便是考虑到前
那也就是说,我们设
初值:
滚动数组优化
通过 DP 转移方程我们发现,每一阶段
这样,虽然时间复杂度还是
去掉第一维
在滚动数组的基础上,我们其实可以直接把第一维去掉。
容易发现,在每个阶段开始时,我们其实执行了一次从
需要特别注意的一点是,第二层是倒序循环的。因为循环到
-
,也就是数组的后半部分,处于第 个阶段,也就是已经考虑过放入第 个物品的情况; -
,也就是数组的后半部分,处于第 个阶段,也就是还没考虑过放入第 个物品的情况。
接下来
但是,如果采用正向循环,假设
好题推荐
3|2完全背包
完全背包的模型如下:
有
种物品和一个容量为 的背包,每种物品有重量 和价值 两种属性,并且每种物品有无数多个,要求选若干物品放入背包使背包中物品的总价值最大且背包中物品的总重量不超过背包的容量。
你会发现完全背包和 01 背包的差别就在于,完全背包中每个物品的数量是无限多个的,你可以选无数多个。类似的,我们还是设
有变化的就在于
类似于 01 背包的,我们也可以省略掉第一维,然后第二维采用正序循环,对应着每种物品可以选无限次,这也就是 01 背包里提及的错误转移,在完全背包里就是正确的。
3|3多重背包
多重背包的问题模型如下:
有
种物品和一个容量为 的背包,每种物品有重量 和价值 两种属性,并且每种物品有 个,要求选若干物品放入背包使背包中物品的总价值最大且背包中物品的总重量不超过背包的容量
直接拆分法
求解多重背包的问题的最直接方法就是把第
二进制拆分法
二进制拆分法运用到了
根据这个性质,我们求出满足
-
根据
的最大性,有 ,等式两边同时减去 ,我们可以得到 ,因此从 中选出若干个相加,可以表示出 之间的任何整数; -
从
以及 中选出,若干个相加,可以表示出 之间的任何整数,而根据 的定义, ,这也就是说,用 这 个数和 ,能且仅能表示出 的任何数。
所以我们把数量为
这
写代码的时候我一般喜欢预处理二次幂和二次幂的和,大小视范围而定。
单调队列优化
使用单调队列优化多重背包,时间复杂度进一步降低到与 01 背包和完全背包相同的
3|4混合背包
混合背包就是把前面三种背包混合起来,有的只能取一次,有的能取
这其实就只是个大杂烩,没啥创新的部分,把他们合并在一起就可以了,引用一下 OI-Wiki 上的伪代码
事实上,01 背包可以和多重背包合并在一块。并且,虽然说完全背包能够用无限次,但是题目肯定会有一个使用次数的上界,你可以把完全背包中的物品个数设为这个上界,这样整个问题就转化成了多重背包问题。
例题:P1833 樱花
3|5二维费用背包(多维背包)
二维背包就是在一维背包的基础上,再加上另一种价值,我们只需要在
如果朴素设的话,空间就是
3|6分组背包
分组背包的模型如下:
给定
组物品和一个容量为 的背包,每组物品有 个,第 组第 个物品的重量为 和价值 。要求每组物品最多选一个,使背包中物品的总价值最大且背包中物品的总重量不超过背包的容量
还是从线性 DP 的角度来考虑,我们发现它的阶段就是考虑到了第几组,那么很自然的就有
与前面背包模型类似的,我们可以省略掉第一维,用
除了倒序循环之外,还需要注意的点枚举每组里物品的循环
好题推荐
3|7有依赖的背包
金明有
元钱,想要买 个物品,第 件物品的价格为 ,重要度为 。有些物品是从属于某个主件物品的附件,要买这个物品,必须购买它的主件。目标是让所有购买的物品的 之和最大。
考虑对于每一个主件和它的若干附件,从大面上来说就两种可能:买主件,买主件+一些附件。而且这几种可能性只能选一种,所以可以看成分组背包。
4|0区间 DP
区间 DP,本质上,还是一种线性 DP。回忆一下,线性 DP 是从初态开始,沿着阶段的扩张不断往规模较大的问题进行递推,直至计算出目标的状态。
区间 DP 也是一样的,它以区间长度作为 DP 的阶段,以两个坐标(区间的左右端点)描述每个状态。在区间 DP 种,一个状态是由若干个比它更小且被它包含的区间所代表的状态转移而来,你可以想成是一种从小区间到大区间的合并过程。因此区间 DP 往往能写出这样的状态来:
设
其中,
4|1性质
区间 DP 有这样的特点:
-
合并:可以将两个或多个部分进行整合,变成大区间,当然也能将大区间分割成若干个小区间;
-
特征:能将问题分解成两两合并的形式;
-
求解:对整个问题设最优值,枚举合并点,将问题分解成左右两个部分,最后合并两个部分的最优值得到原问题的最优值。
4|2例题
从最经典的入手
设有
堆石子排成一排,其编号为 。每堆石子有一定的质量 。现在要将这 堆石子合并成为一堆。每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻。合并时由于选择的顺序不同,合并的总代价也不相同。试找出一种合理的方法,使总的代价最小,并输出最小代价。
如果最初的第
我们可以发现,如果
我们发现这符合区间 DP 的性质,这个性质意味着两个长度较小的区间上的信息能够向一个更长的区间发生了转移,断点
设
后面的这个求和我们可以前缀和预处理,那么最终式子就变成
考虑怎么转移,我们以
初态:
终态:
4|3对于环的处理
考虑上面这个题的原版,也就是
-
方法
:枚举断点。由于石子围城一个环,我们可以枚举分开的位置,将这个环转化成一个链,由于要枚举 次,复杂度 。 -
方法
:断环为链。我们将这条链延长两倍,其中第 堆和第 堆相同。最后,我们取 中的最优解,即为最优答案。这就相当于给了 和 连在一块的机会。时间复杂度 。在做题中,我们比较常用的是第二种方法。
4|4好题推荐
有一定难度。
5|0树形 DP
树形 DP,即在树上进行的 DP。由于树固有的递归性质,所以树形 DP 一般都是递归进行的。
给定一棵由
在树上设计 DP 时,一般就以节点从深到浅(子树从小到大)的顺序作为 DP 的阶段。DP 的状态表示中,第一维通常是节点编号(代表以该节点为根的子树)。大多数时候,我们采用递归的方式实现树形 DP。对于每个节点
5|1基础
下面给道例题,讲一下树形 DP 的一般过程及思路。
某大学有
个职员,编号为 。他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 ,但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。
根据上文写的,我们令节点编号(子树的根)作为 DP 状态的第一维。因为一名职员是否愿意参加只跟他的直接上司是否参加有关。因此我们在每棵子树递归完成时,保留两个“代表信息”:根节点参加时整棵子树的最大快乐指数和,以及根节点不参加时整棵子树的最大快乐指数和,就可以满足最优子结构性质了。
设
- 考虑怎么对
进行转移:当 不参加舞会是,它的直接下属就可以选择参会,也可以不参会,所以
其中
- 在考虑对
转移:因为 参加舞会了,它的直接下属,也就是子节点都不能参会,因此
然后这个题因为是一个有根树,所以要先找到根节点
5|2树上背包
树上背包问题,又称有树形以来的背包问题,实质上是分组背包和树形 DP 的结合。
需要提前说的一点是,树上背包的状态设计一般是三维的,
这个点其实在紫书中没有提及,但是在 OI-Wiki 上是有的,这是好的/qiang。
例题
现在有
门课程,第 门课程的学分为 ,每门课程有零门或一门先修课,有先修课的课程需要先学完其先修课,才能学习该课程。一位学生要学习 门课程,求其能获得的最多学分数。
每门课最多只有一门先修课的这个特点,与有根树种一个点最多只有一个父亲节点的特点类似。
那我们可以根据这个建树,最终就是一个森林的形式。题目中每棵树的根节点的父亲节点是
我们设
转移的过程用到树形 DP 和背包 DP 的思想,我们枚举
记点
注意一下上面给出的限制条件,然后采用背包常用的倒序循环来省略掉第二个维度。
需要特别注意的一点是,树形背包的复杂度是
类似的题目还有 P1273 有线电视网、P1272 重建道路,我们可以按照上述的思路,从三维开始,寻找状态转移方程,再降维处理。
5|3换根法
换根法,又称二次扫描,通常会不指定根节点,并且根节点的变化会对一些值,例如子节点的深度和、点权和产生影响。对于这种问题,我们通常用两次 dfs 来解决问题,从而将
-
第一次扫描,任选一个点为根,在有根树上执行一次树形 DP,也就是在回溯时发生的,自底向上的转移,这一次 DFS 是用来预处理诸如深度、点权和之类的信息。
-
第二次扫描,开始运行我们的换根 DP,我们从刚才选出的一个根出发,对整棵树执行一次 dfs,在每次递归前进行自顶向下的推导,计算出换根后的解。
例题
给定一个
个点的树,请求出一个结点,使得以这个结点为根时,所有结点的深度之和最大。
一个结点的深度之定义为该节点到根的简单路径上边的数量。
你可以发现,这个复杂度,
首先,我们随便给定一个根
之后就是第二次扫描了,这次扫描是全局的,也就是用来求解答案的一次扫描。设
需要注意的是,下文的换根都是在以
对于
-
所有在
的子树上的节点深度都减少 ,那么总深度和就减少了 ; -
所有不在
的子树上的节点深度都增加 ,那么总深度和就增加了 。
通过这两个条件,我们就可以推出状态转移方程了。
于是,我们在第二次 dfs 时,遍历整棵树并状态转移,这样就能求出以每个节点为根的深度和了。最后只要遍历一次所有根节点深度和就可以求出答案,时间复杂度
P2986 [USACO10MAR] Great Cow Gathering G
、AcWing 287. 积蓄程度也是比较好的换根 DP,并且后者需要一定的特判,需要注意。
可以看出,换根 DP 的关键在第二次扫描,我们一般会把问题拆分成两个部分考虑:
__EOF__

本文链接:https://www.cnblogs.com/bloodstalk/p/17524767.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】