动态规划总结 【分类3-例题】
动态规划总结 【分类3-例题】
每一题后面有个小标,可以帮助定位一些东西
1 线性DP
虽然大多数DP都可以叫做线性DP,我们这里只列举几个具有重要意义或者做法巧妙的。
- P1006 [NOIP2008 提高组] 传纸条 降维的技巧
这边我们先转化问题,求来回路径其实就是求两条路径,判重也简单,就是如果重合就不合法跳过,注意特判最后一个终点可以相等。
这个时候,我们观察刻画每一步需要:两个当前点的坐标,O(N^4)复杂度
这样,相当于就是拿点的移动当阶段,但是我们发现,是不是还有一个“步数”可以呢?
这样我们就只需要3个变量3个维度就搞定了,O(N^3)。
所以,当题目存在多个量满足线性增长时,注意筛选一个最优的当作阶段,然后再确定附加状态。
- P4147 玉蟾宫 单调栈优化DP
这个题直接把DP柿子给化没了。。。
如果求得是左边/右边的最小/最大,可以考虑单调栈。
这个,如果你并没有去手模一下,没发现大奶酪肯定在最顶上并且至多一块,那你就别想做出来了。可以简单地上述命题的正确性,不再展开。
2 背包DP
这个题不难。但是其方法很有参考价值。
其实发现,对于每个城堡只有s+1个决策点,其他的都无效。故这些构成一个物品组。
跑分组背包板子即可。
- P1156 垃圾陷阱 状态设计的一般方法
这道题很多可以用作阶段的量:时间、垃圾数,也有隐含的、但是对转移有限制的:生命值(饱食度)。
哦,对了,这个可以看作背包emm
3 区间DP
这个还是很符合区间DP的特征:其实不必考虑折叠之后是什么样子,只需要考虑两个串是否包含,如果包含就可以用拼接的方案。
其实这个题目也是再一次明示我们动态规划的优化逻辑——这个在LCS那个题的第一篇题解提到了:现在决定未来,未来与过去无关。现在已经累积了过去造成的影响。
你没有必要在计算一个状态的时候关心这个状态之前是怎么个组成方式、怎么转移的,你只要判断可以转移就行。就比如这个题,如果你每次转移都考虑两个压缩后字串压缩成了什么,而没有注意到实际上无论是怎么压缩的、压缩成了什么,最初都只是因为有循环节;如果你还考虑会不会有其中一部分的字串压缩、一部分不压缩,而没有看到这些是另外划分方案要考虑的——那你肯定做不出来。就是说,如果你判断“转移与父状态的中间细节有关”,那么你可能就错了,至少如果这样做出来,还不如不用DP。
注意到一次涂色可以节省次数,一定是因为它横跨了一个区间,将上面的需求块给同时涂掉了。所以我们判断区间的左右端点是否相等,判断是否可以节省一次。
所以说,此题我们最开始的入手点就是——为什么、什么情况下一次涂色可以减小次数。
在最优化问题中,考虑答案的贡献从何而来有时可以提供给你思路。
同时,如果你在想,为什么中间还有相等的就不用考虑呢?还是上面的问题,他的子问题,应该已经考虑过这个问题了。递归地分析,递推地计算。
这是一个隐晦的区间DP问题。因为这里并没有区间。
但是注意到老张关灯的顺序是从某个点出发,并且最后要关完所有灯。
进一步发现如果只有一盏灯,答案显然,如果有两盏,很容易可以从两边1盏的情况合并。实际上合并两个区间的答案是可以做到的。
那么考虑区间DP。然而我们发现合并的时候需要知道从区间的哪个断点上开始的,果断升维。
所以这道题再次明示我们递归地分析,递推地计算,将大问题花小是最为常见的策略。
4 树形DP
so,是不是看到无根、任意选根就一定是二次扫描与换根法呢?
注意到一个结论:无论哪个点做根都对答案没有影响
我们考虑什么情况下可以使得一种涂色方案更优,就是若干个点要公用一个染色节点。
因此要完成这个,就要若干叶子一定有公共节点。显然,对于任何的选根方案,假如原来这若干个点就存在LCA,即使后来该LCA变成了儿子,也一定有另一个LCA可以替代。
所以我们记录f[x][0/1]表示将x涂成0/1并且使得x子树中满足的最小代价。
x子节点的f值显然已经满足了他们子树中的需求,你不必再考虑了(同上面的题)
剩下就比较简单了
这个题不方便表示状态,因为你似乎发现每个点x不止可以管辖他的子节点,还能管辖祖父节点、与父亲同辈的节点。
那就麻烦了。但是好像有个贪心策略,那就是:选择一个深度最大且没被管辖的点并且在祖父处修建消防站。这个使用决策包容性容易证明:显然在可以管辖该点的情况下提高到祖父节点不会使得答案变劣。但是我是讲DP的呀
所以其实这一条提醒我们,可以使用覆盖的层数作为状态。但是时刻注意DP的无后效性。
这一道题我们相当于是拿了一个覆盖的区间从上往下扫,一直到只有x的孙子辈被覆盖。
有两种情况,要么是直接切下来就有P个,要么是切完了留着P个。
主要针对第二种,设f[x][p]是x子树留p个需要的数目。
5 环形与后效性
-
Broken robot 后效性处理
这边书上写得还挺好的
下面有一个SPFA处理后效性的题目。
6 状态压缩DP
一般来说,状态压缩DP有十分明显的标志:他们有一个量是一些布尔值,而且不是很多
这个题我们发现N十分小,最大只有18。就考虑加入一维度状态表示猪的消灭情况。
考虑怎么样才可以减小需要的次数。显然选择2个点和原点就可以确定一条二次函数,这样再判断这条函数还可不可以打中别的猪就行了。
- 棋盘覆盖 AC291
嘶~,他怎么想出来的呢?
假如我们按照一般的过程一行行一个个放,那么发现如果一行行来,那么竖着的会对下一行产生限制,而横放的又不会。
所以这也许就产生了书上的表示法。
同时,状态优化DP的精华就是预处理,可以预处理合法的状态,直接判断避免重复计算。
这道题TG178X的总结很好。这里放下:状态压缩和倍增优化dp - TG178X 的博客
这里摘录一些关键点:
如果在每次递归过程中都重复扫描所有已打通的宝藏屋,并尝试打通一条道路,显然会遍历相当多重复的状态,时间复杂度会相当高。为此可以做出两点限制(可视作剪枝操作):
- 【优化搜索顺序剪枝】先打通浅的宝藏屋,再打通深的宝藏屋。显然这不会对最终答案造成影响。
- 【最优化剪枝】对于多个同一种已打通的宝藏屋的集合,只需要关心其中代价最小的那个。因为打通后续的宝藏屋所需的代价与当前已打通的宝藏屋的集合内部的连接方式无关。
- 但是进一步分析可以发现,第一点限制其实体现了动态规划的「阶段」,而第二点限制事实上确立了动态规划的「最优子结构」,而 n 个点被打通的情况便应该作为动态规划的一维「状态」,而这正好是状压所能解决的。
- 本题其实给了我们很多启示:首先,关于 dp 的分析很多时候可以由搜索及其优化入手,这样不仅在思考的时候会更自然,而且即使这个题不能使用动态规划,也可以为其它算法的分析铺垫很多关键的信息;其次,在设计状压 dp 的状态转移方程的时候,应适当引入一些预处理的内容,因为对于进行了状态压缩以后的状态,处理起来可能会相对复杂和麻烦,因此适当的对预处理的内容的引入可以使得状态转移方程的逻辑和设计更加自然,但同时也需要考虑预处理本身的时空和代码复杂度;最后,题目的数据范围在某种程度上也提示了状压 dp 的可行性。过大的数据范围显然会导致压缩成整数的状态使得空间溢出。
关键不在于这个题目,但是在没有思路的时候考虑搜索不仅是保住分数的一个考试技巧,还是可以分析这个问题的一个时机,搜索的递归与dp的分析逻辑是一致的。读者不妨再次阅读上面引用框里的文字(尤其是那段优化)
顺便推下TG178X的博客,写的真的不戳。
7 倍增优化DP
- P1613 跑路 倍增优化经典
这个题面甚至把2^k都写出来了。。
是的,这道倍增优化倒是像二进制划分。直接求解是比较难的,考虑倍增优化的基本思想,就是要预处理出2^k的信息然后拼凑。
这个题不便于直接求出从i到j需要几秒,因为不方便将i->k和k->j的直接合并。
但是方便将两个2^(k-1)合成2^k呀,所以如果预处理可行性,就要简单许多。
所以说,倍增可以进行优化,主要是他利用二进制拆分来只计算必要信息。然后在使用时拼凑。但是如果直接求解一些有关幂次的信息其实不便,但是要知道2的幂次是很容易从小到大合并的。
8 数据结构优化
这个,easy LCS?不对!
但是好像有特殊点:对于每个数1-n在两个序列里出现且仅出现5次
观察经典LCS的转移方程,发现如果要使答案增加,而不只是继承,那么相对于某一个数字,就只有5个点。
所以,我们可以尝试更改状态。这样我们只需要对于其中一个序列,每个数保存5个转移点,只从这些点执行转移。
令f[i]表示匹配到a序列的第i位可以达到的最长LCS。随后列出新的状态转移方程,然后发现这个东西是一个求max前缀和的形状。所以可以使用树状数组维护。
这道题告诉我们什么呢?动态规划的状态一定是精炼的,关心无用的信息,就会耗费时间。
在实际操作的时候,我们应该关注题目中有没有特殊的信息可以限制动规的转移,这样我们可能节省一些时空。
考虑修改推平[l,r]之后,如果可以优化答案,那么应该是:左边以l结尾的最长不下降子序列+中间[l,r]+右边以r开始的最长不上升子序列。
故转化为求倒序的最长不下降子序列,正序的最长不下降子序列,然后枚举修改位置即可。
可以使用树状数组优化。
9 单调队列优化
- P6563 [SBCOI2020] 一直在你身旁 区间DP的单调队列优化
首先这是个区间DP还是容易确定,因为如果买了某一根电线,那么就可以从左右两边的区间更新答案。直接做
写出来
观察到外层的min满足区间DP的特征。可是里面还有个max
注意到在[l,r]中一定存在一个分界点使得k在左边f(k+1,r)大,在右边f(l,k)大,当然,最大的最小是二分特征,这里确实可以二分这个分界点。
但是假如考虑两个区间的这个分界点的关系:
所以如果不按区间长度而是按照左右端点枚举该点,就可以做到均摊
然后分别考虑左右边的情况,左边就是要求一个最小值,满足左右端点l和mid都在单调变化,而且其中的式子只含k和r(定值),可以使用单调队列优化。
右边再往大肯定是愈来愈大的,不优,直接选择分界点更新答案。
无论是不是单调队列,考虑答案的贡献来源和其变化单调性,并利用它进行分类讨论,是本题的关键。
为了控制奶牛不超过k头,我们设f[i]表示将奶牛i排除在外的最大效率值。
这样只要从不超过i-k-1的地方执行转移就保证不会有超过k头奶牛。
不过这样效率太低,考虑到这个决策集合十分经典地单调变化,使用单调队列进行优化。
10 计数类DP
- Gerald and Giant Chess 去重的技巧
首先从数据上看,白色格子很多但是黑色格子很少。
所以要求经过白格子的路线就可以拿所有路线减去要经过黑色格子的路线。
所以我们设f[i]表示不经过其他黑色格子走到第i个黑格子的方案数。
为什么这样设计?因为如果存在一些路径满足经过的第一个黑色格子不一样那么这些路径肯定是互不相同的。
所以,每个状态i可用的转移点(书上写作“划分出的状态”),是互斥的。
so,如果把终点也看做黑色格子,问题就解决了。
所以去重的关键,就是要添加限制条件构造子问题互斥,避免子问题的答案统计到同一种情况。
这种技巧,通常都是围绕:枚举第一个组成元素的某种属性,展开的。
11 数位DP
首先求一个n位魔鬼数的数量还是好求的,线性DP;
然后发挥“试填法”,从少的位数、小的数字开始尝试,然后一段一段跳过,最后找到第X小的。
12 杂项
如果你的状态表示含有后效性,不妨把它放到SPFA上跑。这就是利用迭代来代替递推。
本质上DP就是在一张DAG上推,但是如果不是DAG,就需要最短路算法。
但是在此之前,请检查如果换种转移方法/状态表示能不能解决。
你会注意到,A、B的值极大,但是需要达到的T很小。这个时候我们就可以抛弃比T大的A,减小比T大的B。
同时,如果你把当前资源作为了附加状态,注意如果从当前调用前面和从当前推出后面不同,前者无后效性,后者有后效性。
Summary 全篇总结
解决DP问题的一般方法
-
发现:题目中有哪些信息(量)
-
分类:这些信息哪些对答案有贡献、哪些对范围有限制、这些信息是怎么变化的、取值范围是什么
-
找出适合充当阶段的变量(应该满足线性增长/单调变化)
-
确定附加状态有哪些
-
列出状态转移方程(务必写出范围)
-
观察上式形式,确定是否使用优化
-
开动
设置状态的原则
-
应尽可能俭省,以求可以最简单地覆盖状态空间,及时排除冗余维度
-
应该选择方便进行递推转移的量
-
状态变量的变化要满足单调性(无后效性)
-
状态设置应该考虑俭省不必要的信息,例如上面采集资源的题目
-
状态设置应该考虑有效的转移点,如果这种点很少就不必进行连续的设计
-
如果毫无头绪,可以尝试从暴力或贪心的角度出发,如果还可以贪心地规避一些条件,那再好不过了(Cheese Tower S)
其实大多数人都认为解决动态规划问题,一般是一种考察思维而没有套路的事情。
但是虽说是这样,就像AI的训练过程一样,它肯定在长时间的大量训练中,实际上组建了自己的一个复杂模型,这,其实就是把套路,变成没有套路,有招胜无招的一般方法。
(上面那个方法我测过了,效果不错)
本文来自博客园,作者:haozexu,转载请注明原文链接:https://www.cnblogs.com/haozexu/p/18281767
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~