进阶动态规划学习笔记
一、数位 DP
顾名思义,数位 DP 就是一种在数位上进行的计数类 DP。
1. 基本套路
状态设计:几个维度依次为 位数、最高位填什么、一些特殊限制,表示的是一个前缀和的信息。
方程设计:一般分为两步。
- 预处理
通过简单的 DP,得到一个 带前导 0 和一个 不带前导 0 的初始数组。
- 拼凑
先将题目给的区间询问转换为两个前缀询问之差。然后模拟预处理的 DP 方程再进行计算。
这里注意一个叫作 “卡上界” 的技巧。大致是这样的:假如当前询问的是 566
。明显在我们研究最高位填 5
时,第二位就不能填 7~9
的任何数码,但是可以任填 0~6
;在我们研究最高位填 1~4
时,则又可以在第二位任填 0~9
了。因此,对于 1~4
,它们的 DP 值和预处理数组的 DP 值是一样的;而对于上界 5
,它的 DP 值则是经过重新计算的。对于每一位都要计算卡/不卡上界的结果。但请再次注意,末位不卡上界。
最后,将合法的状态加起来就可以了。注意 最高位不要算上前导 0。
2. 小技巧
可能我比较菜吧,这玩意对我来说挺恶心的。这里总结一些可能有用的小技巧。
-
不管是预处理还是拼凑,将末位特殊对待。
-
可以通过检查计算结果是否为单增序列来检查算法正确性。
-
将所给数事先拆分到数组里可以省事。
3. 例题
-
P4127 [AHOI2009] 同类分布:这道题开始我有点卡住,因为我设计的状态空间炸了。但其实稍微调换一下循环顺序、改变一下状态设计就好了。将 dp 循环顺序改变有时可以节省空间,甚至对卡常有帮助。
二、状压 DP
此内容的基本概念我们已经在上一阶段学习过了。放一个 link 在这里。
几道另外的例题:
-
P3694 邦邦的大合唱站队:看到数据范围猜出状压 DP,然后努力设计一个只与很小的 \(m\) 相关的算法。
-
P2396 yyy loves Maths VII:lowbit 往往有 \(\frac{1}{2}\) 的常数。
-
P4363 [九省联考 2018] 一双木棋 chess:有时尽管将状压 DP 状态设计出来后,看上去不能承受;但当我们将不合法状态剔除后,会发现状态数并不多。另外,博弈论 DP 有时也要有“逆向”思维。
三、树形 DP
此内容的基本概念我们也在上一阶段学习过了。link
普通例题:
-
P3177 [HAOI2015] 树上染色:不同于常规的树形 DP 以子树为单位的状态设计,这道题运用了 “费用提前计算” 的思想,每次计算这条边在整棵树中会被“覆盖”多少次。而且,树形背包严格用 size 卡上下界似乎可以做到 \(O(n^2)\)(我还没看懂证明,先咕)。
-
Tree with Maximum Cost:一道二次换根练习题。
类树结构:
-
基环树
-
P4410 [HNOI2009] 无归岛:仙人掌最大独立集。仙人掌有两种基本方法:dfs 树和圆方树。此处选用 dfs 树。对树形部分树形,对环形部分环形即可。
四、虚树 DP
1. 基本概念
这个东西主要用于多次的树形 DP。每次询问会给你一堆点的限制,且点的总数满足 \(\sum \le n\)。
明显,如果每次询问时直接再整棵树 DP 一次是相当浪费的。应当设计一种算法,只与询问点(关键点)有关。我们把建成的剔除了无用点的树称为虚树。
注意,虽然说虚树剔除了无用点,但它仍然有部分“不是关键点的点”。因为为了维护完整的结构,必须要保留关键点之间的 lca。
2. 基本操作
分为几步:预处理、建虚树、DP。
本人用得最多的构造方式为:二次排序 + lca 连边。
以下抄自 OIwiki:
将关键点按 dfs 序排序。
遍历,求排序后相邻两个点的 lca,并将其加入数组。
对数组再按 dfs 序排序,并去重。
再遍历一遍,求相邻两个点 x, y 的 lca,将 lca 与 y 在虚树上连边。
这里请尤其注意:“数组”过程中可能出现 2n 个点,要开两倍 size。
3. 几个问题
Q:
为什么要按照 dfs 序排序?这样求出来的 lca 为什么就覆盖了任意两个关键点之间的 lca?
A:
可以用 Tarjan 求 lca 的 dfs 过程来理解这个问题。如果当前递归到了子树 x,那么在退出 x 子树递归前,必须把子树 x 内的 lca 全都求完。而节点 x 本身成为一个 lca 的条件就是有两棵子树中分别有一个关键点。那么这两个关键点最简便的选择就是 某棵子树的最后一个关键点 和 某棵子树的第一个关键点。
按照 dfs 序排序,相当于就是确定了 dfs 的顺序,每次求相邻两个点的 lca,本质就是“某棵子树的最后一个”和“某棵子树的第一个”求 lca。
Q:
为什么连接 lca 和 y 就可以保证虚树连边不重不漏?
A:
如果 x 是 y 的祖先,那么 x 直接到 y 连边。因为 x 和 y 的 dfs 序是相邻的,所以 x 到 y 的路径上面没有关键点。
如果 x 不是 y 的祖先,那么就把 lca(x,y) 当作 y 的的祖先,根据上一种情况也可以证明 lca(x,y) 到 y 点的路径上不会有关键点。
——OIwiki
(虽然我觉得这个证明只说了不重性,但我实在想不出来什么情况会漏,就不证了)
4. 注意事项
-
在推广到虚树前,最好先完整打一份 \(O(nq)\) 的暴力,保证其正确性。
-
注意区分关键点和关键点在数组中的下标。
-
虚树点数组记得开两倍 size。
-
多测要清空。
-
一般来说,我们要硬性在虚树中加入节点 1。一定要特判它的一些特殊性。
5. 例题
-
P3233 [HNOI2014] 世界树:这道题中的两次分别对上、对下的 DP 求最近某类点,非常巧妙。这种两次 DP 的技巧也很值得记住。
五、区间 DP
六、普通数据结构优化 DP、单调队列优化 DP
1. 基本概念
总结一句:实际能够用数据结构优化的 DP(不论是变量、数据结构、还是单调队列)都必须遵从一个条件——决策间的相对大小关系不会发生变化。然后,我们才能根据决策的限制选择合适的数据结构进行维护。
2. 1D/1D 动态规划
其中 \(L(i), R(i)\) 是关于 \(i\) 的一次函数,满足单调性;\(val(i, j)\) 是一个关于 \(i, j\) 的多项式函数。
1D/1D 是两种 DP 转移优化方法——单调队列优化、斜率优化——的基础。
单调队列优化 DP 可用于的情景:\(val(i, j) = val_1(i)+val_2(j)\)。但单调队列能够优化的并非一定满足上述条件,详见下文例题。
3. 例题
-
P1782 旅行商的背包、P2254 [NOI2005] 瑰丽华尔兹:单调队列是一维的,但这两道题将状态划分为互不干扰的若干区域,分别使用单调队列优化 DP。
-
P3572 [POI2014] PTA-Little Bird:这道题向我们展示了单调队列优化 DP 不一定得满足所谓“拆为两部分”的条件,最本质的要求是“在入队时就确定决策大小关系”,因此运用 贪心 也能帮助我们确定决策大小关系。
-
P4954 [USACO09OPEN] Tower of Hay G、P5665 [CSP-S2019] 划分:这两道题的难点在 “倒序 DP” 和 “贪心思想”,得到 \(O(n^2)\) 做法后单调队列做法是很显然的。贪心结论是——使得最后一段尽量小。
-
P6563 [SBCOI2020] 一直在你身旁:区间 DP + 贪心 + 单调队列优化。
七、斜率优化
1. 基本概念
斜率优化可用于的情景:\(val(i, j) = val_1(i) \times val_2(j) + val_3(i) + val_4(j)\),即 \(val(i, j)\) 可包含和 \(i, j\) 乘积相关的项。
回顾 1D/1D 的 DP 方程:
暂且去掉 \(\min\),将其移项改写:
以 \(f[j] + val_4(j)\) 为纵坐标,\(val_2(j)\) 为横坐标,则可以发现:上式为一条过 \((val_2(j), f[j] + val_4(j))\) 的一次函数,满足斜率为 \(-val_1(i)\),截距为 \(f[i] - val_3(i)\)。可以发现我们已经将 \(i, j\) 拆解开了。
那么,对于每一个 \(i\),就是用一条斜率固定的直线依次尝试每一个点 \(j\),使得截距尽可能地小。
可以发现:
-
\(j\) 的所有可能取值在 \(j\) 形成的下凸壳(下凸包)上。
-
当 \(j\) 与凸壳前驱形成的线段斜率比 \(i\) 的斜率小、\(j\) 与凸壳后继形成的线段斜率比 \(i\) 的斜率大时,\(j\) 就是 \(i\) 要找的决策。
2. 注意事项
- 斜率计算为了避免精度问题,一般将 \(\frac{y_1}{x_1} < \frac{y_2}{x_2}\) 转化为 \(y_1x_2 < y_2x_1\) 计算。但注意当 \(x_1x_2 < 0\) 时需要变号。
3. 例题
-
T388391 0x5A-斜率优化dp-任务安排3、T388395 0x5A-斜率优化dp-Cats Transport、P3195 [HNOI2008] 玩具装箱、P4360 [CEOI2004] 锯木厂选址:一般来说,横坐标/斜率都是某个位置的前缀和。这些题中,有横坐标、斜率都关于 \(i, j\) 单调递增,因此可以通过单调队列 \(O(n)\) 维护决策集合。
-
P2900 [USACO08MAR] Land Acquisition G
这道题首先巧妙在通过排序、去除无关,得到一个 \(x\) 递增、\(y\) 递减的序列,将一个关于区间最值的 \(val(i, j)\) 转化为了仅关于区间两端点的 \(val(i, j)\)(类似前缀和那种转化方法)。
斜率优化的方程为:\(f[j] = -x[i]*y[j+1]+f[i]\)。观察方程发现,横坐标、斜率都满足递减。但只要横坐标和斜率各自有单调性,都可以用单调队列 \(O(n)\) 维护。(当然,对于下凸壳,在二者单调性相反,且对决策集合下界没有额外限制的情况下,会退化为单调栈。)
-
这道题不一样的地方是:求 \(\max\)。此时将维护下凸壳转为维护上凸壳,且斜率递减、横坐标递增,用单调队列维护。(为什么这里单调性相反,而单调队列没有退化为单调栈?因为上凸壳和下凸壳的性质是反的,画图理解很明显。)
-
【任务安排 3 的变式一】:假如 \(t_i\) 有可能是负数呢?
此时斜率不满足单调性了,而横坐标依旧满足。我们可以使用单调队列维护整个凸壳,每次在凸壳中二分查找斜率即可。(当然,这道题中由于没有额外决策下界限制,单调队列退化为了单调栈。)
-
【任务安排 3 的变式二】:假如 \(c_i\) 有可能是负数呢?
蓝书上给的解决方案相当巧妙——设计一个反向 DP 方程,使得 \(t\) 为横坐标 \(c\) 为斜率,即可用上述方法求解。
-
【任务安排 3 的变式三】:假如 \(c_i, t_i\) 都有可能是负数呢?
横坐标、斜率都不单调。这时我们就需要一个支持随意插入的数据结构——平衡树,来维护下凸壳了。(只能维护不带删除点操作的下凸壳,详见 动态凸包模板题。)
九、决策单调性与四边形不等式
1. 基本概念
-
决策单调性:对于一维线性 DP 转移方程 \(f[i] = \min_{0 \le j < i} f[j]+val(i, j)\) 的任意 \(i_1 < i_2\),对于二者的决策点必然成立 \(j_1 \le j_2\)。这是一类 DP 优化的突破口。
【注意:决策单调性并不意味着在 \(i\) 固定时,对于 \(j_1 < j_2\) 一定有 \(f[j_1]+val(i, j_1) < f[j_2]+w(i, j_2)\)。】
-
四边形不等式:若对于任意的 \(a \le b \le c \le d\),都有 \(w(a, d)+w(b, c) \ge w(a, c)+w(b, d)\),则称函数 \(w\) 满足四边形不等式。
这玩意儿为什么被称为“四边形”不等式呢?因为它跟四边形“对角线之和大于对边之和”的性质刚好相反——“包含”大于“交叉”。
四边形不等式一般被用于证明一些 DP 方程的决策单调性。可以证明,在状态转移方程 \(f[i] = \min_{0 \le j < i} f[j]+val(i, j)\) 中,如果 \(val\) 满足四边形不等式,则 \(f\) 具有决策单调性。具体证明见 OIwiki,懒得证。
-
二维区间 DP 的决策单调性:对于转移方程 \(f[i][j] = \min_{i \le k < j}f[i][k]+f[k+1][j]+w(i, j)\),令 \(p[i][j]\) 表示其决策点。若满足 \(p[i][j-1] \le p[i][j] \le p[i+1][j]\) 则称其满足决策单调性。
-
区间包含单调性:若对于任意的 \(a \le b \le c \le d\),都有 \(w(b, c) \le w(a, d)\),则称函数 \(w\) 满足四边形不等式。
通过证明,我们有结论:
-
若 \(w\) 满足四边形不等式与区间包含单调性,则 \(f\) 满足四边形不等式。
-
若 \(f\) 满足四边形不等式,则其满足决策单调性。
-
2. 一些题目的分析技巧
-
四边形不等式的另一种定义:若对于任意 \(a < b\),都有 \(w(a+1, b)+w(a, b+1) \ge w(a, b)+w(a+1, b+1)\),则可以证明 \(w\) 满足四边形不等式。
点击查看证明
使用数学归纳法。
假设 \(w(a, c+1) + w(a+k, c) \ge w(a, c)+w(a+k, c)\) 成立。
将 \(a+k < c\) 代入原式得:\(w(a+k+1, c)+w(a+k, c+1) \ge w(a+k,c)+w(a+k+1, c+1)\)。
将两式相加得:\(w(a, c+1) + w(a+k+1, c) \ge w(a, c)+w(a+k+1, c)\)。
故此,对于 \(a \le b < c\),有:\(w(a, c+1)+w(b, c) \ge w(a, c)+w(b, c+1)\) 成立。再代入一下 \(b=c\),发现也成立。
此时再仿照上述步骤即可拓展到一般四边形不等式 \(a \le b \le c \le d\) 的形式。这一条是最常用的。
-
概述一下:
-
满足四边形不等式的函数经过线性变换依旧满足四边形不等式。
-
若 \(w\) 可拆解为两个函数之差,则其满足四边形不等式。
-
若函数 \(h(x)\) 是一个单增的下凸函数,若函数 \(w\) 满足四边形不等式,则复合函数 \(h(w(x, y))\) 也满足四边形不等式。【例:\(h(x) = \sqrt x\)】
-
-
打表
在网上看见这么一种说法:“四边形不等式证明可能比较困难,做题时一般都是看到刚好要将复杂度削掉一个 \(n\),立马打表验证一下,然后直接冲。”
3. 实现
对于一维的情况,决策单调性有两种主要实现方法,均可以达到 \(O(n \log n)\) 复杂度。
-
分治法
这种方法的思路十分简洁:对于当前区间 \([l, r]\),如果已经找到了其决策所在区间 \([lk, rk]\),即可先 \(O(n)\) 算出 \(mid = \frac{l+r}{2}\) 的决策位置 \(k\)。此时,我们就知道 \([l, mid-1]\) 的决策所在区间为 \([lk, k]\),\([mid+1, r]\) 的决策所在区间为 \([k, rk]\),再继续递归求解即可。
inline int W(int x, int y); inline void Get(int x, int lk, int rk){//找决策位置 for(int i = lk; i <= min(x-1, rk); i++) if(W(x, i) > W(x, p[x])) p[x] = i; return; } inline void Div(int l, int r, int lk, int rk){//分治 if(l > r) return; int mid = (l+r)>>1; Get(mid, lk, rk); Div(l, mid-1, lk, p[mid]); Div(mid+1, r, p[mid], rk); return; } //主函数初始化 Get(n, 1, n); Div(1, n, 1, p[n]);
但是这种方法有一定的局限性:它只能用于求 \(f[x] = \min_{0 \le i < x} val(i, x)\),而无法直接应用于带有 \(f[i]\) 线性 DP 转移方程。因为它的复杂度正确性来源于 \([lk, rk]\) 的长度始终不大于 \([l, r]\),使得我们能够对 \(mid\) 的决策进行 \(O(n)\) 的暴力更新。这也就要求我们要在递归的开头必须求出正确的 \(rk\)。而对于线性 DP,想必就无法做到在递归一开始就计算出 \(f[n]\)。
-
二分 + 单调队列法:直接看 OIwiki 吧,说得很清楚。
对于二维区间 DP,我们一般采用以区间长度为阶段的转移方式,暴力枚举决策点即可。
4. 题目
-
P3515 [POI2011] Lightning Conductor:证明刚好用到“满足四边形不等式的函数类”的前三条;实现可用分治法。
-
P4767 [IOI2000] 邮局 加强版:看上去是线性 DP 用不了分治法,但由于 DP 方程是分层的,每一层内的计算顺序没有关系。
-
P1912 [NOI2009] 诗人小G:只能用“二分 + 单调队列”。
-
P5569 [SDOI2008] 石子合并:此题为很典型的二维区间优化,但是四边形不等式并不是正解。
-
Optimal Binary Search Tree:这道题难点在建模为区间 DP。(或许只能通过数据范围猜一下?)还有就是这道题的代价函数和决策点的取值有关——这样怎么能满足四边形不等式呢?暂且咕了吧。
十、矩阵 & 倍增优化 DP
1. 基本概念
我在 线性代数学习笔记 中写了一些关于矩阵的基本概念,并总结了矩阵乘法的三大用途:
-
运用矩阵快速幂实现对同一递推式的加速递推。
-
套用数据结构实现任意区间查询矩阵乘法。
-
套用数据结构动态维护矩阵乘法。
由于这一节的标题为“DP 优化”,因此这一节主要体现矩阵乘法的第一条用途。第二、三条用途在下一节“动态 DP”中体现地更为集中。
2. 矩阵优化
怎么用矩阵优化?将 DP 方程写成矩阵的形式,因为矩阵乘法有结合律,对它做矩阵快速幂即可。计数类 DP、最优化类 DP,皆可以。
什么时候能用矩阵优化?当状态数很小,但阶段很多,转移方程又没有变化的时候,而且有“恰好 \(k\) 个阶段”之类的字眼时,就可以考虑。一类矩阵优化的经典题型为恰好 \(k\) 步路径问题,以下大多数例题出自该题型。
-
P2579 [ZJOI2005] 沼泽鳄鱼、P3821 Isaac:根据 周期 性,构建几个不同的转移矩阵。
-
P4159 [SCOI2009] 迷路、P3597 [POI2015] WYC、P3193 [HNOI2008] GT考试:根据边权需要,拆解状态。
-
P2151 [SDOI2009] HH去散步:一反常规以点为 DP 状态,这题使用了 点边互换 的技巧。DP 时,哪一维受到限制,就将哪一位纳入状态。
3. 倍增优化
其实倍增优化本质上和矩阵优化是一样的。只是如果每次计算一个规模为 \(n \times n\) 规模的矩阵乘法,都有一个 \(O(n^3)\) 的常数。故此,当信息的转移足够简单时,我们就将矩阵结构略去,使用倍增优化。
当然,一边变简单了,另一边就会变困难:需要用到倍增的题目相较矩阵就不会那么明显。提示我们这道题要使用倍增的有这么几点:“固定转移”“多次询问”“恰好”。而“固定转移”这条性质的发现,往往需要用到一点贪心思维。
倍增优化的步骤大致就是:预处理 + 拼凑查询。
-
P1613 跑路、P2886 [USACO07NOV] Cow Relays G:这些题很明显属于我们上面提到的“恰好 \(k\) 步路径问题”,但由于信息转移过于简单可以用倍增维护。
十一、动态 DP
1. 基本概念
“动态 DP”是一类将简单的树形 DP 问题,如最大权独立集,加上了动态的点权修改操作或多次询问操作,从而将码量增大了 INF 倍的神奇题目。
算法简单概括就是:矩阵乘法 + 树链剖分。
在修改一个点的点权后,只有该点到根节点路径上点的 DP 值发生了改变。故考虑树链剖分进行维护。
为了用数据结构动态维护,要设法将 DP 方程套入矩阵乘法中。为了方便矩阵维护,我们将 DP 值拆为“轻儿子”部分和“重儿子”部分。对于一个点,我们只储存其轻儿子部分;重儿子部分每次查询都需要通过计算对应重链上的矩阵乘法来获得。
2. 实现注意事项
-
单位矩阵:主对角线为 INF,其余位置为 0 的矩阵。在 Query 时用到。
-
注意设计从矩阵中取出答案的向量。
3. 例题
-
P5024 [NOIP2018 提高组] 保卫王国:这道题的实现有一个恶心的地方,就是两个 INF 值如果存在相减操作,它们还不能设置为相等。
-
P3781 [SDOI2017] 切树游戏:还没做。。。
-
P4751 【模板】"动态DP"&动态树分治(加强版):\(O(n\log^2n)\) 的树剖被卡了,而 \(O(n \log n)\) 的 LCT 常数过大。这道题需要用到一个叫做“全局平衡二叉树”的数据结构。
-
P8820 [CSP-S 2022] 数据传输:虽然这个题只是多次询问,但我相信是可以增加修改点权的操作的(