Loading

动态规划

动态规划(Dynamic Programming,以下简称 dp)是一种用若干子问题得到原问题解的算法,在算法竞赛中可以理解为递推的扩展。dp 并不是某种固定的算法,而是解决问题的一种思路,它贯彻了算法竞赛从入门到精通的整个过程。dp 问题,通常需要几个性质:最优子结构,重叠子问题,无后效性。

最优子结构性质:如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质。

无后效性:即子问题的解一旦确定,就不再改变,不受在这之后、包含它的更大的问题的求解决策影响。

子问题重叠性质:子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。dp 算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率,降低了时间复杂度。也就是记忆化的思路。

dp 的结构分为状态,转移方程和初值。其中转移方程一般有两种考虑角度:填表和刷表。前者是递归的角度,从过去推当前;后者是考虑贡献的角度,从当前推未来。通常用 \(t\text{D}/e\text{D}\) 来描述 dp,前者是状态数,后者是转移数。即 \(O(n^t)\) 个状态和 \(O(n^e)\) 个转移。

dp 主要有设计状态和优化两种考点。

dp 状态设计

dp 的本质是在 dag 上沿着拓扑序求最值路径或者计数,状态设计的关键就是找到最优子结构,且要满足无后效性。根据不同的模型,总结出了若干常见的状态设计。

例 I 最长上升子序列(LIS)

给定 \(n\) 个数 \(a_1,a_2,\dots a_n.\) 求出其一个子序列 \(a_{i_1},a_{i_2},\dots,a_{i_m}\) 满足 \(a_{i_1}<a_{i_2}<\dots<a_{i_m}\)\(m\) 最大。

考虑增量构造子序列,记 \(f_i\) 代表以 \(a_i\) 为结尾的最长上升子序列。考虑枚举下一个位置 \(j\) 满足 \(j>i,a_j>a_i\),这时就可以用 \(f_i+1\) 更新 \(f_j\)。容易发现这个做法复杂度是 \(O(n^2)\)。用树状数组优化可以做到 \(O(n\log n)\)

当然还可以使用刷表的做法,即从 \(j<i,a_j<a_i\)\(f_j+1\) 转移到 \(f_i\)。优化方式一样。以这个最简单的例子,我们看到了一般子序列问题状态设计方案。

例 II 最长公共子序列(LCS)

给定两个序列 \(a_1,a_2\dots a_n,b_1,b_2\dots b_m\),求两个序列的最长公共子序列。

显然一维的状态难以描述两个序列,考虑记 \(f_{i,j}\) 代表以 \(a_{1\sim i},b_{1\sim j}\) 的最长公共子序列。考虑转移,当 \(a_i=b_j\) 时,可以从 \(f_{i-1,j-1}+1\) 转移,否则考虑去掉 \(a_i\) 或者 \(b_j\)。容易发现复杂度为 \(O(nm)\)

本例还有一个基于 dp of dp 的 bitset 优化,以后会有介绍。

背包

背包问题本是最经典的 NP 问题之一,用 dp 的方法可以在 \(O(nV)\) 的复杂度求解。传统的背包问题属于组合优化问题,但在 oi 里将一般的加法卷积均看作背包问题。

  • 基本问题描述

例 III 01背包

\(n\) 个物品和一个容量为 \(W\) 的背包,每个物品有重量 \(w_{i}\) 和价值 \(v_{i}\) 两种属性,要求选若干物品放入背包使背包中物品的总价值最大且背包中物品的总重量不超过背包的容量。

将限制全部放进状态中,对于物品考虑的顺序并无差别,记 \(f_{i,j}\) 代表只考虑前 \(i\) 个物品,当前背包容量是 \(j\) 的最大价值。将第 \(i\) 个物品加入前 \(i-1\) 个物品的背包。及从 \(f_{i-1,j-w_i}+v_i\) 转移到 \(f_{i,j}\)。容易发现这是 \(2D/0D\) 的转移。

滚动数组是减少状态所用空间的一个妙招。比如上述 01 背包问题,\(f_{i}\) 只会从 \(i-1\) 转移,那么每次只保留两组 \(f\) 即可。另外对于上述特殊问题,如果从后往前枚举重量,只保留 \(f\) 一个数组也是可以的。

例 IV 完全背包

完全背包模型与 01 背包类似,与 01 背包的区别仅在于一个物品可以选取无限次,而非仅能选取一次。

稍微修改一下转移 \(f_{i,j}\) 不再从 \(f_{i-1,j-w_i}\) 转移而是直接从 \(f_{i,j-w_i}\) 转移。也更方便滚动数组。

例 V 多重背包

也是 01 背包的一个变式。与 01 背包的区别在于每种物品有 \(k_i\) 个,而非一个。

区别不大,如果把每种物品的每一个都当作一个物品,直接做 01 背包即可。复杂度即为 \(O(W\sum k_i)\)。当然这很浪费,对于每种物品假设选择了 \(r\) 个,容易想到可以将 \(r\) 用二进制唯一分解,这样子就相当于把 \(k_i\) 个物品分成若干组,分别有 \(1,2,4\dots\) 个,最后剩下的部分自成一组。复杂度 \(O(nW\log W)\)

多重背包还可以使用单调队列优化,鉴于本文的学习顺序,这里暂且不作介绍。

到目前未知,我们可以欣赏一些题目。

例 VI Luogu P3985

注意到极差不超过 \(3\),假设我们知道了选了多少物品,将全部物品的重量都减去 \(\min v\),最后再加上即可。于是这样子状态数变成了 \(O(n^3)\),可以通过这道题。

本题只有四种权值,当然可以用一些其他方法。每种权值按贡献排序,分别枚举选了多少个。直接枚举是 \(O(n^4)\) 的。考虑分治再合并可以做到 \(O(n^2polylog(n))\)。类似 Meet in the Middle 的思想。

例 VII 多重背包的判定性问题

可以做到 \(O(nV)\) 的复杂度。考虑对于每个重量设置一个计数器 \(cnt_i\) 代表 \(i\) 需要当前物品至少个。转移时如果当前重量不需要当前物品就能达到,那么 \(cnt_i=0\),否则判断 \(cnt_{i-w}\) 是否超出限制即可。

  • 分组背包

