动态规划题目选解
时不时可能会更新一两下qwq
- Past:比较简单的小清新dp
- Present:感觉有点难度或者属于某种没见过的套路的题
- Future:思维难度比较高/代码很难写/使用了一些黑科技
CF837D Round Subset Past 4
首先一个数末尾 \(0\) 的个数就相当于其唯一分解中 \(2\) 和 \(5\) 指数的 \(\min\)。因此可以考虑算出来 \(p_i,q_i\) 分别为 \(a_i\) 唯一分解中 \(2,5\) 的系数,现在问题就变为:
- 有两个序列 \(p,q\),你需要选取 \(k\) 个位置 \(i_1,i_2,\cdots,i_k\) 使得 \(\min\left(\sum_j p_{i_j},\sum q_{i_j}\right)\) 最大。
考虑一个简单的 dp:设 \(F(i,j)\) 表示前 \(i\) 个数选了 \(j\) 个的最大价值。思考一下如何转移......
......
......
......
发现很难转移。这是因为我们面对的是两个序列,仅仅一个下标上的信息不足以转移。
注意到 \(p_i\) 与 \(q_i\) 都很小,只有 \(O(\log V)\) 级别(\(V=10^{18}\) 为值域),因此考虑在状态里加一维:\(F(i,j,S)\) 表示前 \(i\) 个数中选 \(j\) 个,且 \(\sum q_i=S\) 时最大的 \(\sum p_i\)。
这里选取了 \(\sum q_i\) 而非 \(\sum p_i\) 是因为 \(q_i\) 实际上不会超过 \(\log_5a_i\),\(p_i\) 的底数则是 \(2\),相对大一些;这样可以减小一半左右的常数。
这时候转移方程就呼之欲出了:考虑选不选第 \(i\) 个位置,转移方程就是
注意到 \(S=O(n\log V)\),因此总的时间复杂度为 \(O(n^3\log V)\)。
空间复杂度如果不用滚动数组是 \(O(n^3\log V)\),会 MLE,需要滚动数组滚掉一维。
ABC222E Red and Blue Tree Past 3
首先我们对每个 \(1\le i<n\),将 \(A_i\to A_{i+1}\) 中间的这些边的边权都加一。
设最后的边权序列为 \(x_{1\cdots n-1}\),把红边看做系数 \(c_i=+1\),蓝边看做系数 \(c_i=-1\),那么只需要最后所有 \(\sum x_ic_i=K\)。
\(f(i,j)\) 表示前 \(i\) 个数和为 \(j\) 的方案数,转移枚举最后一个填 \(+1\) 还是 \(-1\) 随便做。
注意 \(j\) 可以是负数,因此需要平移一波值域。
边权之和最大可以是 \(O(nm)\),因此总的时间复杂度为 \(O(n^2m)\)。
CF1575L Longest Array Deconstruction Present 6
dp:设 \(F(i)\) 表示在前 \(i\) 个数中,强制令 \(a_i\) 位于 \(a_i\) 位置上的最大 \(f\) 值。那么这要求 \(a_i\ge i\)。
那么考虑枚举 \(a_i\) 删到位置 \(a_i\) 上后,离它最近的一个满足 \(a_j=j\) 的数是哪个。这样就得到了一个类似 \(F(i)=\max\{F(j)+1\}\) 的形式。
思考一下发现这个 \(j\) 需要满足以下两个条件:
- 由于删完之后 \(a_j\) 在 \(a_i\) 前面,因此要有 \(a_j<a_i\)。
- 由于把 \(a_i\) 移到 \(a_i\) 位置上需要删 \(i-a_i\) 个数,把 \(a_j\) 移动 \(a_j\) 位置上需要删 \(j-a_j\) 个数,而 \(a_j\) 在 \(a_i\) 前面,因此在 \(a_j\) 前面删的数必然会算进 \(a_i\) 前面删的数里面。因此必须要有 \(j-a_j\le i-a_i\),不难发现这也是充分条件。
- 注意到前两条移项一下就等价于 \(j\le i+a_j-a_i<i\),因此也保证了 \(j<i\)。很妙。
因此转移方程就是:
这个形式直接转移是 \(O(n^2)\) 的。加了一些阴间乱搞过了 CF 的官方数据
优化其实很好想。。这是个典型的二维偏序形式,因此按照其中一维排序然后从前往后用一个线段树维护一下前缀最大值就行了。
CF1556F Sports Betting Future 7.5
设 \(f(i,S)\) 为 \(i\) 打败了 \(S\) 中所有人的概率,其中限定 \(i\in S\)。那么设全集 \(U=\{1,2,\cdots,n\}\),答案就是 \(\sum f(i,U)\)。
直接转移好像很难搞,我们考虑容斥:如果 \(i\) 没有打败 \(S\) 中的所有人,那么如果 \(i\) 打败的人组成的集合为 \(T\),概率就是
因此,我们得到转移方程:
考虑预处理一下 \(g(i,S)=\prod_{j\in S}\dfrac{a_j}{a_i+a_j}\),那么转移方程就变为
递推一下,预处理的复杂度就是 \(O(n2^n)\)。而转移的复杂度为 \(O(n^23^n)\),故总的复杂度为 \(O(n^23^n)\)。
看上去 \(3^{14}\cdot 14^2=937461924\) 很大,但理性分析一下可以发现这东西的实际操作数为
算了一下发现 \(n=14\) 的时候只有 \(214904886\),也就 2e8,还是很稳的。
这种 dp 后做容斥,确定一部分另一部分随便选的思路其实很常见,例如一个经典的问题:
点数为 \(n\) 的无向连通图有多少个?
设 \(F(i)\) 表示点数为 \(i\) 的连通图个数。设点数为 \(n\) 的图的个数为 \(g(n)\),如果图不连通,那么考虑枚举 \(1\) 所在的连通块大小,得到转移方程:
\(g(n)=2^{\frac{n(n-1)}{2}}\) 可以轻易求出。这样就做到了 \(O(n^2)\) 的时间复杂度。再如:
ABC213G Connectivity 2 Present 6
设 \(f(S)\) 为删边后 \(S\) 中点仍然连通的方案数,其中钦定 \(1\in S\)。设 \(g(S)\) 为 \(S\) 中的点在原图中的边数。
考虑 \(S\) 中点不连通的方案数:枚举 \(1\) 所在连通块中的点集 \(T\),得到
算出所有的 \(f(S)\) 后,\(1,k\) 连通的方案数是很好求的。时间复杂度 \(O(3^n)\)。
ABC236G Good Vertices Present 5
其实这题的重点不是 dp......不过也很具有代表性所以就写上来了
首先我们考虑给在时刻 \(i\) 插入的边赋予权值 \(i\),那么现在要做的就是:
对每个 \(u\),求出 \(1\to u\) 的一条路径,使得路径边数恰为 \(L\),且权值中的 \(\text{max}\) 最小。
这一步转化才是这个题的重点,剩下的 dp 其实就是个套路
设 \(F_K(u)\) 表示 \(1\to u\) 边数为 \(K\) 的路径中边权 \(\text{max}\) 的最小值。
枚举 \(u\) 上一步的节点是谁,那么转移方程就是:
其中若 \(w(v,u)\) 表示 \(v\to u\) 边权的权值,若不存在则为 \(\infty\)。
由于 \(\text{min}\) 和 \(\text{max}\) 都满足结合律,我们考虑定义广义的矩阵乘法:
那么就有
这是个典型的矩阵快速幂优化形式,使用矩阵快速幂,时间复杂度为 \(O(n^3\log L)\)。
路径长恰为 \(L\) 这种形式一般都可以矩阵快速幂优化 dp。
有一个比这个简单一些的题是 AT4539 Walk,其实这个比上面那个更板子(
其实这个东西还可以用来处理全源最短路:\(F_k(i,j)\) 表示图上 \(i\to j\) 路径经过边数为 \(k\) 的最短路长度,不难得到转移方程 \(F_k(i,j)=\min_{x}F_{k-1}(i,x)+w(x,j)\)。注意到 \(k\le n\),类似地推广一下矩阵乘法就可以做到 \(O(n^3\log n)\) 了。
不过这个东西很逊,远远没有 \(\text{Floyd}\) 之类的算法快,而且不好写(
CF1442D Sum Future 7.5
首先注意到一个性质:至多有一个序列没有被选满。
换句话说就是,只要选了一个序列的第一个数,那么最优方案一定是尽量把这个序列的所有数都选上。
其实证明很容易......这里简单写一下:
- 注意到序列是不降的
- 如果我们剩了两个序列都没有选满,假设两个序列选到的地方下一个数分别是 \(x,y\)
- 如果 \(x>y\),那么 \(x\) 肯定比 \(y\) 前面那些数都大,也就是说选上 \(x\) 换掉 \(y\) 前面的一个数一定更优。
- 反之如果 \(x<y\) 也是同理的。
- 这样一直替换下去,必然会选空其中一个序列。因此不可能有两个序列都没有选满。
这样一来,我们就可以把每个序列看做一个物品,价值就是序列中所有数的和,重量则是序列的长度。
枚举没有选满的是哪个序列,以及具体选到了哪里,我们要做的相当于以下的问题:
给定 \(n\) 个物品,对 \(i=1,2,\cdots,n\),你需要求出:去掉第 \(i\) 个物品后,对其他物品做背包的结果。
如果直接暴力那么复杂度是 \(O(n^2k)\),过不去。
实际上这是一个经典的套路。注意到插入一个物品的复杂度是 \(O(k)\),我们考虑一个分治算法:
- 对于区间 \([l,r]\),找到一个中间点 \(m\)。
- 先将 \([l,m]\) 中的物品插入到背包中,然后 \(\text{solve}(m+1,r)\)。
- 将背包还原,再将 \([m+1,r]\) 中的物品插入到背包中,然后 \(\text{solve}(l,m)\)。
- 这样一来,如果当前的区间是 \([l,r]\),可以发现此时背包中的物品恰为 \([1,l-1]\cup[r+1,n]\) 中的物品!
- 因此,当 \(l=r\) 时我们得到的就是想要的答案。
时间复杂度是 \(T(n)=2T(n/2)+O(nk)=O(nk\log n)\)。
其实后面那个问题还可以分块:我们对每 \(\sqrt{n}\) 个物品做一个背包记录下来,这一步复杂度 \(O(nk)\)。
那么每次只需要做 \(\sqrt{n}\) 次背包的合并。
两个背包合并起来就是一个 \(\text{max+}\) 卷积,直接 \(O(k^2)\) 暴力做,复杂度就是 \(O(k^2\sqrt{n})\)。
总的复杂度为 \(O(k^2\sqrt{n}+nk)\)。
CF1151F Sonya and Informatics Future 7.0
非降序列就是让 \(0\) 都在前面,\(1\) 都在后面。设 \(0\) 的数量为 \(m\)。
考虑设 \(F(i,j)\) 为前 \(i\) 次交换后,前 \(m\) 个数中有 \(j\) 个 \(0\) 的方案数。
考虑这 \(j\) 个 \(0\) 是怎么来的:
- 有可能之前只有 \(j-1\) 个 \(0\),第 \(i\) 次交换将后面的一个 \(0\) 和前面的一个 \(1\) 交换了。注意到前面 \(1\) 的个数和后面 \(0\) 的个数都是 \(m-j+1\),因此方案数为 \(F(i-1,j-1)\cdot (m-j+1)^2\)。
- 有可能本来有 \(j+1\) 个 \(0\),第 \(i\) 次换走了一个。前面有 \(j+1\) 个 \(0\),后面有 \(n-2m-(m-j-1)=n-2m+j+1\) 个 \(1\),因此方案数为 \(F(i-1,j+1)\cdot(j+1)(n-2m+j+1)\)。
- 有可能本来就有 \(j\) 个 \(0\),第 \(i\) 次交换并没有什么用。分四种情况讨论:交换了两个前面的,交换了两个后面的,交换了前后的两个 \(1\),交换了前后的两个 \(0\)。那么方案数就是 \(C_m^2+C_{n-m}^2+j(m-j)+(m-j)(n-2m+j)\)
然而 \(k\le 10^9\),并不能直接这么转移。
见过了矩阵快速幂优化 dp 的套路之后,再看到 \(10^9\) 的范围,很容易想到把这个转移写成矩阵。
\(F(i,j)\) 和 \(F(i-1,j-1/j/j+1)\) 有关,那么只需要在矩阵的这三项中填上对应的系数,其余都填 \(0\) 即可。
时间复杂度 \(O(n^3\log k)\)。AC Code