Diu 的 dp 题单

原本想写学习笔记的,但是 dp 题目太多,套路太深,我把握不住,所以我决定改成写题单。

本题单主要收录一些 dp 题目的神秘操作,当然也有一些套路比较常见,只是选一些做代表收录。


CF1626 A Random Code Problem

期望 dp,桶:开一个桶维护 \(f_{i,j}\) 表示操作 \(i\) 次之后共有多少个 \(j\),对于每一个 dp 值统计答案,设 \(L=\gcd(1,2,\dots,k-1)\),发现 \(L\lfloor\frac{a_i}{L}\rfloor\) 的部分始终不变,统一统计即可。

P3736 [HAOI2016]字符合并

区间 dp 和状压 dp:设 \(f_{i,j,s}\) 表示区间 \([i,j]\) 的串最后合并成串 \(s\) 的最大分数。发现因为每次合并的价值均为正数,所以能合并肯定合并,所以最后一个固定的区间 \([i,j]\) 会合并成长度为 \((j-i-1)\%(k-1)+1\) 的串,直接暴力 dp 即可。

P4007 小 Y 和恐怖的奴隶主

矩阵优化,卡常:发现 \(m\) 很小,但是 \(n\) 很大,考虑直接开 \(m\) 维维护血量。设 \(f_{i,a,b,c}\) 表示攻击 \(i\) 次,有 \(a\) 个奴隶主血量为 \(1\)\(b\) 个奴隶主血量为 \(2\)\(c\) 个奴隶主血量为 \(3\) 的期望打掉 boss 的血量,直接从 \(f_i\) 暴力构建转移矩阵到 \(f_{i+1}\),复杂度 \(O(T166^3\log n)\)。多组数据时,一个优化是把 \(2\) 的次幂的矩阵预处理出来,然后每次二进制拆分,用一个行矩阵(或列矩阵)乘上需要的,优化到 \(O(166^3+T166^2\log n)\)。还需卡常,在矩乘时用一个 __int128 做中间量,最后统一取模。

P4590 [TJOI2018]游园会

状压,dp 套 dp:字符串计数,考虑 \(f_{i,\dots}\) 表示前 \(i\) 个字符,满足什么状态的方案数。对于 \(NOI\) 的限制,我们可以考虑加一位 \(0/1/2\) 表示和 \(NOI\) 的匹配程度。对于最长公共子序列,我们可以考虑还原在 dp 求最长公共子序列的过程。如果把兑奖串看做已知,那么设 \(dp_{i,j}\) 表示第一个串前 \(i\) 个字符,第二个串的前 \(j\) 个字符匹配了多少个,那么 \(dp_{i,j}=\max(dp_{i,j-1},dp_{i-1,j},dp_{i-1,j-1}+[s_i=s_j])\)。发现 \(dp_i\) 的转移只和 \(dp_{i-1}\) 有关。我们可以考虑用一个状态 \(s\) 存储一个 \(dp_i\),然后每次转移暴力解压出来,更新完之后再压缩回去。但是这样做状态数很多,我们可以发现 \(dp_{i,j}-dp_{i,j-1}=0/1\),所以我们可以把 \(dp_i\) 差分起来,再状压。

CF1648C Tyler and Strings

计数 dp,数据结构优化:我们从小到大枚举每一位 \(i\) 表示 \(s\)\(t\)\(i\) 位相同的情况,更新桶,表示当前可用点。如果第 \(i+1\) 位使得 \(s\) 字典序更小,那么后面的可以乱填。那么假设 \(c_i\) 是桶,对于一个 \(j<t_{i+1}\),我们的方案数是 \(\sum\frac 1{c_j}\cdot\prod c_i!\),然而每次枚举一位只会改变一个 \(c_i\) 的值,所以可以用数据结构维护这个过程。具体地,可用线段树,也可用树状数组维护前缀和。

SOJ 4. Journey

期望树形 dp,容斥,卡常

SOJ 5. Festival

期望计数 dp

SOJ 6. Random

期望 dp,矩阵优化,cdq

以上三题做法太神仙,笔者太弱,无法很好总结,另见 solution

SOJ 1.博弈