一种特殊的题型,每个物品还有一个分组,要求这个组里的物品只能选一个。

例 VIII HNOI2007 梦幻岛宝珠

给你 \(n\) 颗宝石,每颗宝石都有重量和价值。要你从这些宝石中选取一些宝石,保证总重量不超过 \(W\),且总价值最大,并输出最大的总价值。

对于 \(100\%\) 的数据,\(1\le n \le 100\)\(1\le W,w_i,v_i \le 2^{30}\)
保证每个 \(w_i\) 能写成 \(a \times 2^b\space (a,b \in \mathbb N)\) 的形式,\(a \leq 10\) , \(b \leq 30\),且答案不超过 \(2^{30}\)

解题的关键是 \(a\le 10\),按 \(b\) 分类,然后背包大小就变成了 \(O(na)\) 了。接下来考虑合并。每次合并相邻的组,然后底下的部分体积刚好是 \(m\) 的后几位。具体来讲 \(g_{i,j}\) 代表,背包体积是 \(j\times 2^i+m\& (2^i-1)\)

  • 有依赖的背包

这里将一种最基本的树形背包叫做有依赖的背包。即选择某个物品那么其祖先一定要选,即形成了树上连通块。这个时候一般有两种做法,一种是直接卷积合并,一种是插入法。前者更好写而后者的复杂度可能更优。理论上不能插入时就只能用前者。

对于 dfs 序的写法,按 dfs 序排序后如果选择这个点就推到 dfs 序下一个,如果不选那么子树内的点都不能选,直接跳过这个子树。

例 IX [JSOI2016] 最佳团体

这是一个很经典的 01 分数规划问题,二分答案,然后更新价值跑树形背包即可。

  • 可撤销背包

当这个背包的除法有定义时,比如说在做多项式乘法时,就是可逆的。可以从多项式角度理解,但是这样不好算。考虑从组合意义记忆,\(f'_{S}\) 即为原数减去一定包含这一项的值,而这个值即为 \(f'_{S-X}\)

例 X CF303E Random Ranking

有一场由 \(n(\le 80)\) 个参与者参加的竞赛。每个参与者都会得到一个特定的分数。如果我们对他们以前的表现做一些统计,我们可以或多或少地预测积分榜。

参与者的分数将在区间 \([l_i,r_i]\) 中均匀分布(分数可以是实数)。你能根据这些数据预测积分榜吗?换句话说,你应该告诉每个参与者他在积分榜上取得固定位置的可能性。

参赛者在积分榜上以增序排序。因此,得分最高的参与者应当是最后一名。

区间dp

当一个问题是区间子结构合并类型的,多半可以使用区间dp。一般状态是 \(dp_{i,j}\) 代表把下标 \([i,j]\) 这一段合并的方案数或代价。

例 XI NOI1995 石子合并

先不考虑 \(1\) 可以和 \(n\) 合并。

\(dp_{i,j}\) 表示把区间 \([i,j]\) 之间的石子合并在一起所需的最小代价。

显然这些石子一定通过两堆已经合并好的石子合并来的。我们枚举这个断点 \(k\),使得先合并 \([i, k]\)\([k+1, j]\) 再把这两堆石子合并。得到转移方程 \(dp_{i,j}=(min_{k\in [i, j)} dp_{i,k}+dp_{k+1,j})+\sum_{t=i}^{j}a_t\)。时间复杂度是\(O(n^3)\)的。

考虑最后一次合并,是可以从两边考虑的。那么我们不妨把序列倍长,然后取中间一段长为 \(n\) 的序列,做上面的做法,强制 \(i,i-1\) 或者 \(1,n\) 这两个点最后再合并。复杂度不变,可以通过此题。

这个技巧被称为断环成链。

例 XII IOI1998 Polygon

我们发现这是个环形结构,按照题目的提示,我们知道可以把换断成链来做。然后就是个非常显然的区间 dp,关键是如何转移。

仿照上一题,我们可以列出转移方程:\(dp_{i,j}=\max(dp_{i,k} * dp_{k+1,j})\),注意这里 \(*\) 不是乘的意思,是运算符的意思,其可以为 \(+\) 也可以为 \(\times\)。由于有乘法,所以需要再记录最小值。

例 XIII P2890 Cheapest Palindrome G

区间 dp 还可以用来解决类似的回文子串问题。设 \(f_{i,j}\) 代表把区间 \([i, j]\) 变成回文串的最少步数。转移只需对 \(i,j\) 分类讨论即可,时间复杂度为\(O(N^2)\)

例 XIV CSP-S2021 括号序列

\(f_{l,r}\) 代表区间 \([l,r]\) 可以形成多少合法串。根据题目条件得知合法串两边都必须是括号,且左边一定是左括号,右边一定是右括号。

第一种 \((S)\) 的情况,直接扫一遍 \(O(n)\)。然后考虑 \((A),(SA),(AS)\)。第一种从 \([l+1,r-1]\) 转移,剩下的扫一遍,从 \(f[i+1,r-1],f[l+1,i-1]\) ,其中 \(s[l+1,i],s[i,r-1]\)\(S\), 转移,复杂度 \(O(n)\)

最后一种考虑枚举断点,转移方程大概是 \(f[l,i]\times f[j,r]\)\(s[i+1,j-1]\)\(S\)。随便前后缀和优化一些也可以做到 \(O(n)\)。但是这样会算重,考虑 \((**)()*()\),会在每个断点处算一次,那么根据套路,直接让他在第一个断点被计算。考虑给 dp 加一维状态,\(f_{i,j,0/1}\) 分别代表能或不能使用最后一种转移,那么 \(f_{l,i,0}\times f_{j,r,1}\) 转移即可。

例 XV APIO2007 动物园

像这种环形问题,是前面选的会影响后面,那不妨枚举前面选方案,然后在这个方案的基础上 dp,到最后再判断是否合法即可。

例 XVI CF1198D Rectangle Painting 1

