动态规划DP tricks
思路
如果贪心无法证明,考虑dp
这是一个正确率不高的想法。
尝试dfs
如果能够构造出一种dfs的过程,那么可以仿照dfs的参数构造出dp的状态。或者可以直接记忆化搜索。
优化转移点
如果有些转移点没有什么决策(比如可以用组合数或者贪心代替),那么就可以想方设法把这些转移点去掉。例如:P6820
优化转移过程
一般使用数据结构优化[1],对区间最值、求和等问题进行优化,以提高转移效率。
贡献反推trick
一个例子是CF1810G:这道题目中对于多个 \(k\) 询问答案,对于单个 \(k\) 可以倒序DP \(O(n^2)\) 做完,但是这样无法一次性处理所有的 \(k\) 的答案。注意到所有 \(k\) 的问题的转移过程本质上是相同的,只不过是初始值不同。所以我们反推出 \(g_{i,j}\) 表示每个状态的初始值对答案产生的贡献,那么我们就能对于每个 \(k\) 轻易地算出答案了。
不难的状态构造
P1018,
有借鉴意义的状态构造(杂类)
打砖块:\(dp_{i,j,0/1}\) 表示前 \(i\) 列用 \(j\) 枚子弹,最后一枚是/否打在第 \(i\) 列状态下的答案。预处理一下 \(p_{i,j,0/1}\) 表示第 \(i\) 列打 \(j\) 个砖块,最后一颗子弹是/否打在了第 \(i\) 列的状态下,实际消耗了多少子弹。最后一颗子弹是/否打在第 \(i\) 列的区别在于,如果最后一颗子弹打在第i列,那么一定打中一个没有赠品的砖块,否则第i列的最后一颗子弹一定打在了有赠品的砖块,并把获得的赠品借给后面的列用。
罕见dp
求长度为 \(n\) 的,包含 \(s\) 作为子串的由小写字母构成的字符串的数量。
结论:为了去重,有递推式 \(dp_{i,j}=dp_{i-1,j-1}+25\times dp_{i-1,j}\) ,更进一步地,
有 \(ans=\sum_{i=len_s}^{n}\binom{i}{len_s}25^{i-len_s}26^{n-i}\) 。直接做就做完了。
推理过程见:博客
背包
删除物品
从已经完成dp的背包中删除一些物品。洛谷文章
简单线性DP
-
最长上升子序列及等价类
-
最长公共子序列
组合类DP
以下待填充
卡特兰数
区间DP
常见的出现方式
- 维护某个区间的答案,并把区间们合并
- 维护某个区间的答案,并从别的区间转移,例如
矩阵取数游戏
一般的特征是数据规模很小,在 \(O(n^3)\) 左右。
只能消除相邻的一段元素、且消除后两边会并到一起,是区间DP的重要特征。如果没有这一特征,就有可能可以用其他方法做。
常见的转移方式
一般来说,区间DP都要先枚举区间长度,然后枚举一个端点,以此确保每个区间都从答案已知的区间转移而来。(如果考虑不清顺序,可以先写一个记忆化搜索再说)
区分最值与方案数:如果是方案数,也许需要使用组合数来处理每次操作的先后关系。比如每次可以删除一个位置,产生的代价与两侧极长连续段有关,那么就可以枚举 \([l,r]\) 内被第一个删除的位置,然后因为不知道左边和右边之间的顺序,但是它们各自的顺序已经被决策,所以乘上 \(\binom{r-l}{i-l}\)。
注意:如果你一次只能选择一个点,是从后往前转移,不是从前往后转移!也就是说,枚举最后选择了什么是对的,枚举最先选择了什么是错的。如果枚举最先选择了什么,就无法确定区间之外的情况;如果枚举最后选择了什么,该点的代价是确定的。([FJWC2017]恐狼后卫,2025/3/23模拟赛T3巧克力)
你可以枚举断点,然后从中端点两侧转移而来。
你可以适当的加入维度,如果目前维度不够。
如果选取有顺序之分(比如你每次可以选择一个),那么可以用组合数(将原区间一分为二,用组合数计算其中一半在时间轴上占据的位置)。
例题
从以下例题中,总结出的教训有:
- 推式子的时候不要忘记状态的定义。不要搞混状态或是遗漏情况(即想当然地将它贪心掉了)。
- 如果仅仅两个维度不够,且数据范围很小,考虑加入新的维度。
- 枚举分界点的时候,仔细考虑有没有枚举的必要,有时候其实没必要枚举分割点。
几道北中集训的题目:
-
关路灯/收集雕像:十分经典的小人取东西模型。一个人从某个位置出发,要抵达一些地点取东西,求最小的代价/限定条件下最大的价值。
这类题目的模板就是 \(dp_{l,r,0/1}\) 表示已经取完了编号 \([l,r]\) 之间的物品,现在小人在 \(左侧/右侧\)。为什么这么设计?因为我们 DP 唯一需要考虑的就是在哪里转弯,而转弯的位置一定在某个物品处。
另一个难点就是代价转移。在“关路灯”中,我们每秒都会消耗没关的路灯的电力;在“收集雕像”中,我们加一个维度 \(k\),\(dp_{l,r,0/1,k}\) 表示收集到了 \(k\) 只雕像的最短时间,这样就能判断雕像有没有消失了。最后统计答案的时候看一下,可抵达的 \(k\) 最大状态的 \(k\) 即可。
放ybtOJ的几道例题:
-
消除木块/UVA10559/洛谷题号:消除连续一段相同颜色的方块会获得 \(len^2\) 价值,并且导致其两端剩余的木块合并。问你给定序列中的最大价值是多少。
解法:\(f_{i,j,k}\) 表示区间消除 \([i,j]\) ,并且消除区间以后的 \(k\) 个与 \(a_j\) 颜色相同的方块所得到的价值。这里的 \(k\) 个方块无需相邻,可以当作中间的阻隔被提前消除以为它们开道。之后从两种情况转移:\(f_{i,j-1,0}+(k+1)^2\) 和 \(f_{p+1,j-1,0}+f_{i,p,k+1}(\forall i\le p\le j,a_p=a_j)\) 。
启示:如果发现DP过程的决策或者贡献依赖于别的什么量,考虑加入新的维度。
备注:尚未交到洛谷(由于没有UVA账号),记得交一下哈
-
最大收益:这题我错了很多次,关键在于状态设计。
你有一些物品排成一行,每个都有大小和价值(价值为正),如果相邻两个物品大小之和 \(\le k\),你可以消去这两个物品(就像消消乐一样),并获得二者价值之和的收益。
我设计的状态是,\(dp_{l,r}\) 表示恰好消去 \([l,r]\) 中所有数字的答案,如果无法正好消去这个区间就设置为 \(-1\)。这样很好转移,但是这样不好统计答案,因为答案可以是很多段区间的并。(也不是不能做)
正解是这么写的:\(dp_{l,r}\) 表示消去 \([l,r]\) 中的某些数字的最大答案,而转移的时候,如果我们消去了 \([l,r]\) 中的所有数字,我们才能消除 \(l-1\) 和 \(r+1\) 位置的数字,因此这种转移时就要判断 \(dp_{l,r}==\sum\limits_{i=l}^rvalue_i\)。此时统计答案就是 \(dp_{1,n}\)。
-
最小代价:
由于消去数字的最大值和最小值与答案有关,而且只能消去相邻的
计数DP
容斥原理
一般来说,DP一个大的部分,DP一个算重了的部分(或是用公式直接算出来),减去即可。
求在 \(m\) 维空间中走 \(n\) 步可能到的位置数目。
维护 \(f_i\) 表示走正好 \(i\) 步可能到的位置,然后维护 \(w_{i,j}\) 表示在 \(i\) 维空间中走 \(2j\) 步回到起点的方案有多少种。
不重不漏划分段:最小起点原则
如果问题是:你可以把序列划分若干段,然后每段如果满足某一条件,则可以被缩成特定字符(转化为特定特征)。求转换完本质不同的字符串(特征数的序列)数量。如 AT_arc110_e 和 AT_agc027_e。
你可以要求每种的本质相同段在合法的前提下左端点最靠右(或者说右端点最靠左)。
具体一点,你对于每个 \(i\),维护以 \(i\) 为右端点的所有合法段中的离 \(i\) 最近的左端点 \(j\),从此处转移。
图论/方格图DP
方格图DP
-
马挡过河卒(类记忆化搜索)
-
先说结论:遇到多次(2次)操作,单纯dp不具有无后效性时,考虑
费用流将两次操作的状态一起存,并且压缩一或多个维度(在此题目中,把两个“纵坐标”维度改为一个“步数”维度)以此去重。推理过程:
显然二维dp有后效性。
考虑 \(dp_{i,j,k,l}\) 表示第一次取现在到了 \((i,j)\) ,第二次取现在到了 \((k,l)\) 最后答案为 \(dp_{n,n,n,n}\) 。一种更优秀也更合理的做法是 \(dp_{i,j,k}\) 表示两遍都走了 \(i\) 步,第一遍横坐标现在为 \(j\) ,第二遍横坐标现在为 \(k\) 。然后如果两个点相同就只加1遍,否则就都加上。
第一种状态设计实际上只会统计第一次和第二次走的步数相同的情况(有浪费的状态),因此不会统计两个人先后取走同一个点的情况,因此是正确的。第二种做法消除了无效的状态,复杂度更优秀,正确性更显然。正解实际上和费用流相关。
变量位置的转换
实例:交换自变量和因变量
如果自变量值域很大,因变量值域较小,考虑交换自变量和因变量。例如:疯狂的采药
化为01-DP(变量大洗牌)
更本质的说,如果一个状态设计可以被转化为01-DP(或者本来就是),那或许就可以再转化回另一种状态设计,从而降低复杂度。这种手法被称为降低维数。
状态压缩
一般适用于 \(n\le 20\) 的情况。用一个维度,第 \(i\) 位是0/1表示第 \(i\) 个东西选不选。一般适用于前面的东西选没选会影响后面选择的情况。(例如:dfs的过程中,有一个vis数组)
预处理转移
有些题目可以花费一定复杂度预处理某个状态下一步会转移为什么状态,类似自动机的边。这样做在DP的时候,就不用计算当前状态会转移为什么状态了。但是这个 \(nxt\) 必须不能和所有维度有关,不然就没用了。
质因数分解状压
适用于gcd、lcm等数论相关的情形。例题:北中oj
由于一个 \(\le 10^9\) 的数至多有 \(9\) 种不同的质因子,因此枚举一个题目中出现了的数字 \(x\) 然后做一个 \(x的质因子种数\) 位的状压,第 \(i\) 位代表 \(x\) 的第 \(i\) 大的质因数。
树形dp
一般有儿子到父亲、父亲到儿子、换根dp三种。
树上背包
说是“树上背包”,其实就是把 已遍历的子树的信息 和 现在遍历到的子树的信息 合并的过程。
细节上,\(dp_{x,i}\) 并不表示 \(x\) 节点的真实答案,而是表示对于目前遍历的子树,得出的 \(x\) 的答案。如果限制循环次数为 \(已遍历子树大小\times 新子树大小\),那么总的均摊复杂度就是 \(O(nm)\),而非双层循环加一层dfs的 \(O(n^3)\)。均摊,很神奇吧。给出一道例题树上染色。关于复杂度的严谨证明 。
另外,通过巧妙的定义状态,树上背包可以用来解决一些需要容斥的计数问题,我说不清楚,直接看题解:建造军营。
dfs序dp。
适用于,倘若不选一个点,那么它的子树就不能选的情况。除此之外,与正常的背包没有区别。
我们就可以先算出每个点的 \(dfn\),然后定义 \(dp_{i,j}\) 表示考虑了 \(dfn\) 在 \(i\) 以后的点,重量在 \(j\) 以内的最大价值。\(dp_{i,j}\) 可以从 \(dp_{i+1,j-b_i}+a_i\) 和 \(dp_{i+sz_i,j}\) 转移而来。
换根 DP
还没写。
例题
模拟赛题:正解考虑dp。设 \(f_{x,i,0/1,0/1}\) 表示考虑到 \(x\) 的子树,选了 \(i\) 条不交路径,\(x\) 是否被覆盖,是否有路径伸出 \(x\) 的方案数,合并子树时分类讨论转移即可。
刷表法与填表法(“我为人人”和“人人为我”)
刷表法(即“我为人人”)是由一个决策点出发更新其他点。
填表法(即“人人为我”)是由很多决策点出发更新自己。
刷表法的好处是好想,在 状态压缩DP 中常数更小。
填表法的好处是可以顺着枚举顺序转移,因此比较方便数据结构维护(当然刷表法也不是不行)。
经典线性 DP
- 检查每个位置是否在某个 最长上升子序列(LIS) 中。
计算出包含每个位置的 前缀LIS \(f\) 和 后缀LIS \(f\),若 \(f_i+g_i-1=len_{max}\) 那么 \(i\) 在某个 LIS 中。
数据结构优化
非常多样。可能有:
- 单调队列:滑动窗口最值
- 单调栈:后面第一个大于 \(a_i\) 的数字
- 树状数组/线段树:区间修改、查询
- 前缀和/差分/预处理(如st表、倍增):静态修改或询问
- set/priority_map:需要排序
等等.
单调队列、斜率、决策单调性优化
决策单调性
如果转移时的代价满足决策单调性,就可以用 决策单调性优化。
单调队列
如果可以转移来的点始终是一个区间(且两端点只增不减),那么可以使用单调队列维护其中的最大值。最典型的应用是单调队列优化多重背包。
斜率优化
适用于 \(f_x\leftarrow a(x)b(y)+c(x)+d(y)\) 一类情况。这个可以看 斜率优化。
分离变量
将关于枚举的 \(i\) 与转移点 \(j\) 的变量分开,并分别在两边计算。
如果 \(i\) 和 \(j\) 相关的项相乘了,那么就可以用斜率优化。
费用提前计算
当有些变量很烦人,无法直接分离,必须新开一个维度(通常与已转移次数有关),可以考虑把产生的费用提前到前面计算,从而分离变量。
典型的例子有 P5785 [SDOI2012] 任务安排。
长链剖分优化关于深度的树上DP
这一思想类似于dsu on tree,保留高子的dp数组,将矮儿子合并进去。
这里的前提是,你的DP是基于深度的,这样一来才能通过移动指针的方式快速从高儿子的dp数组得出自己的dp数组。具体而言,如果你的转移式是 \(dp_{u,j}=\sum dp_{v,j-1}\),那么就令 \(dp_u\) 为 \(dp_{hson}-1\)(指针操作)。
复杂度上,这么做会在每条矮儿子边处转移,由于是基于深度的 DP,转移只会产生 \(len_x\)(即链长)的复杂度,总的复杂度为 \(\sum len\) 也就是 \(O(n)\)。
实现上,要建立一个内存池,并用一个指针数组标记每个点的dp数组的开始位置。记得留足指针变动的空间。P5904 [POI 2014] HOT-Hotels 加强版是一道很差的模板题,数据非常水,因此我也不敢保证我的代码是正确的。
见“数据结构优化”章节。 ↩︎

浙公网安备 33010602011771号