动态规划题目
前置:DP 基础优化技巧
状压 dp 进阶
高进制状压
一般是地图类,一次操作影响 \(\ge 3\) 行。一般采用行内 dfs 方式。
也有对于每个点需要有多种状态的。
P7689 [CEOI2002] Bugs Integrated,Inc.
如果是 \(3 \times 2\),那么第一行设为 \(2\),第二行 \(1\),第三行 \(0\) 就解决了。
如果是 \(2 \times 3\),第一行 \(1\),第二行 \(0\)。
于是我们强制钦定 \(2\) 下面填 \(1\),\(1\) 下面填 \(0\)。
外层枚举行,以及上一行状态,然后行内 dfs 转移即可。
P2704 [NOI2001] 炮兵阵地
放炮的地方设置为 \(2\),然后依次为 \(1\) \(0\),只有 \(0\) 旁边才能放 \(2\)。
P3959 [NOIP2017 提高组] 宝藏
可以很显然的状压,这里需要注意的是必须按层数转移,可以二进制但需要处理很多函数。
这里采用三进制,\(0\) 表示未扩展,\(1\) 表示之前层的,\(2\) 表示最新层扩展出来的。显然只有 \(1\) 才能转移。
这里注意几个细节,每次扩展为 \(1\) 的最高位即可,如果扩展完,把这一位设置为 \(2\),虽然不符合定义,但这样显然是合法的。如果要进入下一层,把所有 \(2\) 变为 \(1\) 即可。
UVA1412 基金管理 Fund Management
有一种做法就是对于每个状态设置为 \(9\) 进制数,然后编码解码一下。不过太慢了,会超时。
有一个技巧就是对于一些我们发现合法状态很少的状压 dp,可以事先预处理出可能的状态进行编号以及预处理它们之间的转移。然后直接按照转移 DP 就行了。
我的详细题解在洛谷上,这是我在洛谷的第一篇题解所以写的很烂。
状压杂题
QOJ8005. Crossing the Border
有 \(n\) 个二元组 \((a_i, b_i)\),将它们分到若干个集合中。划分方案中的一个集合 \(S\) 需要满足 \(\sum_{i \in S} a_i \leq m\),这个集合产生的代价为 \(\max_{i \in S} b_i\)。划分方案的代价为所有集合的代价之和。求总代价最小值以及对应的方案数,后者对 \(998244353\) 取模。\(n \leq 22\)。
很显然的暴力,我们将 \(b_i\) 升序排序,然后按照集合右端点从小到大加入保证方案不重,这样子复杂度是 \(O(3^n)\)。
考虑利用 \(\sum a_i \le m\) 的这个约束,可以想到类似 meet-in-middle 的思想,我们将集合分为两部分,每部分 \(\frac{n}{2}\) 个二元组,然后进行匹配。
我们可以枚举一个中间状态 \(LR\),表示从左边选出 \(L\),从右边选出 \(R\),在这个基础上匹配。我们可以从 \(L\) 的子集内选取集合转移到 \(R\) 的超集之中。假设 \(L'\subset L\),\(R \subset R'\),那么我们需要做的就是 \(L'R \to LR'\)。同样由于每次我们要加入包含最大右端点的集合,所以 \(L'\) 应该不含 \(\operatorname{maxbit}(L)\),这样新加入的部分 \(L-L'\) 满足最高位最新加入。\(R\) 同理。然后利用 \(\sum a_i \le m\),我们可以事先对于 \(L'\) 和 \(R'\) 按照 \(\sum a_i\) 排序,然后只需要枚举 \(L'\),同时对于 \(R'\) 双指针即可。
超集和子集需要预处理,并且排序。
这样子我们相当于枚举了 \(L\) 及其子集,这是 \(O(3^{\frac{n}{2}})\) 的。同时枚举了 \(R\),这是 \(O(2^{\frac{n}{2}})\) 的。所以总的复杂度为 \(O(6^{\frac{n}{2}})\)。
UVA1252 20个问题 Twenty Questions
本质上就是询问一个最小集合 \(s\) 使得有 \(n-1\) 个物品不包含这个集合。
这题有点难搞的原因是一方面是让我们最小化,另一方面是让我们考虑在最劣情况下的答案。是在最坏情况下尽可能小。其实很简单,只需要在 \(\min\) 里面套一个 \(\max\) 就可以了。设出状态 \(dp_{s1,s2}\) 表示已经询问了集合 \(s_1\),目前确定了物品所具备的特征为 \(s_2\),还需要至少多少次可以求出答案。
树形 dp
P10220 [省选联考 2024] 迷宫守卫
首先注意一个要点,就是满二叉树的树高是 \(\log n\) 级别的,这使得后续复杂度有保障。
最优化排列思路一般是逐位确定,Alice 首先应该最优化第一位,同时最小化代价,可是我们似乎不太方便直接找到最大的满足条件的第一位。那么不妨二分下 \(x\),我们需要封堵 \(< x\) 的所有位置,直接 dp 一下最小代价看看是否当前能承受即可。
注意一下,因为我们要求 Alice 要提前设置好所以为了封堵 \(x\) 左右边都需要设置,所以这里是 \(+\) 而不是 \(\max\)。同时将题目中的提前设置转化为这里的动态设置也值得思考。
确定了第一位 \(P_1\) 之后,Bob 必然是直接奔向 \(P_1\) 所在叶子节点,然后退出来进行其他访问。由于是要按照类似 dfs 顺序访问,因此 Bob 活动在访问完第一个子树后似乎受到了限制,没法全局跑来跑去了,但是我们仍然可以对于 Bob 下一次进入的子树进行二分 \(+\) dp,这样就完美解决了。
还有几个细节,算出左右边的代价之后我们不要着急布置,因为目前的费用肯定是可以支付左右代价的,我们等进入对应位置之后再布置。同时我们发现使用 \(w\) 是有点亏的行为,所以计算的时候虽然是取 \(\min(w_u,f_{rc})\),但是实际我们发现如果剩余费用可以支付 \(f_{rc}\),那么就不用 \(w_u\)了,因为 \(f_{rc}\) 这么多费用在进入子树内是必然要被用掉的。还有一点思考就是提前布置其实违反了子问题一致性结构。
P7091 数上的树
我们可以生成一个 \(d_i\) 序列表示 \(n\) 的各个约数,其个数其实不超过 \(23327\) 个。对于禁用情况,我们直接不让其在这个序列中出现就行了。
可以考虑树形 dp,设 \(f_i\) 表示 \(d_i\) 为根的时候的最小代价。我们发现转移的时候那个 lca 的贡献不太好处理。思考一下性质,对于一个数,以它为根的树的大小是一定的。令 \(g_i\) 为 \(d_i\) 的质因子个数的两倍减一,\(g_i\) 就是 \(d_i\) 子树的大小。于是我们 dp 转移就很好办了,每次枚举 \(d_j\times d_k=d_i\) 转移即可。发现固定了 \(j\),那么 \(k\) 唯一,如果直接用 map 记录需要多一个 \(\log\),可以用双指针优化。
P4577 [FJOI2018] 领导集团问题
树上 LIS 模板题。
类比序列 LIS 的 \(O(n\log n)\) 解法,每次在末尾能加入就在末尾加入。不能加入就进行二分然后替换。
使用 std::set 维护这个过程,进行树上启发式合并即可。
P4516 [JSOI2018] 潜入行动
树形上背包 dp
设 \(f_{i,j,0/1,0/1}\) 表示 \(i\) 子树内放置了 \(j\) 个检视器,\(i\) 是否放置了监视器,这个点是否被覆盖了。
暴力合并是 \(O(nk^2)\) 的,其实如果我们把上下界卡死是 \(O(nk)\) 的。
对于两个大于 \(k\) 的子树合并是 \(O(k^2)\) 的,但是每个合并完会少一个大小超过 \(k\) 的子树,所以最多有 \(O(\dfrac{n}{k})\) 次这种合并,复杂度是 \(O(nk)\) 的。
对于两个子树合并之后大于 \(k\) 的情况,其实从局部考虑对于每个点只会发生一次这种合并, 所以对于每个点这种合并均摊是 \(O(k)\) 的。
对于合并之后还是小于 \(k\) 的情况,每个点依然是最多匹配 \(k\) 次,所以单点复杂度还是 \(O(k)\) 的。
对于这个十几种可能的超级合并其实并不需要大分类讨论。
设父子分别为 \(u,v\),现在要 \(f_{v,b,p_2,q_2}\to f_{u,a,p_1,q_1}\)。
在满足 \(p_1\mid q_2=1\) 的情况下,转移到 \(f_{u,a+b,p_1,q_1\mid p_2}\) 即可。
QOJ4815. Flower's Land
给定 \(n\) 个点的一棵树带权。对于每个点求包含它的大小为 \(k\) 的联通块的最大权值。\(n\le 4\times 10^4,k\le 3\times 10^3\)。
先看一个弱化版,求全局最大的联通块。
首先是一个技巧 dfs 序背包的 trick,设 \(f_{i,j}\) 表示 dfs 序在 \([i,n]\) 之内的点选 \(j\) 个点构成的背包。由于是要求联通块所以要么选儿子要么整个子树都不选。
于是就有,\(f_{i,j}=\max(f_{i+1,j-1}+a_i,f_{i+sz_i,j})\)。时间复杂度 \(O(nk)\)。
由于是对于每个点都要求,考虑点分治。每次对于分治中心 \(rt\) 旗下所有点 \(u\),求出同时包含 \(u,rt\) 的联通块的最大权值。
分析一下要选哪些点,首先 \(u-rt\) 路径上的所有点必选(红笔标出)。
然后将点分成两类,dfs 序在 \(u\) 前面的(银+红),dfs 序在 \(u\) 后面的(橙色,其中三角形代表 \(u\) 的子树)。
对于第二类点,直接对于 dfs 序 \(> dfn_u\) 的点做一遍背包即可。
对于第一类点其实不好处理,因为其包含了银色(我们需要加入的贡献)和红色(强制要求加入的贡献),红色 dfs 序混入了银色之中很难分离。这里有一个很巧妙的做法,我们将原本遍历儿子的顺序颠倒(代码实现就是 reverse 一下 std::vector 数组 \(G\) ),这个时候加入 dfs 序 \(\ge dfn_u+sz_u\) 的点的贡献即可,因为这个时候红色会和橙色混合在 dfs 序 \(<dfn_u\) 的部分,而灰色正好被分离出来了。
剪枝一下,分治联通块大小不足 \(k\) 肯定不优,直接 skip。时间复杂度 \(O(nk\log{\frac{n}{k}})\)。