二维区间 dp,首先有个性质,对于一个长方形,要不直接覆盖全部,要不然中间肯定有一条缝。那么设 \(f_{a,b,c,d}\) 代表左上角为 \((a,c)\) 右下角为 \((b,d)\) 的子矩阵的答案。直接枚举那条缝在哪。\(f_{a,b,c,d}=\min(\max(b-a+1,d-c+1),\min f_{a,i,c,d}+f_{i+1,b,c,d},\min f_{a,b,c,i}+f_{a,b,i+1,d})\),复杂度 \(O(n^5)\),常数很小。

区间 dp 的应用前提是需要通过子状态合并来求出当前状态,如果一个区间的答案是可以直接计算的,就不用先考虑区间的 2D 的 dp。区间 dp 也可以用决策单调性优化,将在之后介绍。

例 XVII [THUSC2016] 成绩单

给定一个序列 \(w_1,w_2,\dots,w_n\),可以多次删除一段连续的区间直到全部删完,假设一个删除了 \(k\) 次,则花费是 \(ak+b\sum_{i=1}^{k}(max_i-min_i)^2\),其中 \(max_i,min_i\) 分别是第 \(i\) 次删除时的最大值和最小值,求最小花费。

\(1\le n\le 50,\le w_i\le 1000\).

如果要记录最大最小值的话,其实是 \(O(n)\) 的,所以考虑一个 \(4D\) 的状态 \(f_{l,r,mn,mx}\) 表示将 \([l,r]\) 这个区间删除若干,剩下的 \(mn,mx\) 然后最小花费。同时记一下 \(g_{l,r}\) 代表区间 \([l,r]\) 的答案。考虑转移,一种是直接往 \(l,r\) 的两边缩。否则从两边去掉一个前缀/后缀。复杂度 \(O(n^5)\),常数不大可以通过。

区间 dp 练习:P4766CF1572CP5851CF392E

树形dp

例 XIX [SDOI/SXOI2022] 小 N 的独立集

给定 \(n\) 个点的树的形态以及点的权值范围 \(k\),对于任意 \(i\in[1,kn]\),求有多少种权值分配方案,使得树的最大权独立集大小为 \(i\)

\(1\le n\le 1000,1\le k\le 5\).

其实应该算 dp of dp?但由于一维 dp 过于经典就算了。普通的最大独立集 dp 是 \(f_{u,0/1}\) 代表 \(u\) 强制不选或者不强制选,这两个值最多相差 \(k\)。把差值和 \(f_{u,0}\) 记录下来,状态数是 \(O(n^2k)\) 的。然后卷积即可,复杂度仍然可以用树形背包的复杂度。

  • 树上连通块 dp

朴素做法,\(dp_u\) 代表在 \(u\) 的子树中,选择一个包含 \(u\) 的连通块,满足某些条件的最优决策/方案数。直接合并的复杂度,由于两个点只会在 lca 处合并一次所以是 \(O(n^2)\) 的。有些题目可以使用闵可夫斯基和,插值等技巧做到 \(O(deg_u)\)

Luogu P8564 ρars/ey

  • 按 dfs 序转移

如果对于某种 dp,加入一个信息很高效但是合并信息并不高效,这时我们可以按照 dfs 序转移,每次只用加入一个点或者推导下一棵子树。

  • 换根 dp

P1 COCI2014-2015#1 Kamp

https://www.luogu.com.cn/problem/P6419

具体思路同UM的题解这里就不再赘述,主要是换根的部分,我维护一个“重儿子”,即UM题解中的\(len_u\)是从哪个\(v\)来的,这时在换根时若要将\(x\)换成\(y\),我们进行分类讨论:

  • \(y\)不是\(x\)的重儿子

这种情况在换完根后\(x\)的重儿子以及最长链不会改变,直接重新计算\(y\)即可。

  • \(y\)\(x\)的重儿子

这时\(x\)的重儿子一定会变,我们重新遍历所有与\(x\)相连的节点(不包括\(y\)),重新计算\(x\)的重儿子与最长链。之后再计算\(y\)

这样做的复杂度看起来是\(O(n^2)\)其实不然,因为每个点至多会有一个重儿子,所以每个点在换根的时候至多只会重新遍历一遍与其相连的边,而每条边只与两个点相连所以每条边至多会被遍历两遍,一棵树又只有\(n-1\)条边,所以总的时间复杂度为\(O(n)\)

P2 CF708C Centroids

其实这道题换根的时候不需要再维护一个次大值,提供一种维护重儿子的做法。

我们来考虑暴力怎么做?

枚举每个点,如果它的最大的子树都不大于 \(\frac{n}{2}\) 的话那它肯定可以(已经可以那断一条边再连上即可)。注意这里是不大于所以直接下取整对答案没有影响。

先统计一遍大于 \(\frac{n}{2}\) 的子树的个数,首先这个个数肯定不会大于 \(1\)(这不废话吗?)。然后对于那个大于 \(\frac{n}{2}\) 的儿子,它肯定是子树大小最大的儿子,这不就是重链剖分中的重儿子吗?所以我们每个点还可以记录一个重儿子。

下文删掉的意思是将其连向父亲的边删掉然后连向根节点。

如果一个点的重儿子将其重儿子的子树删掉后还没有小于等于 \(\frac{n}{2}\) 那么显然这个点就不行。

那如果满足情况呢?我们发现如果其本身大小也大于 \(\frac{n}{2}\) 的话也不行。那如果删其他儿子的话一定也会大于 \(\frac{n}{2}\) 了(因为重儿子已经大于 \(\frac{n}{2}\)),所以一定还得删重儿子的儿子,同理最优还是删重儿子的重儿子(有点绕...),而如果不行则删其他儿子也不行。

故我们需要在重链上找一个子树大小小于等于 \(\frac{n}{2}\) 的点,而这个点必然越浅越好(子树大小越大越好)。这样我们将重链放在一个set中维护然后找到第一个小于等于 \(\frac{n}{2}\) 的点,判断减去它能否小于 \(\frac{n}{2}\) 即可。

