基础 dp 二十六题
题有点多,例行的简要题意环节就无了。这些题都是各个类型的 \(\rm dp\) 里最基础的,就当做再过一遍知识点吧。
最基础的一维 \(\rm dp\)。考虑设 \(f_i\) 表示走到 \(i\) 所需要的最少代价,则走到 \(i\) 可以由 \(i-1,i-2\) 转移而来:
边界条件是走到 \(1\) 不需要代价:
答案即为 \(f_n\)。时间复杂度 \(\mathcal{O}(n)\)。评测记录。
是上题的扩展,\(i\) 可以从 \(i-k,i-k+1,\cdots,i-1\) 转移过来:
边界条件和答案不变,时间复杂度 \(\mathcal{O}(nk)\)。评测记录
状态设计变得复杂了一点。注意到每次选择具有后效性,即会影响后一天的选择范围。但对于这种后效性,我们可以通过把选择的内容加入状态来避免。具体来讲,考虑设 \(f_{i,0/1/2}\) 分别表示第 \(i\) 天选活动 A/B/C 的最大幸福度。则有转移:
其中 \(A_{i,0}=a_i,A_{i,1}=b_i,A_{i,2}=c_i\)。边界条件是 \(f_{0,0/1/2}=0\),答案为:
时间复杂度 \(\mathcal{O}(n)\)。评测记录。
01 背包问题,不详细讲推导过程。考虑设 \(f_{i,j}\) 表示前 \(i\) 个物品,选出重量为 \(j\) 的最大价值,则有:
注意重复转移的问题,可以通过倒序枚举 \(j\) 来避免。答案即为 \(\max\{f_{n,0\sim W}\}\)。注意到空间复杂度可以用滚动数组优化为 \(\mathcal{O}(W)\)。进一步观察发现,可以干脆去掉第一维也不影响答案。时间复杂度 \(\mathcal{O}(nW)\)。评测记录。
注意到此题和上一题题面都差不多,只不过我们不再能在状态里记录重量了,因为它达到了 \(10^9\) 级别。但同时,价值缩小到了一个可以接受的级别。所以考虑魔改上述过程,设 \(f_{i,j}\) 表示前 \(i\) 个物品,价值为 \(j\) 对应的最小重量:
注意初始值为 \(f_{0,1\sim W}=\infty,f_{0,0}=0\),依然需要倒序枚举来防止重复转移。类似地,可以把第一维优化掉。最终答案即为最大的 \(i\),满足 \(f_i\le W\)。时间复杂度 \(\mathcal{O}(nV)\)。评测记录。
经典 LCS 问题,不再介绍推导过程。设 \(f_{i,j}\) 表示 \(S\) 前 \(i\) 个字符与 \(T\) 前 \(j\) 个字符匹配,能得到的最大 LCS,则有转移:
意义就是考虑当前能否匹配上,如果能匹配就尝试匹配,否则就往前考虑。答案即为 \(f_{|S|,|T|}\)。但这道题还要求输出方案,\(\rm dp\) 输出方案通常我们都是记录转移的过程。不过这道题稍微有点麻烦,因为还涉及到哪个转移匹配上,哪些不匹配。考虑设 \(las_{i,j}\) 表示 \((i,j)\) 是从哪个二元组转移过来的,设 \(v_{i,j}\) 表示 \((i,j)\) 对应的匹配,如果没有就置为空。转移的时候记录一下,最终输出时从 \((|S|,|T|)\) 往前倒推整个过程即可。时间复杂度 \(\mathcal{O}(|S||T|)\)。评测记录。
DAG 上 \(\rm dp\)。考虑拓扑排序。如果存在一条 \(u\rightarrow v\) 的边,则有转移:
最终答案即为:
评测记录。
经典网格上 \(\rm dp\)。考虑设 \(f_{i,j}\) 表示从 \((1,1)\) 到 \((i,j)\) 的方案数,初始时 \(f_{1,1}=1\),答案即为 \(f_{H,W}\)。转移时,考虑从上还是左转移:
如果遇到障碍不转移即可。时间复杂度 \(\mathcal{O}(HW)\)。评测记录。
注意到,如果我们再观察一下 \(\rm dp\) 的转移:
可以发现,如果不存在障碍,能证明:
组合意义也比较好看出来,发现一共要走 \(i+j\) 步,我们只需要选出 \(i\) 步朝上的即可。
概率与期望 \(\rm dp\)。有一个比较显然的三维状态,考虑设 \(f_{i,j,k}\) 表示前 \(i\) 个有 \(j\) 个正面朝上的,\(k\) 个背面朝上的的概率。最终答案即为 \(\sum\limits_{j=1}^n\sum\limits_{k=1}^{j-1}f_{n,j,k}\)。这样转移的复杂度是 \(\mathcal{O}(n^3)\) 的,无法接受。但注意到,我们关心的是 \(j,k\) 的差,而不是具体值,所以考虑更改状态的设计。设 \(f_{i,j}\) 表示前 \(i\) 个,正面朝上的减去背面朝上的个数为 \(j\) 的概率。则转移为枚举这一位是正还是反:
答案即为 \(\sum\limits_{j=0}^nf_{n,j}\)。注意这里可能要处理负下标,平移一下即可。时间复杂度 \(\mathcal{O}(n^2)\)。评测记录。
概率与期望 \(\rm dp\)。注意到,有用的信息是剩下盘子里寿司的数量,我们需要设计一个合理的状态来描述。考虑设 \(f_{i,j,k}\) 表示剩下 \(1,2,3\) 个寿司的盘子分别有 \(i,j,k\) 个时,需要的操作次数。则转移就比较明显,考虑枚举这次随机到哪种类型的盘子即可:
把等式右边的 \(f_{i,j,k}\) 移到左边即可正常递推。除此之外,注意这个转移顺序比较难找,所以直接上记忆化搜索即可,边界条件为 \(f_{0,0,0}=0\),答案为 \(f_{cnt_1,cnt_2,cnt_3}\)。时间复杂度 \(\mathcal{O}(n^3)\)。评测记录。
博弈论。注意到,如果我们把游戏抽象为一张 DAG,每个点表一种剩余的石头数量,则我们会得到一张 \(\mathcal{O}(k)\) 个点,\(\mathcal{O}(nk)\) 条边的有向图。考虑一个点是先手必胜的,当且仅当它的出边存在一个点是先手必败的点,反之就是先手必败的。发现可以用记忆化搜索搜索这个图的状态,求出每个点的必胜必败状态,共 \(\mathcal{O}(k)\) 个状态,单次转移 \(\mathcal{O}(n)\),总时间复杂度 \(\mathcal{O}(nk)\)。评测记录。
还是博弈论。不过这次如果再抽象为 DAG 就点数太多了。考虑 \(\min\max\) 博弈,即我们考虑当前状态下是先手还是后手操作,然后分别根据它们的最优策略转移。具体来讲,设 \(f_{l,r}\) 表示序列剩下的区间为 \([l,r]\) 时的答案,则:
区间 \(\rm dp\) 即可,时间复杂度 \(\mathcal{O}(n^2)\)。评测记录。
前缀和优化 \(\rm dp\)。考虑设 \(f_{i,j}\) 表示前 \(i\) 个小孩分 \(j\) 个糖的方案数,则最终答案即为 \(f_{n,k}\)。一个比较显然的转移是:
时间复杂度 \(\mathcal{O}(nk^2)\),无法通过。注意到这个转移是区间和的形式,可以直接上前缀和优化,时间复杂度降至 \(\mathcal{O}(nk)\)。评测记录。
区间 \(\rm dp\)。考虑设 \(f_{l,r}\) 表示合并区间 \([l,r]\) 内的史莱姆需要的最小代价,则枚举当前区间是由哪两个子区间合并而来的可以得到:
其中 \(\operatorname{sum}_{l,r}\) 表示区间和,可以通过维护前缀和 \(\mathcal{O}(1)\) 得到。时间复杂度 \(\mathcal{O}(n^3)\)。评测记录。
状压 \(\rm dp\)。考虑设 \(f_S\) 表示当前男性的匹配情况为 \(S\) 时的方案数。这里需要解释的是,我们是按照顺序匹配女性的,即将一个男性加入集合时,我们钦定他匹配的是第 \(|S|+1\) 个女性。这样,\(f_S\) 的意义其实就是对于当前集合,有多少种合法的加入顺序。转移只需枚举上一次加入的是哪个男性:
初始时有 \(f_{\{0,0,0\cdots\}}=1\),答案即为 \(f_{\{1,1,1,\cdots\}}\)。时间复杂度 \(\mathcal{O}(2^nn)\)。评测记录。
树形 \(\rm dp\)。考虑设 \(f_{i,0/1}\) 表示结点 \(i\) 为白/黑色的方案数,则转移时,我们考虑将儿子的方案合并上来:
一遍 \(\rm dfs\) 即可转移,最终答案即为 \(f_{rt,0}+f_{rt,1}\),其中 \(rt\) 为 \(\rm dfs\) 时钦定的根。时间复杂度 \(\mathcal{O}(n)\)。评测记录。
带权的 LIS。比较经典的问题,考虑设 \(f_i\) 表示前 \(i\) 朵花能得到的最大价值,则转移时只需要往前面找一朵花接上即可:
直接转移是 \(\mathcal{O}(n^2)\) 的无法接受。注意到关于下标的限制是很弱的,我们只需要处理完 \(f_i\) 后把它扔到某个我们维护的东西里面,之后再查询时就能保证有效区间是合法的。所以我们把重点放在 \(h\) 的限制上。发现 \(h\) 的值域是很小的,考虑开一个 DS,第 \(x\) 位维护满足 \(h_i=x\) 的 \(f_i\) 最大值。则转移时,我们只需查询一个前缀最大值 \([1,h_i-1]\) 即可。可以用树状数组轻松实现上述功能,时间复杂度 \(\mathcal{O}(n\log n)\)。评测记录。
另外,如果 \(h\) 的值域很大,因为我们只关心大小关系,所以离散化后就能做了。
矩阵乘法优化 \(\rm dp\)。考虑设 \(f_{i,j,k}\) 表示从 \(i\) 到 \(j\),经过恰好 \(k\) 条边的方案数。容易发现 \(f_{i,j,1}\) 即为邻接矩阵。转移时,我们有:
发现这个很像矩阵乘法的形式,更进一步,如果我们设邻接矩阵为 \(\bf A\),则应该有:
归纳法易证。所以最终答案即为:
时间复杂度 \(\mathcal{O}(n^3\log k)\)。评测记录。
数位 \(\rm dp\)。考虑设 \(f_{i,j,0/1}\) 表示考虑了前 \(i\) 位,数位和模 \(d\) 为 \(j\),是否贴着 \(k\) 的上界。则转移有:
其中 \(k_i\) 表示 \(k\) 第 \(i\) 位的值。这个过程因为参数比较多,所以用记忆化搜索实现会更简单,时间复杂度 \(\mathcal{O}(d\log k)\)。评测记录。
这个好像国内不知名,国外称之为插入 \(\rm dp\),主要思路就是每次转移就是往某个序列中插入一个元素。注意到此次转移只和上一次填入的数有关,再加上排列的限制,我们考虑设 \(f_{i,j}\) 表示前 \(i\) 个数,填入 \(1\sim i\),最后一个数是 \(j\) 的方案数。转移就是考虑下一位填什么,但注意,我们要保证填的数是排列,所以加入 \(j\) 时,我们需要把所有 \(\ge j\) 的数向前平移一位,这大概就是插入这个名字的来源吧。这样,转移就比较好想了:
前缀和优化一下即可做到 \(\mathcal{O}(n^2)\) 的时间复杂度。评测记录
状压 \(\rm dp\)。容易想到设 \(f_S\) 表示已经考虑了 \(S\) 内的元素能得到的最大价值,则枚举最后一次加入的子集可以得到转移:
其中 \(\operatorname{sum}_S\) 表示把 \(S\) 集合内所有的元素归为一个集合的贡献。用枚举子集的技巧可以做到 \(\mathcal{O}(3^n)\) 的转移,算上预处理的复杂度,总时间复杂度 \(\mathcal{O}(2^nn^2+3^n)\)。评测记录。
二次扫描换根 \(\rm dp\)。注意到,原题相当于问原树的所有连通块中,包含 \(v\) 的有多少个。而这个问题,我们对原树的根是非常好回答的,具体来讲,如果我们设 \(f_u\) 表示只考虑以 \(u\) 为根的子树,包含 \(u\) 的连通块个数,则有:
即每个子树可以要或者不要,乘起来即为答案。这样,根的答案即为 \(f_{rt}\)。如果我们做 \(n\) 遍树形 \(\rm dp\),可以得到一个 \(\mathcal{O}(n^2)\) 的做法,无法通过。
考虑二次扫描换根 \(\rm dp\),这个做法的主体思路是先随便钦定一个根做一遍树形 \(\rm dp\),然后再 \(\rm dfs\) 一遍树,到达一个结点时,根据当前有的信息求出以它为根的答案。具体到这道题,假设第一遍 \(\rm dp\) 以 \(1\) 为根,则我们考虑再设一个 \(g_u\) 表示非在以 \(1\) 为根时 \(u\) 的子树对 \(u\) 的贡献,最终每个点的答案即为 \(f_ug_u\)。
转移时,我们不妨设 \(g_{fa_u}\) 已经求出,则对于 \(g_u\),它比 \(g_{fa_u}\) 多的贡献是,\(fa_u\) 以 \(1\) 为根时的所有除了 \(u\) 的子树的 \(f\)。具体来讲,有:
\(+1\) 是因为上面也可以不选。
但非常不幸,这道题不保证逆元存在,所以除法寄了。不过没问题,注意到:
维护个前缀和后缀积即可。时间复杂度 \(\mathcal{O}(n)\)。
线段树优化 \(\rm dp\)。发现区间的值域都不大(大了也可以离散化),所以我们一格一格考虑是否放入 \(1\) 和区间的贡献。
考虑我们考虑的点向右推进的过程,当我们碰到一个区间的右端点时,这个区间是否做贡献,取决于上一个放的 \(1\) 是否在它左端点的右边。所以我们先把区间按照右端点单调不降排序,然后设 \(f_{i,j}\) 表示考虑到第 \(i\) 个点,上一个 \(1\) 在 \(j\) 的最大分数。转移就比较好想了:
直接转移是 \(\mathcal{O}(n^2)\) 的无法通过。考虑优化,注意到转移的两个部分,前一个部分是区间取 \(\max\),后一个部分是区间加。(如果我们把条件看为 \(j\ge l_p\) 的话)这都是线段树能干的事。
具体来讲,考虑把 \(\rm dp\) 数组扔到线段树上,初始时第 \(j\) 个位置维护 \(f_{0,j}=-\infty\)。接着每次转移,集体将第 \(j\) 个位置的 \(f_{i,j}\) 更新为 \(f_{i+1,j}\)。我们先不考虑后面的和式,则需要更新的只有 \(f_{i,i}\),一次区间 \(\max\) 即可。然后对于后面的和式,我们只需要给 \(\ge l_p\) 的所有 \(j\) 加上 \(a_p\) 即可,一次区间加即可。最终答案即为 \(\max\{f_{n,1\sim n}\}\)。时间复杂度 \(\mathcal{O}(n\log n)\)。评测记录。
性质优化 \(\rm dp\)。关键问题在于怎么合理分配放置的顺序,注意到一个物品,可以转移到的重量区间为 \([w_i,w_i+s_i]\),则我们尽量想让后转移的区间比前转移的区间靠后,这样能尽量继承前面的,得到更大的价值。所以,我们考虑按照 \(w_i+s_i\) 单调不降的顺序排序。之后,做 01 背包即可。时间复杂度 \(\mathcal{O}(n\log n+nS)\)。评测记录。
状态设计优化 \(\rm dp\)。发现网格很大,没法直接 \(\rm dp\),但注意到障碍点的个数很小,所以可以考虑从障碍点入手。
考虑设 \(f_i\) 表示从 \((1,1)\) 走到第 \(i\) 个障碍点,且不经过任何其他障碍点的方案数,这样,如果我们把 \((H,W)\) 设为第 \(n+1\) 个关键点,答案即为 \(f_{n+1}\)。转移时,我们先考虑不加任何限制的情况,然后减去路上经过关键点的情况,即:
组合数的意义可以参考 Grid 1 的组合意义。预处理出阶乘和逆元,即可做到 \(\mathcal{O}(n^2)\) 的时间复杂度。评测记录。
斜率优化 \(\rm dp\)。延续 Frog 系列题的套路,设 \(f_i\) 表示走到 \(i\) 需要的最小代价,则有转移:
简单推一推式子,把与 \(j\) 无关的扔到左边:
考虑直线 \(y=kx+b\),变一变即为 \(b=y-kx\),发现上面的式子好像很符合。发现,\(y,x\) 都是只关于 \(j\) 的(\(y_j=f_j+h_j^2,x_j=h_j\)),而 \(k\) 是关于 \(i\) 的(\(k=h_i\)),所以本质上相当于平面上有一些点,我们要拿一条已知斜率的直线去切这个点,满足得到的 \(y\) 轴截距最小。
注意到,这样的点一定在下凸壳上,所以考虑对 \((x_j,y_j)\) 维护下凸壳。注意到每次切的斜率是单调递增的,所以每次能切到的点是能单调向前运动的,即如果我们维护的单调队列的队头和下一个点的斜率小于 \(2h_i\),则这个点就永远不可能成为决策点,弹出即可。之后队头就为最优决策点,之后再把当前点对应的决策点加入下凸壳的维护。时间复杂度 \(\mathcal{O}(n)\)。评测记录。