看了一下官方题解,还有一个更优秀的做法。
设 \(f_{u,i}\) 表示 \(u\) 子树内选择 \(i\) 个点可以得到的最大权值。为了求出最后答案,肯定是需要和 \(u\) 之外的一个部分进行合并,所以我们设 \(g_{u,i}\) 表示 \(u\) 上方的一个联通块内选 \(i\) 个点可以得到的最大权值。
我们在 \(u\to v\) 的时候求解 \(g_v\),可以发现其实就相当于在 \(g_u\) 的基础上加入 \(u\) 点,加入 \(v\) 的兄弟子树。关于加入兄弟节点,可以对于 \(u\) 的所有儿子进行一个前缀背包和后缀背包,再把 \(pre_{v-1}\) 和 \(suf_{v+1}\) 拼接在一起就可以恰好扣掉 \(v\) 子树信息。时间复杂度 \(O(nk)\)。
这里要注意一下细节,暴力卷积是 \(O(k^2)\) 的,我们必须把求 \(pre,suf,g\) 的卷积做成类似树形 dp 的转移枚举,才能保证均摊是 \(O(k)\) 单次转移。
对于 \(pre\) 的求解很简单就类似于求解 \(f\) 一样动态更新 \(sz\) 卡住上界。对于 \(pre\times suf\to g\) 的卷积需要注意卷积出来的数组大小必须 \(\ge k-sz_v\) 否则没意义,这个下界优化也可以将大小枚举形式规约到树形 dp 的枚举形式上。
但是 \(pre\) 的求解就需要小心了,因为我们上述说了还要带上 \(g_u\),为了方便直接设 \(pre_0=g_u\),往后卷,但是如果直接做复杂度还是不对,因为这个形式不符合树形 dp 一个一个合并子树了,而是直接就有了一个 \(sz(g_u)\) 的基础了,一条链就可以卡爆。其实 \(pre\) 有用的部分只有 \([k-sz_{suf},k]\) 这么多,因为更小的就凑不出来 \(k\) 了,所以还是可以转化到树形 dp 复杂度上。
CF793E Problem of offices
观察结构,发现一定是形如 a c b d 或者 a d b c 这样子的结构。
考虑树形 DP 记录大小是否可行来凑出题目要求的数据,直接做是 \(O(n^2)\) 的。
使用 std::bitset 优化可行性背包即可做到 \(O(\dfrac{n^2}{w})\)。
闵可夫斯基和 Minkowski
上述是计算几何版本的 Minkowski,对于离散情况,一般是 \((\max,+)\) 卷积,比如 \(h_{i+j}=\max\limits_{i,j} (f_i+g_j)\) 这种形式。称 \(f_i,g_i\) 满足凸性,就是 \(f_i-f_{i-1}\) 和 \(g_{i}-g_{i-1}\) 单调递减的时候。(当然你改成取 \(\min\),差分单调递增也是可以的)。
我们可以用 Minkowski 将原本关于大小平方的卷积优化到关于大小线性。
做法是对于 \(f,g\) 差分之后,取差分前若干大归并就行了。\(h_0=f_0+g_0\),然后 \(h_i=h_{i-1}+\Delta _i\),其中 \(\Delta_i\) 是对于 \(f\) 和 \(g\) 的差分数组进行归并排序之后的第 \(i\) 大元素。
ZROI2834.念念不忘
有一棵有根树,其中 \(1\) 号点为根,对于 \(2\) 到 \(n\) 的每个点,\(i\) 的父亲为 \(b_i\) \((1 \leq b_i < i)\)。
一开始,每个点上都有一个棋子。每次可以选择一个棋子,把它往父亲方向移动一步。你可以进行上述操作任意多步,但是不能移动 \(1\) 号点的棋子。
每个点有个权值,第 \(i\) 个点的权值为 \(f_i\)。假设最后 \(i\) 点上面有 \(c_i\) 个棋子,问 \(\sum\limits_{i=1}^{n} f_i \times c_i^2\) 的最小值是多少?
\(2 \leq n \leq 5 \times 10^5\), \(1 \leq f_i \leq 10^{12}\), \(1 \leq b_i < i\)。
树上背包 dp,考虑 \(dp_{i,j}\) 表示 \(i\) 子树放了 \(j\) 个点,我们发现这是凸的。也就是 \(dp_{i,j}-dp_{i,j-1}\) 递增。
我们可以用启发式合并差分数组,取最小前若干的差分即可。
原理是如果差分不递增的话,我们优先选小的差分会造成某个子节点的第一差分没选,选了第二差分。这显然是不行的,因为我们发现如果要选一段差分,必须从头选,只有最小化且差分递增的时候才能用。
这本质
CF280D k-Maximum Subsequence Sum
考虑分治解决,可以发现这是一个 \((\max,+)\) 卷积。
由于具有凸性可以考虑 Minkowski 和,同时为了处理多组询问,放到线段树上即可。
不过要处理好端点没取完的情况,我们要记录端点是否被选择,如果两个边界都被选择的段接在了一起,就会合并。只需要开两维 \((0/1,0/1)\) 然后如果合并的时候遇到了两个 \(1\) 相遇,那么最后的段数也要 \(-1\)。
ZROI2842.六月雪
先做一步最优调整,对于 \(0 1\) 作 \(\pm 1\) 前缀和,发现对于一个和为 \(k\) 的段可以拆成 \(k\) 个权值为 \(1\) 的段是更优的。我们考虑限制一下段的权值之和 \(\le 1\)。
正着做不好做,考虑反过来做,非正的区间我们设其和 \(sum_0\),那么正的段总和为 \(sum_1=sum-sum_0\),为了最大化 \(sum_1\),我们就要最小化 \(sum_0\),也就是现在要划分 \(i\) 段非正,使得其总和最小。
假设求出来选 \(i\) 段和最小为 \(f(i)\),那么答案就是用 \(k\) 中的 \(i\) 来选负段,用 \(k-i\) 个来选择正段产生贡献,\(\max\limits_{i=1}^k\{\min(sum-f(i),k-i)\}\)。
现在的问题就变成选 \(k\) 个区间和最小。想一想这种非连续的东西是不太好在 \(O(n\log n)\) 的时间内直接 DP 的,可以用分治。我们设 \(dp_{l,r,cnt,0/1,0/1}\) 表示在区间 \([l,r]\) 中选了 \(cnt\) 个区间权值和最小,0/1 表示左右端点是否闭合。可以发现 DP 是凸的,于是我们直接闵可夫斯基和,也就是对于 \(cnt\) 的 \((\min,+)\) 卷积。这样子时间复杂度就是 \(O(qn\log n)\) 的。
发现上述分治比较像线段树结构,于是我们可以使用线段树维护闵可夫斯基和的过程。
对于一个询问,外层先 wqs 二分,然后就没有对于段数的限制了。把询问区间分成 \(O(\log n)\) 个线段树区间,对于每个区间的 \(4\) 种 \(\{0/1,0/1\}\) 组合分别找到最优 \(cnt\),然后合并这 \(O(\log n)\) 个 DP 值即可。
如何对于单个区间找到最优 \(cnt\)?可以发现这是对于凸壳上的一系列点 \((x_i,y_i)\),找到最优 \(y_i-kx_i\) 的过程。如果直接二分的话会多一个 \(\log\),但是注意到值域只有 \(O(r-l+1)\),我们直接开一个桶提前预处理若干最优选择即可。
双栈结构优化 DP
通常用于带删除的 DP。
小 \(\omega\) 的仙人掌
求最短区间 \([l,r]\) 使得 \((w_i,v_i)~(l\le i \le r)\) 做完背包后权值为 \(w\) 的代价小于 \(v\)。\(n\le 10^4,w_i~w\le 5\times 10^3,v_i\le 2\times 10^5,v\le 10^9\)。
\(\operatorname{Trick}\): 双栈优化 DP。需要动态加减不可减信息的时候,我们发现线性次数加法是可以接受的,可以考虑这么一个双栈结构。
比如在本题中,我们发现区间满足双指针性质,但是背包不太好减,观察到值域很小,于是我们可以维护多个背包,然后似乎也有点难办,因为第 \(i\) 个点的背包是依赖于 \(i-1\) 个点的所以删掉第 \(i-1\) 个点的背包依然无法消除第 \(i\) 个点背包内 \(i-1\) 的痕迹。
我们不妨维护两个栈 \(s_1\) 和 \(s_2\)。\(s_1\) 从上到小维护的是 \(r~r-1~r-2\dots p\),\(s_2\) 维护的是 \(l~l+1\dots p+1\),其中 \(l\) 和 \(r\) 就双指针的两个位置。\(s_1\) 为正着做一遍背包,\(s_2\) 为倒着做一遍背包,这个样子我们每次移动 \(l\) 指针,只需要弹出 \(s_2\) 栈顶即可,每次移动 \(r\) 是不断往 \(s_1\) 中加。如果 \(s_2\) 被清空,我们就把 \(s_1\) 中的所有二元组弹出来依次压入 \(s_2\) 中,这样子每个二元组入栈两次,时间复杂度 \(O(nw)\)。
LOJ6515.「雅礼集训 2018 Day10」贪玩蓝月
如果是离线那就线段树分治。
如果强制在线的话,我们可以类似 deque 的实现,使用两个对顶栈来维护 DP 的过程。
实现技巧和上一题基本差不多。
字符串相关
ABC240Ex Sequence of Substrings
暴力做法就是把所有子串抽出来进行字典序排序,这个可以在字典树上完成。
然后每次转移直接暴力找到位置在它之前的,排名比它小的子串进行转移,时间复杂度 \(O(n^2\log n)\)。
子串数量已经是 \(O(n^2)\) 了,如果直接对于全体子串为状态做转移必然是不可行的。这启发我们实际有效状态应该很少,观察数据范围大概是要求出现 \(\sqrt n\) 的东西。
之前做字符串题遇到过一个结论就是对于若干个长度和为 \(n\) 的字符串,不同的长度种类的上限是 \(O(\sqrt n)\) 的。本题选取若干个原串不相交的子串也是同理。但是这还是难以完成本题,不过至少提示了我们可以从长度下手。这里要结合字典序的性质下手,如果我们最终选取的子串中相邻两个串的长度差 \(\ge 2\),那么对于长的那个串可以缩短为长度差等于 \(1\),依然不改变字典序大小关系。
于是我们就可以发现,所选取的子串长度最大也是 \(2\sqrt n\),我们把所有满足要求的子串取出来跑最开始说的暴力算法即可。
ACAM 优化状态转移
P5319 [BJOI2019] 奥术神杖
乘积式取 ln,转化为和式的平均数,这可以用 0/1 分数规划解决。
然后在 AC 自动机上跑 DP 就可以了。设 \(dp_{i,j}\) 表示前 \(i\) 个字符,到了 AC 自动机的节点 \(j\) 所得到的最大权值。遇到关键节点就增加权值。
一些常见的模型
区间约束类
通常为给出若干的区间,要求必须满足区间条件或者完成区间任务可以加分。
一般做法: 以区间边界为转移点进行转移,合并限制发现会有几个关键点,于是 \(pos_i\) 表示上一个关键点最晚出现的位置 (\(pos_i < i\))。\(f_{i,j}\) 表示在位置 \(i\) 上一个关键点为 \(j\)。注意维度可以压缩,比如例 1 直接压掉了第一维。例 3 强制钦定转移位置压掉了第二维。
类似的题目有:
下面给出一道例题。
CF1327F AND Segments
1010101010101001 0 01
1100100100101001 0 01 \(\gets\)
1010101010101000 0 00
1001001011011010 1 01 \(\gets\)
1010110101010101 0 11
考虑到二进制的独立性,所以可以拆位统计。这题拆位又碰上区间,二维限制有点难想。可以画个图,那一竖列为拆出来的一位。 两个箭头代表区间左右端点,加粗的几个数字就是一条限制拆位后约束的数字。这样就一目了然了。
按位与的性质得出,若区间与为1则必须全部为 \(1\),与为 \(0\) 的话区间至少一个 \(0\)。看起来可以先将 \(1\) 填入,再考虑 \(0\) 的放置,然后计数原理乱搞。但是限制为 \(0\) 的区间有重叠就很恶心。
那就只能 \(dp\) 了。二维 \(dp_{i,j}\) 表示在 \(i\) 位时,上一个 \(0\) 填在第 \(j\) 位的方案数。
- \(j<pos_i\) \(~dp_{i,j}=0\)
- \(pos_i \le j< i\) \(~dp_{i,j}=dp_{i-1,j}\) (上一个 \(1\) 在 \(j\) 处,故 \(i\) 处只能填 \(0\) )
- \(j=i\) \(~dp_{i,j}=\sum\limits_{k=pos_i}^{i-1}dp_{i-1,k}\)
看似复杂度不行,但是列出状态转移方程后我们可以惊喜的发现并不需要第一维,直接维护段和即可。这就启发我们有的时候先大胆列出式子哪怕复杂度爆了,后面再优化。
连续段 DP
P9197 [JOI Open 2016] 摩天大楼
双倍经验:P2612 [ZJOI2012] 波浪。
我们将排列放在平面上,可以画出折线。总和显然是相邻纵坐标之差。
考虑对于这个平面进行从上到下的扫描线,在扫描的时候看扫描线与折线交点,然后进行 DP。
维护已经扫描过的点构成的连续段,可以发现连续段内部由于相邻的已经安排上了所有无贡献,只有两个端点朝外有贡献,这里贡献提前计算,每次向下移动一格扫描线,就把所有相邻为选择的点差值累加 \(1\)。
设 \(dp_{i,j,0/1,0/1}\) 表示目前已经有 \(i\) 个连续段,总和为 \(j\),第一个数/最后一个数有没有被加进去的方案数,答案就是 \(\sum dp_{1,\leq L,1,1}\)。
转移的时候如果扫到了一些位置,有如下转移:某个位置新开了一个段导致段数 \(+1\),某个位置合并了两段导致段数 \(-1\),某个位置延续了之前的段此时段数不变。
XYD20932.情报传递
小 \(K\) 收到了一份情报,他要把这份情报传给他的所有朋友。他的 \(n\) 个朋友排成一圈,初始所有朋友都不知道这份情报,在每个时刻会发生下面的事:所有知道这份情报的人会将情报传输给自己相邻的两个人。
然后小 \(K\) 会随机选择\(n\)个朋友中的一个将情报传输给他(已经收到情报的人还可能再次收到)。小 \(K\) 想问你期望多长时间后他的\(n\)个朋友都能收到这份情报,答案对质数\(998244353\)取模。\(n\le 5000\)。
连续段 dp 好题。
我们设 \(f_{i,j}\) 表示这些线段在环上一共覆盖了 \(i\) 个点,形成了 \(j\) 个段的概率,\(g_{i,j}\) 表示期望。这些段我们在开始的时候不设置其相对位置关系,只是要求他们不相邻,否则可以合并成一个段。
每轮,每个段会向左右各扩展一个格子,同时会有一个点被选择。考虑先一个一个来做,先做扩展。
如果你直接向两侧扩展的话,难以确定当前覆盖了几个点,因为可能一个向左,一个向右,二者就重合了。但是如果我们先让所有的段向左扩展一个,转移到新数组 \(f,g\to c,d\),这样子就可以确定覆盖点的个数是增加 \(j\) 的。于是有 \(f_{i,j}\times S_{j,k}\to c_{i+j,k}\),其中 \(S_{j,k}\) 表示把 \(j\) 个无序连续段合并成 \(k\) 个的方案数。由于你之前是无序的,所以新加入之后是要计算一个顺序的,比如一个由 \(m\) 个小段合并成的大段的贡献就是 \(m!\)。
考虑递推 \(S_{i,j}\),这个推导类似于斯特林数的递推。每次新加入一个元素,其可以自己新开一个段,也就是由 \(S_{i-1,j-1}\) 转移而来。也可以选择插入原先的段中,一个长度为 \(k\) 的段能插入的位置个数是 \(k+1\),\(\sum\limits_{1\le z\le j} k_z+1=i-1+j\),所以也可以由 \(S_{i-1,j}\times (i+j-1)\) 转移而来。
这样子就完成了向左扩展,向右扩展也是同理 \(c,d\to x,y\)。
于是 \(x,y\) 就代表扩展了,但是未选择点的 dp 数组。现在时间流动了 "1",所以时间的期望要累加对应的概率,\(y_{i,j}\gets x_{i,j}\)。现在要 \(x,y\to f,g\)。下面 \(f,g\) 的转移相同,只列举其中一个。
当前选择的点可以是之前已经选择过的点,那么不会产生任何新的覆盖。有 \(f_{i,j}\gets x_{i,j}\times \dfrac{i}{n}\)。当前选择的点可能是原先连续段旁边的一个点,这样子那个段的长度就增加 \(1\) 了。我们可以选择放在某个连续段的左边或者右边,于是有 \(2j\) 个选择,那么 \(f_{i,j}\gets x_{i-1,j}\times 2j\)。可以新开一个段,由于这里是不带相对顺序的 \(\dfrac{1}{n}\) ,后面合并段的时候会确定这个贡献。\(f_{i,j}\gets \dfrac{1}{n}\times x_{i-1,j-1}\)。还可以合并两个段 \(f_{i,j}\gets \dfrac{1}{n} x_{i-1,j+1}\times A_{j+1}^2\)。当 \(j=0\) 的时候代表之前只剩一个段了,我们把它左右端点相接,自己合并自己。注意 \(j=0\) 的状态代表已经结束不可再别的状态转移。
初始状态是 \(f_{1,1}=g_{1,1}=1,x_{1,1}=y_{1,1}=0\)。因为你第一轮相当于没有左右扩展的过程,只有选点的过程,所以 \(x,y\) 是 \(0\)。
可以发现第二维的大小为 \(O(\sqrt n)\) 的,于是时间复杂度 \(O(n^2)\)。
网格图行走凸函数 DP
通常用于在一个二维网格图上的行走,给出 \(q\) 个点,求到达它们的最小代价。
直接暴力平方 dp 会超时,这个时候考虑维护整体 dp,通常由于贡献函数为凸函数或者一些取 \(\min\) 操作,会让 \(f_i(x)\) 是一个凸函数,每次会根据更优的斜率选择,更新一段前缀或者后缀。
然后这个函数的维护,就是用一个栈来维护,因为你更新的只有一段前缀或者后缀,符合栈的性质。栈内部需要保存这个凸壳的直线信息。保存这个点的横坐标以及斜率即可,还有一些对全局移动坐标的 tag。然后更新和二分栈维护四边形不等式是一样的,直接考虑如果当前栈顶直线整段都不如新加入的直线就把当前直线删除,如果新加入直线在刚开始就不如栈顶直线,那么直接退出更新,否则在中间二分找到一个交点,满足交点之前新加入直线更优,交点之后栈顶直线更优,更新信息之后退出。
代码很有细节这里放一下两道例题的 Code。
P7294 [USACO21JAN] Minimum Cost Paths P
很显然的整体 dp 的形式。
我们设 \(dp_{i,j}\) 表示考虑了前 \(i\) 列,走到第 \(j\) 行的代价。
因为贡献函数 \(j^2\) 是下凸函数,可以猜测,固定 \(i\),\(dp_{i,j}\) 是一个下凸函数。也就是 \(dp_{i,j}\) 的差分数组单调不减。
每次从 \(i-1\to i\),对于 \(dp_{i}\) 先是直接继承 \(dp_{i-1}\),然后对于每个位置打上一个 \(j^2\) 的标记。
现在考虑列内的转移。上面说了 \(dp_{i,j}\) 的差分数组单调递增,而行内观察形式其实就是差分数组对于 \(c_i\) 进行一个 chkmin。由于差分数组单调,于是我们直接二分找到变化点进行修改即可。
由于第二维是 \(O(n)\) 的,我们不能直接维护。考虑使用单调栈维护转折点。转折点之间都是一条一次函数。
直接的思路是维护一个差分数组,这样子非常方便更新。但是由于不是问 \((1,1)\to (n,m)\) 的代价,而是中间有 \(q\) 个询问,我们不可能每次询问都累加一次差分数组吧。
于是考虑维护 \(f_i(x)=(i-j)\times x^2+c_j\times x-b\) 的形式。这个 \((i-j)\times x^2\) 就是上面说的一个 \(+x^2\) 的 tag,其中 \(j\) 表示上次该点被 chkmin \(c_j\) 更新的时间点 \(j\)。由于上次是被 \(c_j\) 更新,所以就是形式就是 \(val_p+c_j\times (x-p)\),其中 \(p\) 表示前面一个断点,拆开就是 \(c_j\times x+b\)。综上所述,我们只需要在单调栈中对于每个断点维护一个三元组 \((x,j,b)\) 即可。
每次修改由于我们不是直接维护的差分数组,所以可以直接用 \(f_i(x)-f_i(x-1)\) 得到差分值。对于后缀推平,我们可以直接从后缀开始不断 check 并且 pop,由于每列只会进队列 \(O(1)\) 个的点,所有点也只会被删一次,所以复杂度均摊是正确的。pop 到满足题意之后要二分寻找一下,然后插入新的端点。
每次询问就是离线处理,二分找到询问点在栈中哪两个端点之中,然后求值。
时间复杂度 \(O((m+q)\log n)\)。
云斗NOI R7T1 Function

