基础动态规划题单
[Accepted]Luogu4302. [SCOI2003]字符串折叠
\(f_{l,r}\) 表示将 \([l,r]\) 中的字符串折叠得到的最短长度,那么有两种情况:
- \([l,r]\) 由一个字符串重复多次得到,那么我们尝试枚举这个长度 \(x\),每次判断之间用哈希判断分成的 \(\frac n x\) 个串,每个长度判断的时间复杂度是 \(O(\frac n x)\),取最终结果最小值,对于这一段的时间复杂度为 \(O(\sum_{d|n} \frac n d)=O(n\log n)\);
- 其他的字符串的折叠情况一定可以通过将原串分为两部分拼接而成,枚举断点合并即可,时间复杂度为 \(O(n)\);
于是最终答案是 \(f_{1,n}\),总体时间复杂度为 \(O(n^3\log n)\)。
[Accepted]LOJ2124. [HAOI2015]树上染色
由于涂黑的点数有限制,所以自然地设计状态为 \(f_{i,j}\) 表示在以 \(i\) 为根的子树中选了 \(j\) 个为黑点,得到的子树中的最大价值,我们考虑一条边 \(i\) 如何贡献,假设它所连向以 \(x\) 为根的子树,\(x\) 的子树中有 \(j\) 个黑点,那么不论外面怎么选,\(i\) 上面一定有 \(k-j\) 个黑点,于是贡献就是 \(w_i\cdot(k-j)\cdot j\),白点经过这条边的代价类似,然后类似树上背包合并即可。
看起来时间复杂度是 \(O(n^3)\) 的,其实不然。即设现在要计算 \(x\) 的子树的价值,称 \(x\) 的子树为大子树,当前考虑的 \(x\) 的某个儿子 \(y\) 的子树为小子树。由于计算贡献时,我们背包的大小是子树大小,相当于给小子树内的每个点一个编号,给小子树外、大子树的点一个编号,每次分别枚举一个子树内的点和一个子树外的点配对(将枚举的大小看作编号),于是每个点作为小子树内的点配对时,最多与其他 \(n-1\) 个点各进行配对一次,于是总体时间复杂度为 \(O(n^2)\),注意这个时间复杂度成立的前提就是我们合并时枚举的背包大小必须是子树大小。
[Accepted]LOJ2559 [HNOI2003]消防局的设立
虽然说是 DP,但更容易想到的还是贪心,显然对于当前所有未被覆盖的点,我们一定是选择覆盖其中深度最深的节点,而且我们一定是将其放在他的祖父位置放置那么我们考虑如果要覆盖这个点,那么我们一定是在它的祖父位置(如果存在)安置最优(不存在就放在父亲),这样可以覆盖到的点一定最多。
考虑如何确定一个点是否被覆盖,最朴素的想法便是记录到达离它最近的消防站距离它的距离 d[]
,考虑如何更新一个点的距离:
- 将某个点的祖父选中时,修改其父亲和其祖父的
d[]
; - 处理到某个点时,用其父亲和祖父的
d[]
尝试更新自己(来自子孙的如果有一定已经被更新);
然后做就是了。
[Accepted]Luogu1131 [ZJOI2004]时态同步
注意到 \(x\) 的子树内的叶节点的时间一定被同步到其中最初到的最晚的时间,只需要在 \(x\) 下面不满足的子树到 \(x\) 的路径上进行对应的修改即可。
于是 \(f_{i}\) 表示将 \(i\) 的子树内的时态同步所需的最小操作次数,按照上面的想法进行转移即可。
[Accepted]Luogu1220 关路灯
考虑在任意时刻关闭的灯一定是一段连续的区间,于是我们可以设状态 \(f_{l,r}\) 表示将区间 \([l,r]\) 内的灯全部关闭时已消耗的最少电功,发现每次关灯一定是从之前区间的 \([l,r]\) 的边界中移动到 \(l-1\) 或 \(r+1\) 关灯,于是完善状态为:
\(f_{l,r,0/1}\) 表示将区间 \([l,r]\) 内的灯全部关闭时,最后关闭的灯是 \(l\) 或是 \(r\),已消耗的最少电功,
转移就考虑枚举一个区间 \([l,r]\),然后从 \([l+1,r]\) 和 \([l,r-1]\) 分别进行转移即可。
[Accepted]Luogu4766 [CERC2014]Outer space invaders
不难发现如果我们可以把每一个外星人看作一个时间轴上的区间,对于所有完全包含于某个区间 \([l,r]\) 内的所有外星人,必然有一次的杀伤距离是其中距离最远的外星人所处的位置,然后再考虑消灭该外星人所选取的时间 \(t\),那么显然与 \(t\) 有交集的所有外星人都会被消灭,那么剩下的一定是完全包含于 \([l,t-1]\) 和 \([t+1, r]\) 中的外星人。
根据上面的想法,我们可以设计如下的状态:\(f_{l, r}\) 表示将被完全包含在 \([l,r]\) 中的外星人全部消灭所需的最小代价,于是应当有转移:
\[ f_{l,r}=\min_{l_x\leq t\leq r_x}\{d_x+f_{l,t-1}+f_{t+1, r}\}, \]
其中 \(x\) 为该区间内距离最远的外星人的编号。显然我们需要将时间序列离散化。
[Accepted]Luogu1864 [NOI2009]二叉查找树
本题的一大难点就是观察出这棵二叉查找树是 Treap,改变权值会使得其旋转, 但中序遍历得到的数据值的序列不会变化,于是我们可以在这个序列上进行操作。
同时,虽然题目要求每个节点的权值不同,但是可以是实数,所以我们可以让某个点的权值无限逼近某个值,于是原题中权值不同的要求就消失了。
考虑对于一段中序遍历,它同时也是一个 dfn 序列,由于 Treap 的形态由各个节点的权值决定,我们可以考虑用这个 dfn 序列来还原出一棵子树,同样考虑到由于 Treap 的权值是满足堆性质,于是我们只需要确定出根节点的权值,剩下两棵子树的权值的下界也就确定了,而剩下的两棵子树怎样形成显然是另一个子问题,于是可以动态规划。
设 \(f_{l,r,x}\) 表示将 dfn 序列中的 \([l,r]\) 划分为一棵子树,子树中的点的权值不小于 \(x\) 的最小代价,然后我们考虑要不要修改当前根的价值,如果不改(要求 \(v_i\geq x\)),那么一定具有转移:
\[ \begin{aligned} f_{l,r,x}&=\min_{l\leq i\leq r}\left\{f_{l,i-1,v_i}+f_{i+1,r,v_i}+sum_{l,r}\right\},\\ \end{aligned} \]
否则我们要改,那么一定是贪心地将该点的权值修改为 \(x\),这样剩下的子树的限制会更小,于是有转移:
\[ \begin{aligned} f_{l,r,x}&=\min_{l\leq i\leq r}\left\{f_{l,i-1,x}+f_{i+1,r,x}+sum_{l,r}+k\right\},\\ \end{aligned} \]
其中,\(sum_{l,r}\) 表示 \([l,r]\) 的访问频度的和,这样做是将访问频度的代价拆分为了每层上的一部分一部分的代价和;
注意我们需要把所有的权值进行离散化到区间 \([1,n]\) 中,这样我们最后的答案就是 \(f_{1,n,1}\).
Luogu6478 [NOI Online #2 提高组]游戏
[Accepted]Luogu2157 [SDOI2009]学校食堂
首先不难看出代价就是 \(a\text{ xor } b\)。
对于状态,一个很自然的想法是 \(f_i\) 表示 \(i\) 及之前的完事了所花费的最小时间,但是注意到这样显然不能合理地区分状态,因为选择可以是不连续的,但是同样注意到对于 \(i\) 来说向后的扩展不可能超过 \(7\) 个人,于是我们考虑记录这 \(7\) 个人的状态,也就是将状态补充为 \(f_{i,S}\),表示 \([1,i-1]\) 中的人都已经打完了饭,第 \(i\) 个人以及他后面的 \(7\) 个人的状态是 \(S\),用一个 \(8\) 位二进制数表示,再考虑到转移时同样需要上一个打饭的是谁,于是在状态中还需要增添一维,要表示上一个打饭的人的位置,由于第 \(i-1\) 个人已经打了饭,显然上一个打饭的人不可能比 \(i-8\) 还要考前,所以用 \([-8,7]\) 表示上一个打饭的是 \(i\) 向后第几个人(用数组存需要 \(+8\)),于是,我们就能用 \(f_{i,S,j}\) 完整地表示一个状态,即:\([1,i-1]\) 中的所有人都已经打了饭,\([i,i+7]\) 中是否已打饭的情况是 \(S\),上一个打饭的是第 \(i+j\) 个人。
设计好了状态,下面来考虑如何转移。
注意到,对于状态 \(f_{i,S,j}\) 如果 \(S\) 代表 \(i\) 的一位已经为 \(1\),那么它应当和状态 \(f_{i+1,S',j-1}\) 等价,其中 \(S'\) 为将 \(S\) 中代表 \(i\) 的一位去掉,加上代表 \(i+8\) 的状态(为 \(0\))的一位,这一点是显然的,这一步的转移是不需要代价的。
之后,我们应当尝试从 \([i,i+7]\) 中寻找一位未打饭的给他打饭,转移到对应状态,注意给这个人打饭需要保证在其之前的所有未打饭的人都能接受他先打饭,为此我们需要维护一个位置为可行的能打饭的最远位置,这个位置是前面所有人可接受位置的最小值,我们只需要顺序枚举,在转移 \(f\) 的同时维护即可,注意到如果一个人不能满足了,那么他后面的一定也不行。
粗略的估计整体的时间复杂度是 \(O(n\cdot2^8\cdot15\cdot7)\),其中包含了很多不可达的状态。
[Accepted]Luogu5005 中国象棋 - 摆上马
考虑直接设计状态为 \(f_{i,S_1,S_2}\),表示考虑到第 \(i\) 行,摆放状态为 \(S_i\),第 \(i-1\) 行的摆放状态为 \(S_2\)。直接预处理出对于单行,上一行不能存在的位置有哪些,然后再对于两行 \(S_1,S_2\)(\(S_1\) 在下,\(S_2\) 在上),上面一行 \(S_3\) 不能存在的位置有哪些,容易发现这恰好也即是 \(S_1\) 在上,\(S_2\) 在下时,下面一行 \(S_3\) 不能存在的位置,然后可以直接做。
[Accepted]Luogu3977 [TJOI2015]棋盘
容易发现关键点一定在中间的一行,于是可以直接处理出当前行每种状态上下的影响范围,设状态为 \(f_{i,S}\),为到第 \(i\) 行的状态为 \(S\),可行的情况数,时间复杂度为 \(O(n2^{2m})\),显然不可过,于是考虑优化。
注意到对于一个状态 \(S\),可行的转移来源都是确定的,也就是
\[ f_{i,S}=\sum_{S'}c_{S,S'}\cdot f_{i-1,S'}, \]
其中 \(c_{S,S'}\) 为当前行状态为 \(S\),上一行是否可以为 \(S'\),\(c_{S,S'}\) 显然都是确定的,于是考虑用矩阵快速幂优化。显然状态数不会超过 \(2^6=64\),于是构建一个 \(64\times64\) 的转移矩阵 \(T\),\(T_{i,j}\) 为第 \(j\) 个状态是否可以从(上一行状态为)状态 \(i\) 转移,\(F_i\) 为一个 \(64\) 元行向量,表示第 \(i\) 行每种状态的可行方案数,显然有
\[ F_i=F_{i-1}T\Rightarrow F_n=F_1(T)^{n-1}, \]
然后直接矩阵快速幂即可,时间复杂度为 \(O(2^{3m}\log n)\).
[Accepted]LOJ2318 [NOIP2017]宝藏
较为重要的一点是看出我们最终一定是得到一棵原图的生成树,然后对于一个树上的点,它的贡献就是它连向原本生成树的边权乘上它所在的深度,于是我们设计状态 \(F_{S,i}\) 为当前已经加入生成树的点集为 \(S\),生成树的最大深度为 \(i\) 时的最小代价。
那么考虑转移,我们枚举形成 \(S\) 的子集 \(S'\),\(S\) 由 \(S'\) 向外连边得到,显然应当连接 \(\complement_SS'\) 中的点向 \(S'\) 中所花费最少的边,于是应当是这些边权求和乘上最大深度,显然这样算出的答案不一定是正确的,但是假设 \(\complement_SS'\) 中有的点不是连到 \(S'\) 中最大深度的点上,那么一定存在另一个集合 \(S''\) 包含这些点,且最大深度与 \(S'\) 相同,显然这个集合得到的答案不会比 \(S'\) 得到的更大,于是更新到 \(S\) 上时得到的答案一定是正确的答案。
总体时间复杂度为 \(O(3^n\cdot n^2)\),考虑到不合法的状态,\(3^n\) 和 \(n^2\) 都不容易跑满,于是能过。
[Accepted]Luogu5933 [清华集训2012]串珠子
我们首先直接考虑设 \(f_S\) 为点集 \(S\) 中的点全部连通的方案数,直接计算发现不好计算,正难则反,设 \(g_S\) 为点集 \(S\) 中的点所有的连边的方案数,\(h_S\) 为点集 \(S\) 中的点不连通的方案数,显然有
\[ f_S=g_S-h_S, \]
\(g_s\) 的计算比较显然,就是存在的点两两之间连边的方案数的乘积,可以预处理。
再来考虑 \(h_S\),发现我们可以指定 \(S\) 的一个子集 \(S'\) 连通,剩下的部分连通性随意,这两部分之间不相连,这样的不同的方案的数量就是 \(h_S\),我们考虑如何在不算重的情况下计算 \(h_S\),对于我们指定连通的子集 \(S'\),我们可以强制要求其中包含点 \(x\)(\(x\in S\)),得到的 \(\complement_S S'\) 自然一定不含 \(x\),这样,将 \(S\) 分为两个部分,\(x\) 所处的部分一定都不同,于是得到的结果一定是不重复的。
[Accepted]Luogu4363 [九省联考2018]一双木棋chess
看到数据范围大致知道是状压,但是要考虑怎么记录屈居的状态,注意到所下的棋,自上至下每一行靠左边的棋子个数一定是非严格单调递减,同时发现我们不需要知道当前局面的每一个棋子都是谁下的,并且每个局面接下来该谁下都是固定的,于是我们可以直接记录左上角的连通块的轮廓线,将竖边记为 \(1\),横边记为 \(0\),从右上角到左下角一次记录,于是就可以得到一个长度为 \(n+m\) 的 01 串,每个 01 串都唯一表示一个局面。
于是我们设计状态为 \(f_{S}\) 表示 \(S\) 局面时剩下的棋子还可以得到的得分。
转移时就可以依次考虑轮廓线边缘的每个位置是否可以放置,然后进行转移,为了方便,就可以直接用记忆化搜索解决。