DP笔记 2021.2.4 下午
Preview
今下午的主要内容是
-
序列 DP
-
区间 DP
可以说是动态规划里面最简单的一种类型, 通过学习和思考此类问题, 大家能够加深对动态规划本身的理解.
序列 DP
序列上的 DP 状态设计最基本的形式
-
\(F_i\) 表示以 \(i\) 结尾的最优值或方案数
-
\(F_{i, k}\) 表示以 \(i\) 结尾附加信息为 \(k\) 的最优值或方案数, 转移的话往往是枚举上一个断点.
\(F_i = max(F_j + w_{j+1 , i})\) (\(j\) 是一个满足转移条件的断点)
这是最简单的一类序列上的 DP
Luogu P1772 [ZJOI2006]物流运输
有 \(m\) 个码头和 \(e\) 条航线, 每条航线有成本. 有连续 \(n\) 天需要从 \(1\) 号码头到 \(m\) 号码头运输货物. 每个码头会在某些天数区间内不许经过. 每更换一次运输路线, 要付出 \(k\) 的成本求这 \(n\) 天的最小总成本.
\(m \leq 20\), \(n \leq 100\)
其实就是分成很多段, 每一段选同一个运输路线, 然后得到一个最优的划分方案, 使得成本最小.
\(f_i\) 表示前 \(i\) 天的运输最小成本。
\[f_i=min(f_j + k + w_{j + 1, i} * (i - j))\ (j其中 \(w(x,y)\) 表示最短的在第 \(x\) 天到第 \(y\) 天都能用的路线长度, 把能在则几天一直走的点加进图中, 跑最短路径即可.
\(O(N^2mlogm)\)
bzoj1296粉刷匠
有 \(n\) 条木板要被粉刷, 每条木板分为 \(m\) 个格子, 每个格子需要被刷成蓝色或红色.
每次粉刷可以在一条木板上给连续的一段格子刷上相同的颜色. 每个格子最多被刷一次.
问若只能刷 \(k\) 次, 最多正确粉刷多少格子.
\(n,m<=50\), \(k<=2500\)
\(n == 1\)
如果只有一条木板, 那么设 \(g_{i, j}\)表示前 \(i\) 个格子刷 \(j\) 次的最多正确格子
\[g_{i, j}=max(g_{k, j-1} + w_{k+1, i})\ (k\(w_{x, y}\) 为第 \(x\) 到第 \(y\) 个格子的最多同色格子数, 哪个颜色出现的多刷哪个, 直接记一个前缀和即可.
有多条木板, 设 \(f_{i, j}\) 表示前 \(i\) 个木板刷 \(j\) 次的最大答案.
\[f_{i, j} = max(f_{i-1, k} + g_{m, j-k})\ (k \leq j) \]其实像这种般的 DP, 就是把影响答案的信息用多维状态来表示, 什么必要什么就放在状态里.
CF 314E
给定一个长度为 \(n\) 的仅包含左括号和问号的字符串, 将问号变成左括号或右括号使得该括号序列合法, 求方案总数. 例如 \((())\) 与 \(()()\) 都是合法的括号序列.
\(n<=3000\)
括号序列问题, 往往就是把左括号看成 \(+1\), 右括号看成 \(-1\),我们只需要保证任意一个前缀大于等于 \(0\), 且总和为 \(0\), 就代表是个合法括号序列了.
令 \(dp_{i, j}\) 表示当前到第 \(i\) 个字符, 现在还有 \(j\) 个左括号.
那么分 \(3\) 种情况考虑:
-
若第 \(i+1\) 个字符是左括号, 则能转移到 \(dp_{i+1, j+1}\).
-
若第 \(i+1\) 个字符是右括号, 则能转移到 \(dp_{i+1, j-1}\).
-
若第 \(i+1\) 个字符是问号, 则能转移到 \(dp_{i+1, j-1}\) 与 \(dp_{i+1, j+1}\).
最终 \(dp_{n, 0}\) 就是方案总数啦.
\(O(n^2)\)
bzoj4922
给出一些括号序列,要求选择一些括号序列拼接成一个合法的括号序列, 使得总长最大.
\(1 \leq n \leq 300\), 表示括号序列的个数
括号序列的长度 \(len\) 不超过 \(300\).
\[(((())))\ \rightleftharpoons\ empty\\ )(((())))(\ \rightleftharpoons\ )(\\ )())))(\ \rightleftharpoons\ ))))( \]首先对于每个括号序列, 把左边的左括号和右边的右括号对消, 最后能得到一坨这样的东西:
\())…))((…((\)
就是 \(x\) 个右括号然后 \(y\) 个左括号, 记作 \((x, y)\)
然后考虑假如我们的子集选好了, 我们要按照什么顺序拼接才能拼成一个合法的括号序列呢?
这就转化成了另一个问题:
BZOJ3709
在一款电脑游戏中, 你需要打败 \(n\) 只怪物 (从 \(1\) 到 \(n\) 编号). 为了打败第 \(i\) 只怪物, 你需要消耗 \(d_i\) 点生命值, 但怪物死后会掉落血药, 使你恢复 \(a_i\) 点生命值. 任何时候你的生命值都不能降到 \(0\) (或 \(0\) 以下)
请问是否存在一种打怪顺序, 使得你可以打完这 \(n\) 只怪物而不死掉.
\(N \leq 10^5\)
贪心, NOIp 的贪心很多都是按照某种方式排序, 然后依次选或处理.
我们来看看应该怎么排序.
-
如果 \(a_i - d_i > 0\), 说明打掉这个怪兽有血可恢复, 那么血量会变多, 明显我们按照伤害 \(d_i\) 从小到大排序即可, 然后一个个杀下来.
-
如果 \(a_i - d_i < 0\), 说明会亏血. 一个精妙的想法就是, 最后剩余的血量值, 假设是 \(x\), 那么 \(x\) 是固定的. 然后可以看作初始血量为 \(x\), 怪兽的属性 \(a\), \(d\) 交换,这样就和上一种情况一样了.
回到bzoj4922
我们还是把左括号看成 \(+1\), 右括号看成 \(-1\), 同样是保证任意一个前缀大于等于 \(0\), 且总和为 \(0\).
那就是每一个给定的序列都是 先 \(-L_i\)再 \(+R_i\), \(L_i\) 是对消后左端右括号的数量, \(L_i\) 是对消后右端左括号的数量. 然后依次拼起来之后任何一个前缀都大于等于 \(0\), 这个其实和刚刚所讲的题目完全一样.
我们按照上一题的做法排序即可, 排序后我们从左往右做 DP.
设 \(f_{i, j}\) 为前 \(i\) 个括号序列 \(-1\) 与 \(+1\) 的和为 \(j\) 个时选出括号序列最长的长度和.
也就是前 \(i\) 个括号序列左括号比右括号多 \(j\) 个时的最长的长度和.
转移时考虑下一个括号序列选不选即可.
\(Len_i\) 为排完序后第i个括号序列的长度.
\[f_{i+1, j-L_{i+1}+R_{i+1}} \leftarrow f_{i, j} + len_{i+1}\ (j \geq L_{i+1})\\ f_{i+1, j} \leftarrow f_{i, j} \]最后答案就是 \(f_{n, 0}\). 复杂度 \(O(nlen^2)\)
卡特兰数
-
\(1, 2...n\) 依次进栈, 求有多少种可能的出栈序列.
-
由 \(n\) 对括号形成的合法的括号序列由多少个?
-
\(n\) 个节点共能构成多少种二叉树, 左右子树是认为不同.
-
凸多边形的三角划分的方案数: 把一个凸多边形用 \(n - 3\) 条直线连接 \(n - 3\) 对顶点, 共形成n-2个三角形,求方案数.
-
一个 \(n * n\) 的格子, 从 \((0,0)\) 走到 \((n,n)\), 求不跨过 \((0,0) \Rightarrow (n,n)\) 这条直线的路径方案数.
\(N \leq 10^5\)
我们设 \(f_n\) 表示 \(n\) 个数依次进栈所能形成的出栈序列数.
似乎和之前不一样, 好像不是划分成一段一段那样的简单形式.
我们可以考虑另一种形式的状态转移方式, 以转移到子问题.
注意一段一段划分我们可以枚举最后一段的起点, 但是这里不是一段一段的, 我们要考虑另外的转移方式.
实际上我们发现我们可以枚举 \(1\) 这个数是什么时候出栈的. 那么我们可以得到:
\[f_n = \sum_{i = 0}^{n - 1} (f_i * f_{n - 1 - i}) \]其实还有一个更简便的形式:
\[\binom{2n}{n} - \binom{2n}{n - 1} \\ =\frac{\binom{2n}{n}}{n + 1} \]一个经典题
有 \(n\) 个数, 选择其中若干数, 使得每连续 \(k\) 个数中都至少有一个数被选中, 且选出的数的和最小.
\[k<=n<=1000\\ k<=n<=100000 \]令 \(dp_i\) 表示前 \(i\) 个数满足题目要求且第 \(i\) 个数被选中, 这样的情况下选出的数的和最少是多少.
通过枚举上一个被选出的数 \(j\) 在哪里, 有:
\[dp_i=min(dp_j)+a_i\ (i - j \leq k) \]\(O(n^2)\)
单调队列
我们合法的转移区间不断向右移动, 而这就是一个典型的滑动窗口问题.
对于两个决策 \(j_1\), \(j_2\), 满足 \(j_1 < j_2\)
若 \(dp_{j_1} < dp_{j_2}\), 则当 \(i - j_1 >k\), \(i - j_2 \leq k\)时, \(j_2\) 能代替 \(j_1\)
若 \(dp_{j_1} \geq dp_{j_2}\), 则无论何时, \(j_1\) 都不可能比 \(j_2\) 优, 可以直接删除.
每次在队列末尾插入删除一个数或者在队首删除一个数, 且该队列始终保持单调递增.
因此称为单调队列优化.
每个数进入队列出队列一次, 转移是 \(0(1)\) 的, 总时间复杂度为 \(O(n)\)
int l=1,r=0;
for (int i=1;i<=n;i++) {
while (l<=r&&q[l]
区间 DP
区间 DP 一般就是设 \(dp_{i, j}\) 表示区间 \([i,j]\) 所能形成的最优答案或者方案数.
或者像序列一样, 多加几维表示附加的信息.
最简单的区间dp:合并石子
有 \(n\) 堆石子, 每次只能合并相邻的两堆石子, 并且消耗相邻两堆石子个数的体力值, 问最少消耗多少体力值将 \(n\) 堆石子合并为 \(1\) 堆.
\(N \leq 100\)
\(dp_{i, j}\) 表示将区间 \([i,j]\) 这段区间内的石子合并为 \(1\) 堆的最小体力值.
答案就是 \(dp_{1, n}\).
转移, 考虑对于区间 \([i, j]\) 它一定是由两段区间 \([i,k]\), \([k+1,j]\) 合并成的, 所以转移就考虑枚举 \([i,j]\) 区间内的一个分割点 \(k\) 转移即可。
\[dp_{i, j} = sum_{i, j} + min(dp_{i, k} + dp_{k + 1, j})\ (i \leq k < j) \]一定要注意区间 DP 的枚举顺序!
for (int l = n; l >= l; l--)
for (int r = l; r <= n; r++)
if (l == r)
dp[l][r] = 0;
else {
dp[l][r] = inf;
for (int k = l; k < r; k++)
dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r]+(sum[r] - sum[l - 1]));
}
CF 245H
给定一个字符串 \(S\), \(Q\) 组询问, 每次询问区间 \([l,r]\) 内有多少回文子串.
\(len_S \leq 1000\)
状态: \(dp_{i, j}\) 表示区间 \([i,j]\) 有多少回文子串.
\[dp_{i, j} = dp_{i, j - 1} + dp_{i+1, j} - dp_{i+1, j-1} + ([i,j]是否是回文串) \]poj 3280
给你长度为 \(m\) 的字符串, 其中有 \(n\) 种字符, 每种字符都有两个值, 分别是插入这个字符的代价, 删除这个字符的代价, 让你求将原先给出的那串字符变成一个回文串的最小代价.
\(dp_{i, j}\) 代表区间 \(i\) 到区间 \(j\) 成为回文串的最小代价, 那么对于 \(dp_{i. j}\) 有三种情况:
-
\(dp_{i+1, j}\) 表示区间 \(i\) 到区间 \(j\) 已经是回文串了的最小代价, 那么对于 \(s_i\) 这个字母, 我们有两种操作, 删除与添加, 对应有两种代价, \(dp_{i+1, j} + add_{s_i}\) 或 \(dp_{i+1, j} + del_{s_i}\), 取这两种代价的最小值.
-
\(dp_{i, j - 1}\) 表示区间 \(i\) 到区间 \(j - 1\) 已经是回文串了的最小代价, 那么对于 \(s_j\) 这个字母,同样有两种操作, \(dp_{i, j-1}+add_{s_j}\) 或 \(dp_{i, j-1} + del_{s_j}\), 取最小值.
-
若是 \(s_i == s_j\), \(dp_{i+1, j-1}\) 表示区间 \(i + 1\) 到区间 \(j - 1\) 已经是回文串的最小代价, 那么对于这种情况, 我们考虑 \(dp_{i, j}\) 与 \(dp_{i+1, j-1}\) 的大小.
然后 \(dp_{i, j}\) 取上面这些情况的最小值.
能量项链
环形问题有一个很常见的处理办法是, 断环为链, 然后把这个链复制一遍接在原链的后面.
然后做区间 DP, 最后取答案就是找 \(dp_{i, i+n-1}\) 里面取最优的即可.
在项链上有 \(N\) 颗能量珠. 能量珠是颗有头标记与尾标记的珠子, 这些标记对应着某个正整数. 并且, 对于相邻的两颗珠子, 前一颗珠子的尾标记一定等于后一颗珠子的头标记.
如果前一颗能量珠的头标记为 \(m\), 尾标记为 \(r\), 后一颗能量珠的头标记为 \(r\), 尾标记为 \(n\), 则聚合后释放的能量为 \(m * r * n\), 新产生的珠子的头标记为 \(m\), 尾标记为 \(n\).
需要时, Mars人就用吸盘夹住相邻的两颗珠子, 通过聚合得到能量, 直到项链上只剩下一颗珠子为止. 显然, 不同的聚合顺序得到的总能量是不同的, 请你设计一个聚合顺序, 使一串项链释放出的总能量最大.
\(N \leq 100\)
在读入的时候现将珠子们复制一遍放到后面, 断环成链
设 \(f_{j, i}\) 表示左端点为 \(j\) 号珠子, 右端点为 \(i\) 号珠子的区间所能得到的最大能量, 转移就枚举最后一步聚合的位置即可.
\[f_{j, i} = max(f_{j, i}, f_{j, k} + f_{k + 1, i} + a_j * a_{k + 1} * a_{i + 1})\\ Ans = max(f_{1, n}, f_{2, n}, f_{3, n},..., f_{n, 2n - 1}) \]区间dp两类主要的转移套路
一般 \(n=1000\) 的区间 DP 问题, 由于状态就是二维的了, 转移一般都是 \(O(1)\), DP 转移过程中主要考虑就是 \(L\) 和 \(R\) 两个边界的情况, 正如之前的 \(2\) 道题.
而 \(n = 100\) 的区间 DP, 除了边界往往还要枚举这个区间从哪个位置划分.
不能说一定是这样, 但是绝对是一个很好的思考方向. 看数据范围猜算法.
总结
-
组合数学的经典数列: 卡特兰数
-
序列 DP 问题一种常见的优化方法: 单调队列优化. 之后背包 DP 和基环树问题中,都要涉及单调队列优化, 我们在此先对其有个初步的了解.
-
括号序列问题, 把左括号看成 \(+1\) 右括号看成 \(-1\) 的常用套路.
-
区间 DP 状态设计的一般形式.
-
区间 DP 处理环形问题.
-
区间 DP 转移一般考虑区间边界的情况, 或者由两个小区间合起来的情况.
整理自赵和旭的 DP 课件.