还是维护整体 dp。可以发现每次加入一条斜率为 \(a_i\) 的直线去更新一段前缀,其中强制要求更新第一个位置,可以发现斜率小的直线代表代价低会逐步取代斜率大的直线,所以最后出来的应该是一个上凸函数。按照上述方法维护这个凸函数即可,具体可以参照上面的代码。
其实这个凸函数等价于这个贪心:我们的行动一定是先往左上角行动若干步,然后遇到一个最小的列之后直接在这列往上走到底。
ZROI2885. 激光阵
暴力方程 \(dp_{i,j}=\min(dp_{i-1,j}+w_{up},dp_{i,j-1}+w_{right})\)。
线段树优化 dp
对于这种整个整个往上转移的,而有明显的分段特征的,这里的段就是激光的起点在每一行的作为分界点,分界点之间的 \(w\) 不变。可以考虑线段树维护整体 DP。
发现行内转移不太好处理,我们要利用同一段中 \(w\) 相同,可以维护 \(dp_{i,j}+w_r(j,n)\),这样子行内影响就消除了,每一个位置的值就等价于前缀最小值。
我们只需要处理行间转移以及 \(w_{up}\) 即可,发现 \(w_{up}\) 的值仅在水平激光端点处变化,我们找到端点后所有端点后的值如果是从上一行转移的应该整体 \(+1\)。怎么找到从上一行转移的位置呢,根据前面前缀最小值的性质,我们发现从一个出发应该是一段 \(x,x,x....x,<x,<x\),这个样子 \(<x\) 的位置必然不是通过前缀最小值得到的,于是我们只需要找到第一个小于 \(x\) 的位置,然后给那段后缀整体 \(+1\)。
维护差分法
发现这个 DP 转移式子很有差分的感觉,而且行内具有单调性,于是考虑维护差分。
首先行内转移的式子规定了差分的上界是 \(w_{right}\)。然后可以通过行间转移来使得差分更小,大概就是从小到上会有一个 \(w_{up}\),然后可以用他加上上一行的 \(\delta'\) 来更新这一行 \(\delta\) (初始值是上界 \(w_{right}\))。模拟从下往上合并的过程,其实如果我们是从小往上合并的话,那么初始值应该是上一行的 \(\delta'\),然后和 \(w_{right}\) 也就是上界取 \(\min\),如果是前者小,那么不会发生变化,如果是后者小那就更新 \(\delta\),同时由于这个地方的值变了,那么会影响下一个 \(\delta_{next}\),具体来说下一个值应该会变大。模型化,就是那个点有一个容量上界,我们可以往点里面加球,如果球多于上界,就会一直往右传递,一直转移到最后一行每个位置里的球数就是最后的差分数组。每个位置的容量就等于上一行该位置的容量加上新增的向上的激光。
使用 std::set 维护这个过程就行了。
杂题
Public judge NOIP Round #6 抉择
感觉这种优化到尽头还无法解决的 \(dp\) 就是要挖掘一下性质。
部分分挺简单的,但是正解思路其实和特殊性质最后一档很想,二者的思想都其实是选更多的大概率会更优一点。我们来分析一下为什么不多选,比如选了 \(a_j\) 和 \(a_i\),我们非要在其中插入的一个 \(a_k\),那么可能 \(a_k\) 的很多位都为 \(0\),导致我们损失了一些高位。这里给出一个结论也就是只要 \(a_i~and~a_j\) 与 \(a_i~and~a_k\) 的最高位相同即可,很明显最高位大于其他位之和。所以我们只需要对于每一位保存最近的即可。注意别忘记考虑按位与为 \(0\) 的情况。
P9870 [NOIP2023] 双序列拓展
一道优化 dp 的好题目。
下面只讨论 \(f_i>g_i\),方程是显然的
时间复杂度 \(O(qnm)\)。这显然不太好压缩维度或者用数据结构来优化了。
那就需要寻找 dp 性质了。
方法一:将 dp 方程转为有组合/几何意义的东西
这里可以变成走方格,从 \((1,1)\) 到 \((n,m)\)。我们发现走不到终点的充要条件是有一个围了一圈的封堵。这个直接可以双指针来优化。
方法二:bool 型 dp 提取状态到 dp 值中。
可是很多类似的题目我们都可以发现对于 \(dp_i\) 并不是需要越大越好,所以不能只记录一个最大的 dp 值。每题的解决方法都不同,有的题目是观察出一维度固定另一维度合法的是一个区间,本题是用到了可回退的思想。
我们先最大化 \(dp_i\)。
如果 \(a_i \le a_{i+1}\),那么显然是可以的 \(dp_{i+1}\) 在 \(dp_i\) 的基础上继续扩展,即找到一个 \(b_j \ge a_{i+1}\)。
如果 \(a_i > a_{i+1}\),那么不能继续扩展,我们可以贪心地进行回退,退到第一个 \(b_j<a_{i+1}\),此时对于 \(a_{i+1}\) 是满足条件的,可以证明对于 \(\le i\) 的位置也是满足条件的,因为 \(a_i > a_{i+1}\),所以 \(a_i\) 对位 \(b_j\) 就可以了。时间复杂度 \(O(qn\log m)\)。
考虑去掉 \(\log\),我们发现每次回退又向前很重复。这里有一个观察,如果在位置 \(i \to i+1\) 的时候发生了回退了,如果 \(a_j>a_i\) 那么本次回退是无效的,因为 \(a_j\) 会比 \(a_i\) 走得更远。于是我们发现只有前缀 \(\max\) 才会对答案产生向前的贡献且可以抵消之前的回退。回退的时候只需要检查 \(a_i\) 是否大于 \(b\) 序列的前缀 \(\min\) 即可。需要注意的一点是我们虽然是免去了回退但是后面必须有点可以恢复过来。这就是要求序列的末尾必须是前缀 \(\max\) 特殊性质可以满足这点。如果不是呢,将序列从 \(\max\) 处分成两半,前一半顺着扫,后一半倒着扫,这样可以满足了。
P6847 [CEOI2019] Magic Tree
可以发现约束条件就是子节点被切断的时间要在祖先节点之前,否则就没机会切断了。
于是设出 dp,用 \(f_{u,i}\) 表示在 \(u\) 节点,它和其父亲的边在 \(\le i\) 时刻被切断所得到的最大价值。
注意这里的 \(f\) 其实是前缀最大值因为定义是 $ \le i$。暴力做的话时间复杂度 \(O(nk)\)。
但是这里是二维的,复杂度显然不行,一般这种二维 dp 想要优化的话,一般就是第二维通过转化为图像或者线段树合并啥的或者更难点就是找性质。本题第二维显然是整体 dp 的形式,我们只需要用 STL 或者数据结构维护第二维,每次直接继承儿子的第二维并且进行 \(O(1)\) 个修改。
本题采用 std::map 来维护。
可以发现有用的点并不多,因此 map 中的第二维信息只有子树内出现的 \(d_v\)(相邻 \(d_v\) 之间的 \(f\) 是相同的)。这就会出现一个问题,在合并两个 map 的时候,假设是 \(rt_u\gets rt_v\),如果 \(rt_v\) 中出现过的一个值在 \(rt_u\) 中没出现过,那么对应点累加的时候该点对应的值在 \(rt_u\) 中将会是 \(0\),这不符合前缀 \(\max\) 的要求啊。于是我们考虑维护差分 \(g_{u,i}\),可以注意到差分也是可以对应位置相加的且可以自动完成对于空白位置的前缀 \(\max\) 要求。
这样子我们就完成了 \(\sum f_{v,i}\) 的转移处理,直接启发式合并 map,然后对应位置相加即可。
现在要处理的就是单点(\(d_u\) 位置) \(+val_u\),并且作为前缀值往后更新前缀 \(\max\)。
\(d_u\) 位置 \(+val_u\),是很好做的,我们直接对于差分数组 \(g_{u,d_u}\gets val_u\),然后不可在后面直接 \(-val_u\),因为前缀 \(\max\) 的要求需要保证差分数组恒正。于是我们就直接把中间的一段 \(\sum g_{v,i}\le val_u\) 的差分值直接舍去即可,这一段是暴力往后更新,因为我们可以发现每个点只会进入 map 一次,然后最多被舍去一次,所以时间复杂度均摊是正确的。
还有一种线段树合并的做法,可以去题解区学习。
CF1476F Lanterns
由于目标是全局覆盖,所以我们断断续续地覆盖最后再用后面来覆盖前面空缺的是很难用状态表示的。
于是考虑记录一个连续段 \(dp_i\) 表示到第 \(i\) 个灯笼能覆盖地最长前缀有多长。可是我们如果在中间一段出现一个位置的最优选择是向后,但是他前面没有补齐怎么办呢?
等后面补齐的时候这个位置再往前延展。也就说对于当前的 \(i\) 可以找到一个最靠前的位置 \(j\) 使得 \(i\) 可以覆盖到 \(f_j+1\),于是我们便可以在 \((j,i)\) 之内的位置向前延展了。
P3643 [APIO2016] 划艇
值域过大考虑离散化,然后按照原有端点,分成很多小端。设 \(f_{i,j}\) 表示第 \(i\) 个数的值域在 \([l_j,r_j]\) 之间。考虑转移的时候,\(k \to i\) 如果值域区间不同还可以转移,但是如果他们在同一个值域区间内就不好办了。
那就只能使用大招,强行钦定 \(k \to i\) 的转移是不同区间的,同时 \(k+1,k+2...i\) 都落在 \([l_j,r_j]\) 之间或者取值为 \(-1\)。小细节:由于状态设计要求这里的 \(c_i\) 不能取 \(-1\)。设一个有 \(m\) 个数待选择,那么方案数是 \(\begin{pmatrix} m+Len_j-1\\ m \end{pmatrix}\)。
组合数可以线性递推。
我们仔细观察一下求和式子,似乎可以前缀和优化。但是一定要小心,那个 \(m\) 其实是和 \(k\) 有关的,所以我们只能对于第二维进行前缀和优化。
同时可以用前缀和优化,发现第二维求和与 \(i\) 无关,于是我们可以设 \(s_{i,j}=\sum\limits_{z=0}^{j-1} f_{k,z}\)。那么 \(f_{i,j}=\sum\limits_{k=0}^{i-1}s_{i,j}\times \begin{pmatrix} m+Len_j-1 \\ m\end{pmatrix}\)。
注意细节,为了前缀和方便,我们将 \(j\) 提取到第一维进行枚举。同时,第二维枚举 \(i\) (倒序)也有利于使得 \(m\) 每次只增加 \(1\),便于线性递推组合数。
时间复杂度 \(O(n^3)\)。
P7606 [THUPC2021] 混乱邪恶
综合了各种技巧的背包题。
设计状态 \(f_{0/1,l,g,x,y}\),第一维滚动数组,后面分别表示 \(L,G\) 目前积累的值,然后就是坐标。可以发现每次移动的 \(\pm 1\),可以用 bitset 优化。然后还有一个经典结论就是随机游走的期望最大移动距离是 \(\sqrt V\) 级别的,这样子坐标维度直接开 \(2\sqrt n\) 级别就行了。
CF809D Hitchhiking in the Baltic States
朴素的状态以及转移显然不行,我们可以联想到 LIS 的更优做法,那就是动态维护长度为 \(i\) 的 LIS 结尾数字最小。
本题也可以沿用该思路,设出 \(f_i\)。那么考虑加入一个数 \(\in [l_i,r_i]\),这个时候就需要找到所有的 \(f_i<r\),对应的 \(f_{i+1}= \max(f_i+1,l_i)\)。化简一下,也就是找到最大的 \(j\) 使得 \(f_j<l\),此时 \(f_{j+1}\) 必然大于等于 \(l\),故\(f_{j+1}=l\),其余的 \(l \le f_i<r\), \(f_{i+1} \gets f_i+1\)。
必然是数据结构维护,但是第一感得到的线段树好像并不能有效完成第二个操作。我们发现第二个操作中的 \(f_{i+1}\) 都是强相关于 \(f_i\),于是可以把 \(f_i\) 右移然后区间 \(+1\),那么空出来的那个位置怎么办,其实正好用 \(f_{j+1}\) 来填补。然后末尾多出来一位怎么办,如果本来就可以让答案序列增长一位那么就不用管,否则在后面再删掉一个节点就行了。
UVA10934 装满水的气球
首先确定策略,如果只有一个球那么我们显然只能从下往上一层一层摔直到确认到位置。如果多一个球,那么实验次数就减小了。因为我们可以在某次直接到更高层,然后试错。考虑 dp,由于我们不知道答案楼层是什么,所以楼层不应该放在状态里面。
设 \(dp(i,j)\) 表示用 \(i\) 个球,实验 \(j\) 次可以确定最大高度。
本次测试的楼层应该是 \(dp(i-1,j-1)+1\),因为如果本次气球破了,保证可以用剩下的操作次数和气球完成实验。我们要求的是最高楼层所以应该是要加上最好的情况,也就是气球没破,那就还能往上扩展 \(dp(i,j-1)\) 层。于是 $$dp(i,j)=dp(i-1,j-1)+dp(i,j-1)+1$$
- 虽然是加起来,但是意思不是都要进行一次,其实是根据气球有没有破碎选择其中一种方式来继续询问。
每次询问就是找到一个最小的 \(i\),满足 \(f_{i,k}\ge n\),二分即可。时间复杂度 \(O(T\log K)\)。
ABC221G Jumping sequence
发现每一步都选择一个方向都太难搞了,考虑 \((x,y) \to (x+y,x-y)\) 旋转一下坐标系,那么每一个方向就是独立了的,我们每次可以在每个方向上选择前进还是后退。于是对于每个维度都有
选择 \(p_i \in \{-1,1\}\),使得 \(\sum p_i\times d_i=T\),其中 \(T=A+B,A-B\)。
这很像背包的形式,两边同时加上 \(\sum d_i\),再除以 \(2\),得
选择 \(p_i \in \{0,1\}\),使得 \(\sum p_i \times d_i=\frac{T+\sum d_i}{2}\)。
这是一个 0/1 背包的形式,可以用 bitset 辅助完成。
P10973 Coins
多重背包最大价值可以用单调队列优化,在 \(O(nW)\) 的时间内求解。
对于求哪些价值可以被构造出来,也同样是 \(O(nW)\) 的。除了 \(f_j\) 表示价值 \(i\) 能否被凑出来,我们还需要一个辅助数组 \(g_j\) 表示外层枚举物品 \(i\) 的时候,凑出价值 \(j\) 需要的最少 \(i\) 个数,每次如果个数 \(<c_i\) 就可以转移。
CF1667D Edge Elimination
构造题目依然可以考虑 \(dp\)。当信息只与子树有关所以可以树形 \(dp\),所以本题可以记录与父亲有关状态,使得子树独立。设 \(f_{u,0/1}\) 表达 \(u\) 与 \(fa\) 删边的时候,有偶/奇数条子边。根据子节点的 \(f_{v,0/1}\) 值种类分为 \(4\) 种。如果全为 \(0\),那就无解。对于一个 \(1\) 的情况,显然是在 \(u\) 剩奇/偶数的时候交替删。如果有两个 \(1\),这些东西可以用来平衡。
AGC032D Rotation Sort
本题如果直接去思考变动序列的变化情况,其实是比较难做的。我们并不知道该把目前的数移到哪个位置,因为后面的其他数字变动会影响当前的数字移动的位置。
所以转化思路,我们考虑哪些是不动了。确定不动的序列(这是一个单调递增的数列)的之后,其他数字根据相对大小关系就可以确定往前放还是往后放,付出 \(A\) 或 \(B\) 的代价。
设 \(f_{i,j}\) 表示处理了前面 \(i\) 个数,当前不动序列的最后一个数字为 \(j\)。根据 \(a_i\) 和 \(j\) 大小关系,还有是否选择 \(a_i\) 作为不动序列来转移。
可能咋一看第一个转移有点小问题,就是 \(a_i>j\) 不一定是要往后放啊,可能后面的移动到前面这个的位置就正确了。但其实这个决策是被包含在了第三个方程里面所以这个转移没有问题。
本题数据小,直接暴力转移 \(O(n^2)\) 即可。
ARC125F Tree Degree Subset Sum
结论是对于重量为非负数且 \(\le n+1\) 的背包,如果构成某重量,最少用 \(l\) 个,最多用 \(r\) 个。那么 \([l,r]\) 内的任意个数个都可以凑成该重量。
对于一棵树有 \(\sum d_i=2n-2\),令 \(d_i\gets d_i-1\),有 \(\sum d_i=n-2\),可以套用上述结论。
同时和为定值的数一共有根号种,直接二进制拆分多重背包即可。
P9084 [PA 2018] Skwarki
看到序列最值,考虑对于笛卡尔树进行计数。
首先有一个观察就是只会删除 \(\log n\) 轮。
可以发现每次都会删除 \(<2\) 个子树的点,当然最左边的点和最右边的点需要特殊讨论。
同时由于全局最大值的存在,除了 \([1,n]\) 特殊考虑之外其余每段区间最多有一个边界。
因此设 \(f_{i,j,0/1}\) 表示将一颗大小为 \(i\) 的笛卡尔树在经过 \(j\) 次操作后删光的方案数。其中这颗笛卡尔树有无边界点。
先讨论无边界点的情况,这个时候还需要分类讨论,一个就是根最后被删除,一个是儿子最后被删除。
对于前者需要两个儿子删除轮数都是 \(j-1\),枚举左子树大小,用组合数分配一下权值即可。
对于后者,要求一个儿子轮数为 \(j\),另一个为 \([0,j-1]\),注意系数还有一个 \(2\) 代表左还有右最后删除。
有边界的情况,要么是有边界的那一侧最后被删光,要么是那一侧被删光之后根拥有了边界,然后根最后被删,也是类似转移。注意这里的第二个转移没有系数 \(2\),因为有一个第三维是 \(1\),另外一个是 \(1\),两个 \(f\) 数组可区分。
上一个前缀和优化,最后在根处特殊处理一下转移。
对于初始有 \(f_{0,0,0}=f_{0,0,1}=1\),按照区间大小从小到大转移即可。时间复杂度 \(O(n\log n)\)。
QOJ7199.Bomb
我们需要连出双向可达性,也就是所有点在同一个 SCC 内部。
考虑对于一个前缀进行 DP,假设目前已经对于前 \(i\) 个点完成了他们的半径选择。那么图的形态一定是若干段区间在同一个 SCC 内部,且 SCC 是一条链,前面的连向后面的,因为我们可以通过后续设计半径来使得后面某个覆盖到最前面来合并前面的一系列 SCC。而且我们只关心第一个 SCC 的形态,因为能覆盖到第一个 SCC,且第一个 SCC 会往后连,那么所有点都可以双向可达了。
考虑何时第一个 SCC 能发生扩展,假设是从黑色扩展到橙色,需要满足以下条件
-
有绿色的返回边
-
两段之间有一个蓝色连边
-
橙色段内是要从头到尾串起来(最优形态一定是每一个连向下一个)。