然后各种DS套上去就行了,但是我们不想写DS怎么办(其实是我不会),其实就是求一个子树内子树大小小于等于 \(\frac{n}{2}\) 且最大的点,这不是dp就好了吗?(这也是为什么这题难度是蓝的原因)

\(dp_x\)\(x\) 子树内子树大小小于等于 \(\frac{n}{2}\) 且最大的点的子树大小(不包括 \(x\)),则根据上面的推导有 \(dp_x=max(dp_{son_x}, size_{son_x}(size_{son_x} \le \frac{n}{2}))\)。其中 \(son_x\) 代表 \(x\) 的重儿子。

然后换根求dp值,按我上面的做法判断即可。

具体怎么换根呢?

如果我们现在想要把根从 \(x\) 换成 \(y\),可以分成两类讨论。

首先不管哪一类都要将 \(x\)\(y\)\(size\) 改变,具体实现就是换根dp的基础了吧。

第一种情况:\(y\) 不是 \(x\) 的重儿子。那这种情况 \(x\) 的重儿子不会改变,只要判断 \(y\) 的重儿子有没有改成 \(x\),然后更新 \(y\) 的重儿子与dp值。

第二种情况:\(y\)\(x\) 的重儿子。此时 \(x\) 的重儿子会改变(变成除了 \(y\) 以外与 \(x\) 相连的一个点),那么只要遍历一遍与 \(x\) 相邻的点重新求一下 \(x\) 的重心并更新dp值。重复情况一 \(y\) 的更新。

时间复杂度:\(O(n+m)\),因为只有一个重儿子所以遍历也只会遍历一遍,又每个边也只会被遍历两遍所以总的时间复杂度是 \(O(m)\) 级别的。

关于重心本题的另一种解法

实际上,树形 dp 还经常会结合数据结构来优化,具体参考 dp 的数据结构优化。

数位dp

  • 直接维护

直接记状态 \(f_{i,j}\) 代表 \(i\) 位数最高位是 \(j\) 的方案数,这种状态一般用于相邻若干位有要求的转移。这种 dp 的转移,我们一般会采用填表法,也即枚举前一位的填法,来贡献到这一位,因为这样是好记忆化搜索的。

Windy 数

先拆成 \(\le R\) 的范围内有多少 windy 数,然后设状态 \(f_{i,j}\) 代表长度为 \(i\) 最高位是 \(j\) 的有多少 windy 数,转移从 \(f_{i-1,k}\) 转移,\(|j-k|\le 2\)

  • 维护进位

\(f_{i,j}\) 代表 \(i\) 位数,向下一位进了 \(j\)。这种状态一般用于几个(很少)数的和是一个大数。而写法上,也通常采用刷表法。事实上,刷表法也是可以记忆化的,具体可以看下面的例题。

CF1290F

这里暂且跳过前面的分析,直接推出结论:

\[\sum_{x_i>0}x_ic_i=-\sum_{x_i<0}x_ic_i\\ \sum_{x_i>0}x_ic_i\le m \]

\(y\) 同理。注意到 \(n\)\(x\) 均很小,考虑将 \(m,c_i\) 写成二进制逐位确定 \(c_i\) 的每一位。注意到这里要求和 \(\le m\),且 \(\sum=-\sum\),所以需要考虑进位。对于有进位的数位 dp,一般只能从下往上枚举。

类似的,之前是枚举 lcp,这里我们枚举 lcs。具体来讲,对于此题,维护 dp 数组 \(f(d,px,py,nx,ny,limx,limy)\)。表示已经确定了 \(0\sim d-1\) 位,现在要确定第 \(d\) 位,这一位上已经有 \(px,py,nx,ny\) 的进位,分别代表正数和负数的 \(x,y\)。然后对于 \(\sum_{x_i>0}c_ix_i\),维护 \(0\sim d-1\) 这些位构成的数是否 \(\le m\)。记作 \(limx\)\(limy\) 同理。这样转移枚举这一位,然后算出进位和这一位最后的值,根据这个值判断是否 \(\le m\) 即可。

[SDOI/SXOI2022] 进制转换

对于 \(i\),记二进制和三进制的数位和分别为 \(a(i)\)\(b(i)\)。求 \(\sum\limits_{i = 1}^n x^i y^{a(i)} z^{b(i)}\)\(998244353\) 取模。

\(1\le n\le 10^{13}.\)

考虑一个暴力的 dp,从高到底枚举三进制的状态,同时存二进制的状态。对于三进制数位和的贡献以及十进制的贡献直接计算,而二进制数码可以在最后计算。这样复杂度其实就是二进制状态数也就是 \(O(n)\)。考虑减少无用状态,在枚举到三进制后面的部分的时候并不会影响二进制前面的部分,所以这一部分贡献可以提前计算。而状态只需要记到能有影响的部分,以及会不会进位即可。考虑这个进位,其实可以干掉了很多 \(1\),但是考虑到我们只统计 \(1\) 的数量,所以并不是很有所谓。

然而这样子复杂度还是不对,直接 meet in the middle 即可。复杂度 \(O(\sqrt{n}polylog(n))\)

类似题目 CF582D,。

  • 上下界的枚举

一种很好写的做法是记忆化搜索,那这时再无脑多记录一个有没有顶到上界就好了。否则一般的数位 dp 还需要我们枚举 LCP。

给定区间 \([l,r]\),求 \(\max_{l\le u,v\le r}u\operatorname{xor} v\)

异或最大显然考虑从高到低位贪心,一个很暴力的想法是把所有 \(l,r\) 之间的数建 trie 树,但是这题显然不行,但其实如果一个 trie 是满的我们,就比用建出来了,这样复杂度就对了。其实直接枚举较小的数有没有卡下界,较大的数有没有卡上界即可。

总结来讲,对于没有进位的数位 dp,我们通常采用预处理一个 dp 数组,代表某个长度的数的答案,然后枚举 lcp。对于有进位的数位 dp,只能使用更大常数的记忆化搜索。

那其实所有数位 dp 都是从低位不断填到高位的,

状态压缩dp

