DP及其优化
常见DP模型及其构造
序列DP
ARC074 RGB Sequence
题意
给你一个长度为 \(n\) 的序列和 \(m\) 组约束条件,每组条件形如 \(l_i,r_i,x_i\),表示序列上的 \([l_i,r_i]\) 中恰好有 \(x_i\) 种颜色,现在要你用三种颜色给这个序列染色,求满足所有约束的方案数。
\(n,m \le 300\)。
技巧:设计出契合数据范围的状态
题解
注意到最多只有三种颜色,因此可以把颜色的信息记得暴力一些。设 \(dp[i][j][k]\) 表示三种颜色最后一次分别出现在第 \(i,j,k\) 个位置的的方案数。容易发现当前已经填过数的位置 \(i = \max\{i,j,k\}\),转移十分显然。
树形DP
ARC086 E Smuggling Marbles
题意
给出一棵 \(n\) 个点的有根树,初始时其中一些点上有一个石子,每次同时将所有石子从所在的点移动到父亲上,根节点上的石子移动到篮子里。如果有一个点上的石子数大于 \(1\) 则移除所有石子,树上没有石子时结束。求所有 \(2^n\) 种初始局面经过操作后篮子里石子的期望数量。
\(n \le 2 \times 10 ^ 5\)。
技巧:长链剖分优化复杂度与向下深度相关的DP
题解
首先可以发现,每一层的DP是独立的,因此可以分开考虑。可以先考虑一下 \(O(n^2)\) 的暴力DP。我们可以设 \(dp_{i,j,0/1}\) 表示 \(i\) 号结点的子树,往下 \(j\) 层的点向上移动到 \(i\) 号节点,最后为 \(1/0\) 的方案数。直接暴力合并就可以做到 \(O(n^2)\) 的复杂度。
考虑如何优化。这种复杂度与节点向下的深度有关的DP,都可以用长链剖分进行优化。每次只要把长儿子的DP值直接从长儿子合并到父亲节点。注意合并的时候不能直接赋值,否则复杂度会错掉。再将其余儿子的信息合并上来。这样复杂度是 \(O(n)\) 的。
考虑一下如何证明这个复杂度。为了方便,我们记不是长链的边为轻边。由于每个轻边的下面一定是一条长链,所以每条轻边合并的复杂度为它下面的长链的长度,而长链合并的复杂度也是长链的长度。因此总复杂度即为所有长链的总长度,也就是 \(O(n)\) 的。
数位DP
CF908G Original Order
题意
定义 \(f_n\) 表示将 \(n\) 的各个数位上的数排序后形成的数,例 \(f_{50394} = 3459\)。给定 \(n\),求 \(f_1 + f_2 + \cdots + f_n\)。
\(n \le 10^{700}\) 。
技巧:考虑每一位对答案的贡献
题解
题目是个比较明显的数位DP,但是直接做DP并不方便。考虑每个位的贡献,如果从低位到高位的第 \(i\) 位至少是 \(j\) ,那么至少有 \(i\) 个数位大于等于 \(j\) 。而从低位到高位的第 \(i\) 位的贡献,可以用第 \(i\) 位大于等于 \(1\) ~ \(9\) 的方案数相加得到。因此我们只要算出至少有 \(i\) 位大于等于 \(j\) 的方案数即可。
因此可以设计出数位DP,设 \(dp_{i,j,k,0/1}\) 表示前 \(i\) 位,有 \(j\) 位大于等于 \(k\) ,是否达到上界的方案数,转移十分显然。
清橙A1235 Digit
题意
求一个满足条件的 \(n\) 位数 \(a\),满足它的数字和为 \(s_1\),并且,\(a \times d\) 的数字和为\(s_2\)。注意 \(a\) 不能有前导 \(0\)。
\(1 \le n \le 100,0 \le s_1 \le 9n,0 \le s_2 \le 9(n + 1),0 \le d \le 9\)。
技巧:预处理DP处理贪心
题解
由于题目要求 \(a\) 是最小的,因此你需要类似按位贪心去填每个数。但是你又不好保证你填的数一定合法,因此需要一个预处理DP。设 \(dp[i][j][k]\) 表示 \(s_1\) 为 \(i\),\(s_2\) 为 \(j\),从这一位向前进位为 \(k\) 的最小长度。转移的时候需要一个类似BFS的东西。
然后就可以按顺序填每一位了。填数位的时候,枚举一下下一位进位多少。填的时候注意一下 \(s_2\) 的进位会很鬼畜,所以直接暴力算进位后的数位和比较好。
LIS
题意
定义 \(f_n\) 表示将 \(n\) 的各个数位拆开形成序列的最长上升子序列,给定 \(l,r,k\) ,求满足 \(l \le n \le r\) 且 \(f_n = k\) 的 \(n\) 的数量。
\(1 \le r \le r ≤ 10^{18},k \le 10\)。
技巧:DP套DP
题解
考虑我们是如何在 \(O(n\log n)\) 的时间内求LIS的。维护一个类似单调栈的东西,每次二分换掉大于当前元素的最小元素。于是这里也用类似的操作,利用状压DP与数位DP来完成这个过程。转移相对比较显然。
状压DP
AGC016 F Games on DAG
题意
给出一个 \(n\) 个点,\(m\) 条边的 DAG ,每条边的起点编号均小于终点编号。两个人在图上博弈,先在 \(1\) 号点和 \(2\) 号点上分别放一个棋子,每次操作可以将一个棋子沿一条边移动。两人轮流操作,直到不能操作的人输。
求有多少边的子集,满足在只保留这个子集的图上先手必胜。
\(1 \le n \le 15,1 \le m \le \frac{n(n - 1)}{2}\) 。
技巧:边集相关的方案数转化到点集上,再进行连边算方案数。
题解
首先根据 SG 函数可以得到,原题可以等价于选择一个边集,使得 \(SG_1 = SG_2\) 。
我们设 \(f_S\) 表示考虑了 \(S\) 中的点之后,同时包含 \(1,2\) 节点且 \(SG_1 = SG_2\) ,或者不包含 \(1,2\) 号节点,但可以通过这个连边方式使得 \(SG_1 = SG_2\) 的方案数。计算 \(f_S\) 的时候,可以考虑枚举 \(S\) 中 \(SG\) 值为 \(0\) 的点集 \(x\) ,这里再记一个 \(y\) 为 \(x\) 在全集 \(S\) 下的补集。这里强调一点,点集 \(x\) 中的点是在加入了 \(y\) 集合中的点之后,所有点的 \(SG\) 值变为 \(0\) 的。
考虑 \(x\) 集合与 \(y\) 集合之间的连边需要满足哪些条件:
- \(x\) 集合内部没有连边;
- \(x\) 向 \(y\) 集合的连边任意;
- \(y\) 集合中的每个点至少与 \(x\) 集合中连一条边;
- \(y\) 集合内部的连边方案恰好为 \(f_y\);
其中第 \(4\) 条相对难以理解一些。首先可以注意到一点,对于一个不包含 \(1\) 号点与 \(2\) 号点的点集,一定有办法通过加边使得 \(SG_1 = SG_2\) ,因为你可以直接把 \(1,2\) 节点定为没有出边的点,这样 \(SG_1 = SG_2 = 0\) 。于是第 \(4\) 条对于 \(1,2\) 号节点不在 \(S\) 中的情况就一定成立了。对于 \(1,2\) 号节点同时位于 \(x\) 集合,那么 \(SG_1 = SG_2 = 0\) ,这一结论也显然成立。对于 \(1,2\) 号节点同时位于 \(y\) 集合,只要将所有 \(y\) 集合中的点连向至少一个 \(SG\) 值为 \(0\) 的点,这样 \(y\) 集合中的点的 \(SG\) 值会同时增加 \(1\) ,\(y\) 集合内部的连边方案数恰好为 \(f_y\) 。
由于 \(1\) 号结点与 \(2\) 号结点之间不能有连边,因此 \(1,2\) 号节点不能同时位于不同的集合中。于是我们只需要一个子集枚举的状压DP即可。
DP优化
基本优化
一些基本常用的套路优化:
设计状态和转移的时候尽量剔除不必要的部分。
冷静分析复杂度,特别是有关树上DP的题目。
状压DP中的常数优化往往有意想不到的效果。
对于状态直接存储存不下的题目,利用
std::map
或者哈希表。
特别注意树形DP的时候,自己经常没注意一个地方然后算错复杂度。就是树形DP的时候,如果每个节点状态的大小为每个子树的大小,合并时必须枚举每个子树的每个状态进行合并,那么复杂度并不是 \(O(n^3)\) 而是 \(O(n^2)\) 的。原因是任意两个点只会在LCA处贡献 \(1\) 的复杂度。
斜率优化
这个玩意儿我是真的学一次忘一次,这一次学彻底一点,加强一下理解,免得下次又忘了。
斜率优化用于最优化题目中快速找到最优解。一般来说,可以通过推式子分析题目最优转移的凸壳形式。维护凸壳一般要分析凸壳上点的加入顺序选择合适的数据结构。对于插入的点横坐标单调,查询的直线的斜率也单调,就可以用单调队列优化。对于查询的直线斜率不单调,需要在凸包上二分;对于插入的点横坐标不单调,则需要动态凸包或者离线CDQ分治。
这段话里头提到了两个概念,可能有些不太清晰。一个就是查询直线的斜率,另一个是插入的点的坐标。这两个东西如果数形结合一下,会有更好的理解。考虑斜率优化的一个经典的式子。
假设 \(b\) 单调考虑两个决策点 \(j,k\) ,不妨设 \(k < j\) ,那么如果决策点 \(k\) 优于决策点 \(j\) ,就会有
于是通过这个式子,我们可以比较好地理解上面所说的两个概念了。在这个式子里头,\((b_k,-d_k)\) 就是 \(k\) 决策点对应的坐标,\((b_j,-d_j)\) 就是 \(j\) 号决策点对应的坐标;而 \(a_i\) 就是 \(i\) 号点查询时直线的斜率。
通过这样的数形结合,我们也能很好地理解单调队列解决斜率优化的几个步骤了。由于插入的点的横坐标单调,因此我们可以直接用队列来维护决策凸包。而有因为查询的斜率单调,因此我们要及时弹出队首那些随着查询直线斜率单调变化而不再可能成为最优解的点。还因为对于每一个决策点,我们已经证明了在当前询问直线斜率下,横坐标靠前的决策点一定比决策点靠后的点更优,因此队首就是当前决策的答案。
UOJ104 APIO2014 Split the sequence
题意
你正在玩一个关于长度为 \(n\) 的非负整数序列的游戏。这个游戏中你需要把序列分成 \(k+1\) 个非空的块。为了得到 \(k+1\) 块,你需要重复下面的操作 \(k\) 次:
- 选择一个有超过一个元素的块(初始时你只有一块,即整个序列)
- 选择两个相邻元素把这个块从中间分开,得到两个非空的块。
每次操作后你将获得那两个新产生的块的元素和的乘积的分数。你想要最大化最后的总得分。
\(n \le 100000,k \le 200\)。
技巧:斜率优化DP
题解
考虑操作顺序对答案的影响。容易发现,每次分割就让你获得了这两个块之间的两两元素之间的贡献。于是最后你没有拿到的贡献也就是在同一个块内的元素两两之间的贡献。这也就意味着你的选择顺序不会影响你最后的总得分。
于是我们就可以直接套用斜率优化DP来解决这个问题。注意到这里无论是查询直线的斜率还是插入点的横坐标都是单调的,因此可以直接用单调队列优化。
凸优化
个人认为课件里对于凸优化的解释相当的清楚所以我就偷个懒了……
凸优化解决的是一类选择恰好 \(k\) 个某种物品的最优化问题。一般来说这样的题目在不考虑物品数量限制的条件下会有一个图像,表示选择的物品数量与问题最优解之间的关系。
问题能够用凸优化解决还需要满足图像是凸的,直观地理解就是选的物品越多的情况下多选一个物品,最优解的增长速度会变慢。
解决凸优化类型的题目可以采用二分的方法,即二分凸壳上最优值所在点的斜率,然后忽略恰好 \(k\) 个的限制做一次原问题。
这样每次选择一个物品的时候要多付出斜率大小的代价,就能够根据最优情况下选择的物品数量来判断二分的斜率与实际最优值的斜率的大小关系。
理论上这个斜率一定是整数,由于题目性质可能会出现二分不出这个数的情况,这时就需要一些实现上的技巧保证能够找到这个最优解。
凸优化是DP优化的一种重要思想,感觉这些题目都还挺巧妙的,积累一些套路。因为这是一个我没接触过的套路,所以还是放了点水题。
CF739E Gosha is hunting
题意
现在一共有 \(n\) 只怪物。你有 \(a\) 个A型球和 \(b\) 个B型球。A型球抓到第 \(i\) 只神奇宝贝的概率是 \(p_i\) ,B型球抓到的概率则是 \(u_i\)。不能往同一只怪物上使用超过一个同种的球,但是可以往同一只上使用两种球(都抓到算一个)。你要制定一种方案,最大化抓到的怪物数的期望最大值。
\(n,a,b \le 2000\)。
技巧:凸优化简单应用
题解
首先可以得到一个 \(O(n^3)\) 的DP。设 \(dp_{i,j,k}\) 表示前 \(i\) 只怪物,用了 \(j\) 个A型球和 \(k\) 个B型球抓到的怪物数最大的期望值,转移比较显然。容易发现,随着A型球与B型球的数量增多,DP的增长速度会越来越慢,因此可以考虑凸优化。凸优化大概就是二分一次A型球的收益。然后对于每次操作的收益都减去这个二分的收益,做一次没有次数限制的DP。如果DP出来使用的A型球的个数大于 \(a\) ,那么就要加大这个二分的斜率,否则要放缓这个二分的斜率。对于两维都可以这样凸优化,因此复杂度为 \(O(n \log^2 L)\)。
LOJ2478 九省联考2018 林克卡特树
题意
现在有一个 \(n\) 个点的树,每条边有一个整数边权 \(v_i\),若 \(v_i \ge 0\),表示走这条边会获得 \(v_i\) 的收益;若 \(v_i < 0\),则表示走这条边需要支付 \(-v_i\) 的过路费。小L需要控制主角Link切掉树上的恰好 \(k\) 条边,然后再连接 \(k\) 条边权为 \(0\) 的边,得到一棵新的树。接着,他会选择树上的两个点 \(p,q\),并沿着树上连接这两点的简单路径从 \(p\) 走到 \(q\),并为经过的每条边支付过路费/ 获取相应收益。
求Link能得到的总收益-总过路费最大是多少。
\(0 \le k < n \le 3 \times 10^5\)。
技巧:条件信息的转化
题解
题目经过转化之后,可以发现,最后的所求可以看成,你需要从一棵树中选择 \(k + 1\) 条不相交的链。最大化这 \(k\) 条链的边权和。
可以想到一个树形DP。设 \(dp[i][j][0/1/2]\) 表示 \(i\) 所在的子树,选择了 \(j\) 条链。\(0/1/2\) 分别表示 \(i\) 号点与儿子没有连边,连了 \(1\) 条边与 \(2\) 条边的最大边权和,转移要稍微讨论一下。直接DP可以做到 \(O(nk)\) 的复杂度,需要优化。
可以发现,随着加入的边越来越多,增加的边权一定会越来越小,因此可以直接套用凸优化,复杂度优化到 \(O(n\log W)\)。
这个题实现上注意妙用重载运算符。像我这种写一大堆 if
的代码,是真的调不出……只能抄抄网上的代码维持生活这个样子。
LOJ566 YQL 的生成树
题意
对于可重实数集 \(\{a_i\}\),定义离差为:对于任意实数 \(d\),\(\sum_i |a_i − d|\) 的最小值。
现在给出一个 \(n\) 个点,\(m\) 条边的无向联通图,求最大离差生成树。
\(n \le 2 \times 10^5,m \le 5 \times 10^5\)。
技巧:寻找巧妙性质
题解
首先不难发现,这里的 \(d\) 应当是 \(a_i\) 的中位数,于是我们可以考虑枚举中位数。我们将所有边权大于中位数的边染黑,其余边染白,那么我们就只要求恰好包含 \(\lfloor\frac{n - 1}{2}\rfloor\) 的最大生成树即可。这就是一个凸优化的经典问题了。直接二分一个权值,对于所有的黑边都加上这样一个权值。然后根据所选黑边的条数来调整这个加上的权值即可。直接这么做,复杂度是 \(O(nm\log W)\) 的,不足以通过此题。考虑如何继续优化。
容易发现瓶颈在于中位数的枚举。我们可不可以不枚举这个中位数呢?
考虑我们现在的做法,先枚举中位数 \(M\),然后二分增长率 \(k\) 。令原来边权为 \(w\) 的白边边权为 \(M - w + k\),原来边权 \(w\) 的黑边边权为 \(w - M\)。注意到对于所有边权同时加上一个 \(M\) 对答案没有影响,因此两种边的边权分别转为 \(2M - w + k\) 与 \(w\)。
这里我们会有一种想法,如果我们令 \(C = 2M + k\) ,则黑白边边权分别为 \(C - w\) 和 \(w\) 。如果我们考虑转而二分 \(C\),由于这里我们确定的顺序有所改变,我们先确定了中位数,再来考虑每条边的颜色,因此初步分析可能会有最终黑边的边权 \(< M\) 的情况从而导致答案错误。也就是说,我们只有保证了对于最终所选出来的黑边与白边,黑边的边权都 \(\ge M\) 且白边的边权都 \(< M\) ,这个做法才正确。
首先黑边的边权是一定大于等于白边的边权。因为我们选出来的是最大生成树。对于一条边权为 \(w_1\) 的黑边和一条边权为 \(w_2\) 的白边,并存在 \(w_1 \le w_2\) 的关系。如果我们交换这两条边的颜色,那么答案一定不会更劣;
然后还需要排除掉两种情况,一种是黑边中边权最小的边的边权小于 \(M\) ,另一种是白边中边权最大的边边权大于 \(M\) 。我们不妨考虑第一种情况。如果存在一条边权为 \(w\) 黑边满足 \(w < M\) ,我们直接令 \(M = w\) ,容易发现这样所有权值 \(< M\) 的黑边,贡献都会变大,也能转化为更优的方案。于是这种做法也就得证了。
求最小生成树的时候可以预处理排序。整个复杂度为 \(O(m\log n\alpha(n))\) 。
分治与数据结构优化
数据结构优化应该司空见惯了,这里就不在赘述了。重点讲讲分治优化。
这里的分治优化并不是指分治优化某个值的计算,而是利用分治简化某些棘手的限制。
BZOJ4380 Myjnie
题意
\(n\) 家洗车店从左往右排成一排。有 \(m\) 个人要来消费,第 \(i\) 个人会驶过 \([a_i,b_i]\) 之间的洗车店,并选择这些店中最便宜的一个进行一次消费,但是如果这个价格大于 \(c_i\),这个人就不会消费。
请给每家店指定一个价格,使得所有人花的钱的总和最大。
\(n \le 50,m \le 1000,1 \le a_i \le b_i \le n, c_i \le 5 \times 10^5\)。
技巧:利用分治思想简化限制条件
题解
容易发现这是一个区间DP的模型。对 \(c_i\) 离散化之后,设 \(dp_{i,j,k}\) 表示从 \(i\) 到 \(j\) 的区间内,只考虑消费区间位于 \([l,r]\) 的顾客的贡献,洗车店价格最小值为 \(k\) 的最大收益。那么我们区间合并的时候,先枚举这个区间的最小值 \(l\) 在什么地方,然后考虑合并 \([i,l - 1]\) 和 \([l + 1,j]\) 两个区间。这里需要预处理出消费区间穿过 \(l\) 这个点的每个顾客中,\(c_i\)大于等于 \(k\) 的顾客数。然后就可以直接转移了。
题目还要求输出方案。记方案的时候,除了要记每个区间的最小值的位置,还要记每个区间每个位置对应的价格。
决策单调性与四边形不等式优化
本质上这两个东西是一个东西。四边形不等式优化只是决策单调性优化的二维版本。一维决策单调性优化常用的方式是分治或者单调栈。
感觉自己对这一块非常陌生,通过一道题来简单了解一下,熟悉一下套路。
LOJ6039 珠宝
题意
有 \(n\) 个珠宝,每个珠宝价值 \(c_i\),能产生 \(v_i\) 的愉悦度,现在你有 \(m\) 元,问你最多能获得多大的愉悦度,对于 \(m \in [1, k]\) 回答问题。
\(n \le 10^5,c_i \le 300,v_i \le 10^9,k \le 5 \times 10^4\) 。
技巧:决策单调性简单题
题解
首先注意到 \(c_i\) 的范围很小,因此可以把珠宝按照 \(c_i\) 分层。对于 \(c_i\) 相同的珠宝,显然我们会取愉悦度前若干大的珠宝,\(c_i\) 相同的我们在同一层转移。直接这么做显然复杂度爆炸,因此还需要优化。
注意到每个 \(c_i\) 相同的珠宝,按照 \(v_i\) 从大到小排序,前缀和的增长率是单调递减的,而且对于 \(c_i\) 相同的层,如果我们将决策点按照 \(\bmod c_i\) 的余数对决策点分类,\(dp\) 值也是单调的。这样我们可以得到,这个 \(dp\) 在同一类的决策也是单调的,为什么呢?
设 \(x\) 号点的决策点为 \(j\),而 \(y\) 号点的决策点为 \(k\) ,且 \(y > x\) 。假设 \(k < j\),那么就会有:
其中 \(Sum[x]\) 表示使用 \(x\) 元购买价值为 \(c_i\) 的珠宝,能够产生的最大愉悦度。由于 \(Sum\) 增长率是单调的,容易发现 \(Sum[x - k] - Sum[x - j] > Sum[y - k] - Sum[y - j]\) ,与上式矛盾。于是我们便得到了决策是单调的。
得到这一点之后,可以对于每一层每一类的转移,利用分治进行优化。
首先一个很蠢但是我想了很久的问题,为什么需要分治呢?因为你对于每个点还是要从上一个决策点扫一遍才能得到最大值。决策点虽然单调,但没有快速求的办法。
分治是怎么做的呢?首先分治下去的时候,传下去四个参数 \(L,R,ql,qr\) 。\(L,R\) 表示考虑 \(ql\) 到 \(qr\) 这个区间内的转移的决策点在 \(L\) 到 \(R\) 内。然后你需要找到 \(mid = \frac{l+r}{2}\) 的转移的决策点。这个可以扫一遍 \(L\) 到 \(R\) 得到。然后就可以递归子区间解决。
于是时间复杂度 \(O(ck \log k)\) 。