博弈论,DAG 图上 dp,背包 dp:可以发现每个点有无棋子对答案贡献互相独立,可以用一次 dp 解决。可以先设 \(f_v\) 表示节点 \(u\) 可以使得当前先手领先多少个点,对于一个可以转移的 \(v\),如果 \(u,v\) 同色,那么 \(f_u\Leftarrow f_v+1\),否则 \(f_u\Leftarrow -f_v+1\)\(f_u\) 在所有状态中取 \(\max\),特殊地,持棋者可以一步不走,所以 \(f_u\) 也可以为 \(0\)。一遍 dp 更新完 \(f\) 之后,我们按颜色分开,即一部分是对 A 有正贡献,一部分是负贡献。那么对两者分别跑背包,然后统计即可。

P6803 [CEOI2020]星际迷航

博弈论,树形 dp,矩阵优化:模拟赛时奇妙 65 分,至今不知原因。

我们发现除了第一棵树,其他树上节点我们只需要知道以它为根节点的情况,可以考虑换根 dp。我们发现一个一个必胜点向后面接了一条边什么事都无,而一个必败点往后借一条边,如果连的是必胜也无用,否则会改变某一些点的 dp 值。我们可以考虑矩阵优化。最后再做一次树形 dp 统计答案即可,细节极多。

CF1500F Cupboards Jumps

构造,普通 dp,set 的神秘操作:我们发现题目和 \(h\) 的值无关,只和 \(h\) 的差分有关,所以我们考虑对差分 \(d_i\) 进行 dp,直接设 \(f_{i,x}\) 表示前 \(i\) 个数中 \(|d_i|=x\) 是否可行。然后我们可以发现 dp 数组的转移情况只有:以某个点为中心翻转,整个值域覆盖,删除或加入单点。我们发现单点的值不多,可以用 set 维护,对于区间翻转操作,可以用两个便捷维护,用两个值维护区间是否翻转,即区间便宜的值。每次翻转操作我们对整个区间进行翻转,这样单次复杂度 \(O(\log n)\)

P6773 [NOI2020] 命运

计数树形 dp,线段树合并维护:因为所给的链都是返祖链(两个端点有祖先-后代关系的链),所以我们可以设 \(f_{u,i}\) 表示子树 \(u\) 内,需要在深度为 \(i\) 的祖先下面之前选一个点的方案数。如果不需要再选择,那么 \(i=0\)。然后我们考虑转移,对于 \(u\) 的一个儿子 \(v\),我们可以考虑类似树上背包的转移,假设 \(dep_u\) 表示节点 \(u\) 的深度,其中根节点深度为 \(1\),那么:

\[f'_{u,i}=\sum_{j=0}^{dep_u}f_{u,i}\times f_{v,j}+\sum_{j=0}^if_{u,i}\times f_{v,j}+\sum_{j=0}^{i-1}f_{v,i}\times f_{u,j} \]

前面一个 \(\sum\) 是求当前点选择的情况,后面两个是求不选择的情况。我们发现这个东西维护一个前缀和就能够快速求出。我们可以考虑线段树合并,维护区间 dp 值的和。然后合并的时候可以维护两个全局变量表示前缀 \(f_u\) 的和和后缀 \(f_v\) 的和。总体比较好写。

类似题目推荐:P5298 [PKUWC2018]Minimax,期望树形 dp,同样是用线段树合并维护。

P6669 [清华集训2016] 组合数问题

数位 dp,组合数:其实就是 \(C_i^j\equiv 0(\bmod k)\) 的个数。然后用 Lucas 定理拆开:\(C_n^m\equiv \prod C_{a_i}^{b_i}\)。我们发现满足条件必须要这些组合数中必须要有某个 \(C_{a_i}^{b_i}=0\),因为每个 \(a_i,b_i<k\)。所以要有 \(a_i<b_i\)。我们发现问题可以转化成两个 \(p\) 进制数 \(i,j\),两个有上限,要求 \(i\ge j\),某一位 \(a_l<b_l\),然后跑数位 dp 就行了。

如果不会 Lucas 定理,可以看我的 数论家族学习笔记

P5468 [NOI2019] 回家路线

本题有 加强版,注意两者数据范围不同。

斜率优化,图上 dp:发现转移花里胡哨,我们如果按每个点进行 dp 的话,发现光转移的复杂度就是 \(O(n^2)\) 的,根本吃不消。我们可以考虑把航班设到状态,然后把时间(航班时间)和空间(出发点和终点)放到转移条件。然后我们把转移的式子拆开,发现可以斜率优化,现在开始考虑转移条件。对于每个航班,更新时从出发点更新,完了之后插入到终点,这样就处理好了空间限制。对于时间限制,我们可以用类似扫描线的思路,把每个航班拆成两个,按时间排序。第一次扫到就更新 dp 值,第二次插入到单调队列里。

P3244 [HNOI2015]落忆枫音

普通 dp,计数:对于一个 DAG 上的统计,我们有一个结论:假设点 \(u\) 的入度是 \(d_u\),那么方案数就是 \(\prod d_u\),具体就是每个点选择一个父亲。但是新加了一条边 \((s,t)\),可能会有环,我们要从上述答案中减去环的情况。我们假设这个环上的点为 \(a_1,a_2,\dots,a_k\),大小为 \(k\),那么我们住了这个环以外的点乱选,那么要减去的值为 \(\dfrac{\prod d_u}{\prod d_{a_i}}\)。我们发现这个东西可以 dp,我们假设 \(f_u\) 表示从 \(t\)\(u\) 的路径上,该式的值。容易得到 \(f_u=\dfrac{f_v}{d_u}((u,c)\in E)\),那么答案就是 \(\prod d_u-f_s\)

SP3734 PERIODNI - Periodni

SPOJ 太奇怪了,所以直接放 luogu 链接了。

笛卡尔树,树形 dp,组合计数:我们发现,如果按照笛卡尔树的思路分割序列,对应的两个子树内是不会互相影响的。建完笛卡尔树之后,我们考虑树上背包。设 \(f_{u,i}\) 表示子树 \(u\)\(i\) 个的方案数。然后随便用组合数统计一下就可以了。

SOJ 28.寄

树上 dp:我的理解是拆维。考虑一个高维 dp,设 \(f_{u,x,j}\) 表示子树 \(u\) 内,最近的关键点距离点 \(u\)\(x\),剩下 \(k\) 个点没匹配的最小贡献,这里需要统计 \(k\) 个没匹配的点到点 \(u\) 的距离。显然可以大力背包,因为 \(x\)\(j\) 都和子树的 size 有关,所以能过 \(O(n^4)\) 的。我们考虑拆维,设 \(f_{u,j}\) 表示子树 \(u\) 内,剩下 \(k\) 个点没匹配的最小贡献,\(g_{u,x}\) 表示子树 \(u\) 内所有点匹配,最近关键点距离点 \(u\)\(x\)。然后 \(f_u\) 正常更新,用 \(g_u\) 来更新 \(f_{u,0}\)\(g_u\) 的更新就相当于已经知道两棵子树的 \(g\),合并它们(两子树顶点有一条权值为 \(w\) 的边)。假设两子树根分别为 \(v_0,v_1\),那么我们可以先枚举关键点 \(j\)\(v_0\),再枚举 \(v_1\) 里面有多少个没匹配的点,让它们全部连向 \(j\),这一步要用到 \(g_{v_0}\)\(f_{v_1}\)。同理,反过来,当关键点在 \(v_1\) 时做相同处理。更新 \(g\) 的时间复杂度可以考虑每两个点会在它们的 lca 被统计,所以是 \(O(n^2)\),更新 \(f\) 的同理。总时间复杂度 \(O(n^2)\)

SOJ 30.润

ddp:先考虑朴素 dp,设 \(f_{l,r},g_{l,r}\) 表示区间 \([l,r]\) 有无进位的答案,那么如果 \(s_l=0\),那么 \(f_{l,r}=2f_{l+1,r}+work(l,r),g_{l,r}=f_{l+1,r}+g_{l+1,r}+work(l,r)\),否则 \(f_{l,r}=f_{l+1,r}+g_{l+1,r}+work(l,r),g_{l,r}=2g_{l+1,r}+work(l,r)\)。然后考虑 ddp,写成矩阵形式,从 \(r\) 往前转移。注意 \(work(k)\) 只和 \(k\) 的二进制位数有关,可以递推,但是要特判 \(k=2^p\) 的情况。所以查询时可以先找到从右往左的第一第二个 \(1\) 的位置,分别查询即可。细节非常多,不过大样例好评!