建议在初学的时候就学习广义的状态压缩 dp。除了常见的 \(0/1\) 状态,也可以是别的什么压缩。状压的本质就是映射,将一个集合映射成一个正数,然后进行转移(有点类似自动机的样子)。位运算状压是最常见的。

  • 二进制压缩

也就是出现:\(0\),没出现:\(1\)。将一个集合压缩成一个实数,通过位运算操作可以很快的判断:是否相邻等条件。通常这样的题目有一个方面是一个很小的数。

[NOI2001] 炮兵阵地

维护相邻两行是否有炮兵的方案,这个是斐波那契数列,所以很快。

Corn Fields G

给定 \(n\)\(m\) 列的方格矩形,求有多少种方案选择若干不相邻的方格

一个暴力,记录每一行的状态,然后枚举下一行的状态,同上这是斐波那契数,应该是 \(O(nF_m^2)\)。考虑轮廓线 dp 枚举插头的位置,然后记录这一行前面的以及上一行后面,可以做到 \(O(nm2^m)\) 或者更低的复杂度。

[USACO12MAR] Cows in a Skyscraper G

\(O(3^n)\) 勉强能过,但是我们思考其实只需要记一个集合最小分成多少时最后一个部分还剩多少即可。因为一旦多分了一个组,肯定会有更多的空间。这样就可以 \(O(2^nn)\)

CF327E Axis Walking

\(O(2^{n-1}n)\) 的做法不再赘述。注意到 \(m\le 2\),考虑容斥。容斥后转化为 \(=x\) 的集合个数,\(m=1\) 的情况直接 meet in the middle。否则应该转化为一个高维前缀和的问题。

!!!!!

[NOIP2017 提高组] 宝藏

有点类似斯坦纳树了。同时记录树的高度,然后 bfs 扩展即可。不过这里要预处理扩展,仔细分析复杂度应该是 \(O(3^nn+2^nn^2)\)

[SCOI2008] 奖励关

结合期望 dp 的知识,期望+最优化是一类很讨厌的模型。因为这时往往要注意 dp 的顺序和后效性。考虑从后往前 dp,转移就可以取 \(\max\) 了。

https://codeforces.com/problemset/problem/1149/D

[NOI2015] 寿司晚宴

  • 轮廓线 dp

考虑一类棋盘 dp,枚举一个插头 \((i,j)\) 而我们只关心 \(i\) 这一行在 \(j\) 前的和 \(i-1\) 这一行 \(j\) 之后,我们就可以插头 dp。

\(n\) 个球,编号 \(1\sim n\) 排成一行,每个球可以染成 \(w_i\) 种颜色也可以不染色。第 \(i\) 个球和 \(i+1\) 个球不能同时染色,第 \(i\) 个球和第 \(i+k\) 个球也不能同时染色。\(1\le n\le 300\)

\(k\) 很小的时候,直接转移状压最后 \(k\) 个数是什么,复杂度是 \(O(n2^k)\)

\(k\) 比较大的时候,可以画成 \(k\times n/k\) 的网格图,最后一行和下一列的第一行有连边。这样只用枚举第一行之后就可以逐行插头,复杂度 \(n4^{n/k}\)

平衡一下,并且发现状态数是斐波那契,就可以通过此题。

  • 插头 dp

  • 高维状压

状压不一定要二进制,还可以三进制,甚至是枚举划分数什么的。

一些经典的结论:

一个集合的划分数,贝尔数:\(1,1,2,5,15,52,203,877,4140,21147,115975\dots\)

整数 \(n\) 的划分数:\(1,1,2,3,5,7,11,15,22, 30, 42, 56, 77, 101, 135, 176, 231, 297, 385, 490, 627, 792, 1002, 1255, 1575, 1958, 2436, 3010, 3718, 4565, 5604, 6842, 8349, 10143, 12310, 14883, 17977, 21637, 26015, 31185, 37338, 44583, 53174, 63261, 75175, 89134, 105558,\dotsb\)

现代状压题一般的考察点就两个:

  1. 优化转移。一般方法是 fwt/fmt 之类的或者一些状态设计的技巧。
  2. 发现状态很小,直接暴力。这个比较诈骗...

[九省联考 2018] 一双木棋 chess

最小斯坦纳树,给你一个图,以及一个点集合 \(S\)

找到一个连通子图 \(G\),使得其包含所有 \(S\) 中的点且边权和最小。

显然最后这个图 \(G\) 是一棵树,考虑枚举一个根,然后合并两棵根相同的无交子集。这样复杂度是 \(O(3^n)\) 的(枚举子集的经典结论)。然后还可以再在根节点上接上一个新的根节点,在不改变 \(s\) 的情况下,这一部分的复杂度是最短路的复杂度。

自动机上dp

这里指比较明显的 dfa,不考虑 dp of dp。

HNOI2008 GT考试

像这种字符串匹配有关的 dp 一般都有一维是自动机上的状态(套路)。因为要构建长度为 \(n\) 的准考证号所以还有一维长度。这样我们就知道了子状态:\(dp_{i,j}\) 代表长度为 \(i\) 的准考证号与不吉利数字匹配长度为 \(j\) 的方案数。

很容易想到转移方程:\(dp_{i,j}\gets \sum_{k=0}^{m-1}dp_{i-1,k} \times g_{k,j}\)。其中 \(g_{k,j}\) 代表加一个字符从状态 \(k\) 到状态 \(j\) 的方案数。

先来看怎么求这个 \(g\),这就要用到 kmp 自动机。枚举第一个状态 \(i\),然后再枚举在后面加的字符 \(c\),如果 kmp 自动机已经建出来了则可以知道加一个字符可以从 \(i\)\(j\),令 \(g_{i,j}\gets g_{i,j}+1\)

如果暴力去跑刚才的dp的复杂度是\(O(nm^2)\)的,肯定会炸。我们发现每次转移都只和 \(i-1\)\(g\) 有关,而 \(g\) 是不变的,所以可以用矩阵加速转移。

计数类dp

对于非最优化类的 dp 在上面的部分也都看到过很多次,其实这一类内容可能放在组合计数中差不多,但是考虑到基础的内容,所以这里稍做介绍。

  • 容斥原理

很多容斥的过程可以用 dp 来表示。