橙色的可以直接前缀计算,绿色也很好写,但是这个蓝色需要一些讨论,蓝色的不一定是 \((i,i+1)\) 这条边,可以是 \([1,i]\) 中的某个点覆盖过去的,但是我们不能只记录是否覆盖到 \(i+1\),可能其可以覆盖得更远,为我们之后服务。所以直接 DP 的做法是就是记 \(f_{i,j}\) 表示前缀 SCC 包含了 \([1,i]\),最远覆盖到了 \(j\) 的最小代价。
DP 的约束就是第二维要大于第一维。
暴力转移的时间复杂度是 \(O(n^4)\) 的,如果进行一些分类讨论可以做到 \(O(n^2)\) 的。
但是这个做法还是太复杂了,这里有一个线性做法,就是我们直接记录第二维度还是太麻烦了。
考虑找到一个极大的转移区间进行转移,第一个前缀 SCC 覆盖 \([1,i]\) 之后,恰好可以连接到 \(i+1\)。

按照这个方式连边就可以做到线性 DP 了。其中绿笔部分为中点的时候最优。设 \(f_i\) 表示前缀 \([1,i]\) 为一个 SCC 且和 \(i+1\) 有连边的最小代价,每次暴力枚举 \(j\) 转移,时间复杂度 \(O(n^2)\)。
CF889E Mod Mod Mod
第一次见到这种转移的 DP。本题我们只记录当前最优,剩下的递归到后面转移。
首先注意到只有前缀最小值的 \(a_i\) 才有用,保留这些关键序列即可,记为 \(\{b_i\}\)。设 \(pre_i\) 表示 \(b_i\) 以及它之前包括被删的数有多少数字。
倒着 DP,设 \(f_i\) 表示将前 \(i\) 个 \(b\) 都改成 \(b_i\) 的最优答案。假设在 \(b_{i+1}\) 处我们选择了 \(x_2\),在 \(b_i\) 处选择了 \(x_1\),肯定是希望最大化 \(x_1\),当 \(x_1<b_{i+1}\bmod b_i\) 的时候,\(x_2=\lfloor\dfrac{b_i}{b_{i+1}}\rfloor b_{i+1}+x_1\),当 \(x_1\ge b_{i+1}\bmod b_i\),\(x_2=(\lfloor\dfrac{b_i}{b_{i+1}}\rfloor-1) b_{i+1}+x_1\)。
第二种转移是可以直接做的,因为不管 \(x_1\) 取什么值,这个转移都是合法的, \(dp_i\gets dp_i+pre_i(\lfloor\dfrac{b_i}{b_{i+1}}\rfloor-1) b_{i+1}\)。
第一种转移很难做,因为我们并不知道 \(dp_i\) 中的 \(x_1\) 取值。这个时候我们可以直接往前递归,找到一个最小的 \(j\),满足 \(b_j\le b_{i+1}\bmod b_i\),按照上述讨论继续转移即可。由于每次取模大小会减少至少一半,所以我们单次只会转移 \(O(\log V)\) 次。
时间复杂度 \(O(n\log n\log V)\)。
P6891 [JOISC2020] ビルの飾り付け 4
首先 \(O(n^2)\) 的 \(dp_{i,j,0/1}\) 是显然的。
CF1444E Finding the Vertex
P5912 [POI2004] JAS
Uninity
貌似这种交互库动态构造的题目都是要先用 \(dp\) 求出最优询问策略。
询问 \((u,v)\),根据回答,在 \(u/v\) 的子树中寻找。寻这个过程就是一个边分治的过程。我们需要最小化边分树每条边的深度。当边深度满足条件的时候,边分树和边深度集是双射的。需要满足的条件就是对于任意两条权值相等的边,他们公共祖先的边需要有边权值小于他们。

浙公网安备 33010602011771号