P1 CF559C Gerald and Giant Chess

https://www.luogu.com.cn/problem/CF559C

直接 dp 的复杂度是 \(O(hw)\) 的显然不行,发现黑格的数量很少,我们尝试从反面入手。首先所有的情况是 \(C_{h+w-2}^{h-1}\)。就是在总共要走的 \(h+w-2\) 步中选择 \(h-1\) 向下的方案数。然后减去至少经过一个黑点的方案数。

首先我们要找到一个基准点,可以是第一次经过的黑点。这样我们不关心后面怎么走,和前面的公式一样。现在我们考虑怎么不经过其他黑点走到当前黑点,我们发现这其实就是个子问题,用它前面的黑点更新就好了。实现的时候需要排序,保证前面的已经算出答案。

  • 无标号计数

这里的无标号是广义的概念,即每个组合对象是相同的。这是一个非常好的性质,也就是说组合对象集合可能只需要记录大小进状态。

Code Festival Final 2016 Road of the King

有一个 \(n(\le 300)\) 个点的图,目前一条边都没有。有一个人在 \(1\) 号点要进行 \(m\) 次移动,终点不必是 \(1\) 号点,假设第 \(i\) 次从 \(u\) 移动到 \(v\),那么在 \(u\)\(v\) 之间连一条有向边。

问有多少种序列能满足:最终 \(n\) 个点组成的图是一个强连通图。答案对 \(10^9+7\) 取模。

题解

概率期望类 dp

  • 倒序定义

思考期望 dp 记录的东西,理应是概率乘以值,如果单纯的写从前往后 dp 写成 \(\frac{f_{i}+1}{deg_i}\to f_{j}\) 那么记录的概率就不再对了。真正的概率应该用 dp 计算。然后倒退就没有这种担忧,所以倒退的思考量少了很多。

很多一维的题目正推和倒推本质是一样的,所以很多初学者可能会误解。

非常规状态概览

dp 的学习过程只能是:做题->不会->看题解->再做题,很多 dp 不能用套路来解释。这里我只给出一些例子,更多内容可以参考我的 dp 选题:动态规划题目总结

dp 的优化

优化思路

  • 减少状态数
  • 优化转移,前缀和/数据结构
  • 滚动数组
  • 决策单调性/斜率优化

矩阵加速

给你一张图,你刚开始在1号节点,每次你可以走到相邻的节点,每条边有一个边权,代表需要花费的时间。问有多少种方案刚好t时间走到n号节点。答案对2009取模。

对于 \(30\%\) 的数据,保证 \(n \leq 5\)\(t \leq 30\)

对于 \(100\%\) 的数据,保证 \(2 \leq n \leq 10\)\(1 \leq t \leq 10^9\)

先想一想\(30\%\)的数据怎么做?看到计数问题多半是组合数学或dp。而这题组合数学应该不太好做所以我们考虑dp。

设经过\(i\)步到达节点\(u\)的方案数为\(dp[i][u]\)。那所有连向u的边都可以被用来扩展,所以\(dp[i][u]=\sum_{(v,u) \in E} dp[i-val(v,u)][v]\)\(val(v,u)\)是边权。

这样做应该是\(O(n^2t)\)的(菊花图)。

怎么优化呢?如果边权只有可能是1的话,就有\(dp[i][u]=\sum_{(v,u) \in E} dp[i-1][v]\)。i的dp方程只与i-1有关,所以可以矩阵快速幂优化。

注意到边权只有可能是1-9,那么我们也可以用一个稍微大一点的矩阵来存。

具体来讲假设我们现在要求的是m时刻。

那我们的答案向量可以是\(\left[ \begin{matrix} dp[m-1][1] & dp[m-1][2] & ... & dp[m-1][n] & ... & dp[m-9][1] & ... & dp[m-9][n] \end{matrix} \right]\)

如果存在一条\(v->u\)的边且边权为\(val(v,u)\)。则令转移矩阵的第\(n \times (val(v,u)-1)+v\)行第\(u\)列设为1即可。

而为了保存m-1到m-8作为下一步转移的需要,我们还要做一步预处理。

数据结构优化

我的洛谷博客

单调队列优化

P6326 Shopping

[SDOI2017] 苹果树

斜率优化

感谢cxl的博客,讲得很清楚:https://www.cnblogs.com/Xing-Ling/p/11210179.html

斜率优化的特点在于可以在权值 \(w(i,j)\) 中可以找到一项 \(f(i)\times g(j)\) 的项,其中 \(j\) 是决策点,这时我们不能用单独的单调队列来优化因为这里没有单调性。

决策单调性的定义:对于形如 \(f[i]=\min _{0 \leq j < i}(f[j]+w(j,i))\) 的状态转移方程,记 \(p[i]\)\(f[i]\) 取到最小值时 \(j\) 的值,\(p[i]\) 即为 \(f[i]\) 的最优决策。如果 \(p[i]\)\([1,n]\) 上单调不减,则称 \(f\) 具有决策单调性。斜率优化的 \(w\) 函数都包含一项 \(g_i \times q_j\),通常以 \(g_i\) 作为斜率。

一般的斜率优化——凸包+单调队列

引入:[ZJOI2007]仓库建设

显而易见的dp,很显然又是一个区间划分问题。

容易推出 \(O(n^2)\) 的dp方程:令 \(g_i=-\sum_{j=1}^{i}a_j\times p_j\)\(P_i=\sum_{j=1}^{i}p_j\),有 \(f_i=\min_{j=0}^{i-1}\{a_i\times P_{i-1} + c_i + g_{i-1} + f_j - g_j - a_i \times P_{j}\}\),其中 \(f_i\) 代表从 \(1\)\(i\)\(i\) 处结束的最小费用,显然答案是 \(f_n\)。如果没有看懂dp方程也没关系,反正是学斜率优化的套路,先假装自己已经知道了这个方程就好。但是这题 \(1\le n\le 10^6\) 这种方法无法通过此题。考虑用单调队列优化dp的套路,把 \(i\)\(j\) 区分开来(其实我已经帮你们区分好了),因为有关 \(i\) 的都可以看作常数所以我们可以不管它,这样dp方程化成了 \(f_j-g_j-a_i\times P_{j}\),如果直接用单调队列维护是没有单调性了,因为对于不同的 \(i\) 有不同的 \(a_i\),这就是斜率优化的标志。

这我们用线性规划的角度来解决这个问题,将其表达为坐标系上的直线和点,其中 \(f_i\) 是截距,\(a_i\) 是斜率,\((P_{j},f_j-g_j)\) 是决策点,显然对于一个斜率和一个决策点即可确定这条直线,而我们现在要做的就是最小化截距。显然我们的最优决策点一定是在下凸包上的(如果要求最大值则在上凸包),且这个凸壳与直线的切点即为最优决策点,这里借用cxl的一张图:

当然这还有代数法的严格证明。

考虑怎么高效实现。这道题的决策点横坐标是单调递增的,所以我们可以用Andrew法维护下(上)凸壳,这就非常简单了(不会的同学可以上百度查一下),具体实现(这里偷懒用了斜率而不是叉积,且用了单调队列来维护):

while (head < tail && slope(q[tail - 1], q[tail]) > slope(q[tail], i)) tail--;
q[++tail] = i;

那么如何找到切点?即为在凸包上两点斜率第一个比直线斜率大的点。如上图的 \(\text{E}\)。因为凸包的斜率是单调的,而这题直线的斜率也是单调的所以如果前面的决策点对直线不行的话就可以直接舍掉了。

f[i] = a[i] * p[i - 1] + c[i] + g[i - 1];
while (head < tail && slope(q[head], q[head + 1]) < (double)a[i]) head++;
f[i] = f[i] + f[q[head]] - g[q[head]] - a[i] * p[q[head]];

显然每个决策点只会进队出队各一次所以时间复杂度为 \(O(n)\)

习题

[HNOI2008]玩具装箱

[APIO2010]特别行动队

[APIO2014]序列分割

Cats Transport

[CEOI2004]锯木厂选址

[SDOI2016]征途

斜率不单调的情况:凸包+二分

[SDOI2012]任务安排

看一道这样的题,在我们列出套路的式子后发现这个斜率并不单调,也就是说这个题不具有决策点调性,也就是说我们无法用单调队列来维护。但是这个凸壳的斜率还是有单调性的,所以我们可以考虑在凸壳上二分。

int l = head, r = tail - 1, ans = tail;
while (l <= r) {
	int mid = (l + r) >> 1;
	if (slope(q[mid], q[mid + 1]) > t[i]) {
		ans = mid;
		r = mid - 1;
	} else {
		l = mid + 1;
	}
}

时间复杂度为 \(O(n\log{n})\)

一些更高级的斜率优化技巧

【算法】斜率优化进阶

luogu

解析

根据贪心容易想到每天要不全部买入要不然全部卖出(因为如果可以赚那为什么不多赚一点)

那么就可以得到一个转移方程:\(f[i]\) 代表到第 \(i\) 天把所有金券都卖掉可以得到的钱最多为多少。

考虑转移时可以从任意一天买进在今天卖出,可以得到下面的方程:\(f[i]=\max_{0<j<i}\frac{f_j\times a_i\times r_j+f_j\times b_i}{a_j\times r_j+b_j}\)

今天还可以不卖也不卖,\(f[i]=f[i-1]\)

故我们可以写出一个 \(O(n^2)\) 的代码:

f[0]=s;
for (int i = 2; i <= n; i++) {
  for (int j = 1; j < i; j++) {
    f[i] = max(f[i], (f[j]*a[i]*rate[j]+f[j]*b[i])/(a[j]*rate[j]+b[j])
  }
  f[i]=max(f[i],f[i-1]);
}

考虑优化掉它,因为 \(val(i,j)\)\(i\)\(j\) 的乘法项所以不能用单调队列优化,得斜率优化。可以先提出来一个 \(g_j=\frac{f_j}{a_j\times r_j+b_j}\)

先化成斜率式:\(\frac{f_i}{a_i}-\frac{b_i}{a_i}g_j=g_jr_j\),然后我们发现无论是斜率还是决策点都没有单调性,所以用平衡树/cdq来实现。

我是用cdq来写的,不会的看这里

#include <bits/stdc++.h>
using namespace std;
const double inf = 1e9;
const int MAXN = 100005;
struct node{
	double k, g;
	int id;
	friend bool operator < (node a, node b) {
		return a.k > b.k;
	}
}p[MAXN], tmp[MAXN];
int n;
double s, f[MAXN], a[MAXN], b[MAXN], rate[MAXN];
int q[MAXN], head, tail;
double X(int i) { return p[i].g; }
double Y(int i) { return p[i].g * rate[p[i].id]; };
double slope(int i, int j) { return X(i) == X(j) ? (Y(i) > Y(j) ? inf : -inf) : (Y(i) - Y(j)) / (X(i) - X(j)); }
bool cmp(int aa, int bb) {
	return X(aa) != X(bb) ? X(aa) < X(bb) : Y(aa) < Y(bb); 
}
void cdq(int l, int r) {
	if (l == r) {//递归到最下曾时,dp值已经算出,那就可以算这个点的 X,Y 值了 
		f[p[l].id] = max(f[p[l].id], f[p[l].id - 1]);
		p[l].g = f[p[l].id] / (a[p[l].id] * rate[p[l].id] + b[p[l].id]);
		return;
	}
	int mid = (l + r) >> 1, h1 = l, h2 = mid + 1;
	for (int i = l; i <= r; i++) p[i].id <= mid ? tmp[h1++] = p[i] : tmp[h2++] = p[i];
	for (int i = l; i <= r; i++) p[i] = tmp[i];
	//根据下标分成两个部分使得前半部分的下标都是小于后半部分的下标 
	//此时[l,r]区间内的斜率可能不单调,但是[l,mid]和[mid+1,r]的斜率是单调的 
	cdq(l, mid);//递归处理前半部分 
	//归并排序后保证决策点的横坐标是单调的 
	//把前半部分的点建成凸包 
	head = 1, tail = 0;
	for (int i = l; i <= mid; i++) {
		while (head < tail && slope(q[tail - 1], q[tail]) <= slope(q[tail], i)) tail--;
		q[++tail] = i;
	}
	for (int i = mid + 1; i <= r; i++) {
		while (head < tail && slope(q[head], q[head + 1]) >= p[i].k) head++;
		f[p[i].id] = max(f[p[i].id], (-p[i].k + rate[p[q[head]].id]) * p[q[head]].g * a[p[i].id]);
	}
	cdq(mid + 1, r);
	h1 = l, h2 = mid + 1;
	int top = 0;
	while (h1 <= mid && h2 <= r) {
		if (cmp(h1, h2)) tmp[++top] = p[h1++];
		else tmp[++top] = p[h2++];
	}
	while (h1 <= mid) tmp[++top] = p[h1++];
	while (h2 <= r) tmp[++top] = p[h2++];
	for (int i = 1; i <= top; i++) p[l + i - 1] = tmp[i];
}
int main() {
	scanf("%d%lf", &n, &f[0]);
	for (int i = 1; i <= n; i++) {
		scanf("%lf%lf%lf", &a[i], &b[i], &rate[i]);
		p[i].k = -b[i] / a[i];
		p[i].id = i;
	}
	//先按照斜率排序 
	sort(p + 1, p + 1 + n);
	cdq(1, n);
	printf("%.3lf", f[n]);
	return 0;
}

写代码时需要注意的细节

  1. 两个点横坐标相同时如果 \(Y(j)\ge Y(i)\) 则返回正无穷,否则返回负无穷是必要的。

  2. 要注意斜率和横坐标是否有单调性

  3. 维护单调队列时如果等于最好也出队

  4. 要保证队列中有至少一个元素(如果没有切点的话这个点也是最有决策点)

决策单调性

将 dp 抽象一下,给定一个向量 \(f\) 和一个矩阵 \(A\),考虑求出一个向量 \(g_i=\min_j(f_j+a_{i,j})\)

如果一个矩阵 \(A\) 的第 \(i\) 行的最小值的位置 \(b_i\) 是单调不减的就是单调矩阵。如果一个矩阵所有的子矩阵都是单调矩阵就称 \(A\) 是完全单调矩阵。

四边形不等式 \(i\le j,x\le y\)\(A_{i,x}+A_{j,y}\le A_{i,y}+A_{j,x}\)。如果 \(j=i+1,y=x+1\) 满足四边形不等式,则 \(A\) 满足四变形不等式。满足四边形不等式的矩阵是蒙日矩阵。蒙日矩阵每一行或每一列加上一个常数 \(C\) 单调性不变。

由于蒙日矩阵一定是完全单调矩阵,所以dp方程有决策单调性

一般做法分治,假设当前分治区间是 \([l,r]\) 答案区间是 \([x,y]\) 考虑求出第 \(mid=\frac{l+r}{2}\) 行的最小值的位置 \(p\),那么 \([l,mid-1]\) 的答案只能在 \([x,p]\) 取到,同理 \([mid+1,r]\) 的答案只能在 \([p,y]\) 取到,递归即可。

一般做法二分栈,对于任意两列,左边的列在一段前缀最优,右边的列在一段后缀更优。所以我们可以求出当前列第一个不如下一列的位置,这个二分求出。

NOI2009 诗人小G

直接列出转移方程 \(dp_i=length(j,i)^p+dp_j\),这个幂函数显然是凸的于是它有决策单调性于是直接二分栈即可。

loj6039 雅礼集训2017 Day5 珠宝

\(C\) 相同的物品分成一组,肯定是从大到小选,记方程 \(f_{i,j}\) 代表前 \(i\) 组选则总体积为 \(j\) 个的最大收益,转移可以写成 \(f_{i,j}=\max f_{i-1,k}+s_{i,\frac{j-k}{i}}\)。其中 \(s_{i,{j-k}{i}}\) 是前缀和。

那这个 \(s_{i,\frac{j-k}{i}}\) 是单调的所以显然有决策单调性,按照模 \(i\) 个余数分类,然后直接分治即可。

CTT2018 ZYB的游览计划

分治做法的优势,在于如果矩阵元素不能 \(O(1)\) 计算的时候,如果可以快速扩展,用分治法类似莫队算法加入删除复杂度不变。而这一题只需要维护集合内的点的dfs序的顺序即可。

CF868F

和上一题近乎一样的做法

凸优化

八省联考2018 林克卡特树

我们先来转换一下题意,即为选 \(k+1\) 条互不相交的链,使得权值和最大。估计没人和我一样选 \(k\) 条小于 \(0\) 的边变成 \(0\)

这个东西看起来就只能 dp 求,设 f[i,j,0/1/2] 代表以 i 为根的子树选出 j 条链,然后 i 不选,i 是一条链的顶,i 的子树中有一条穿过 i 的链。然后转移就比较显然了,为了方便,我们最后合并信息到 \(f[i,j,0]\) 上。通过打表我们发现这是一个关于 j 的凸函数,所以可以 wqs 二分。根据数据范围也可以猜测。我们稍微改写一下 dp 方程即可。

然后我的写法又丑又难写还难调,于是我打开了题解,看到了下面这种写法,稍微学习了一下(就是用一个 pair 来存,然后重载运算符)。

dp 套 dp

将内层 dp 的答案作为外层 dp 的状态,一般用在对内层 dp 答案的情况计数。一般考察点在于内层本质不同状态很少!

[AGC022E] Median Replace

有个奇数长度的 \(01\)\(s\) 其中有若干位置是 ?。每次可将 \(3\) 个连续的字符替换成这三个数的中位数。求有多少方案将 ? 替换成 \(0/1\) 使得进行 \(\frac{N-1}{2}\) 次操作后的字符串是 \(1\)

参考

https://oiwiki.org/dp/

https://www.luogu.com.cn/blog/sysblogs/dp-tricks

https://www.luogu.com/article/ki71nw88

posted @ 2023-01-20 11:15  Semsue  阅读(85)  评论(0编辑  收藏  举报
Title