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. \(1, 2...n\) 依次进栈, 求有多少种可能的出栈序列.

  2. \(n\) 对括号形成的合法的括号序列由多少个?

  3. \(n\) 个节点共能构成多少种二叉树, 左右子树是认为不同.

  4. 凸多边形的三角划分的方案数: 把一个凸多边形用 \(n - 3\) 条直线连接 \(n - 3\) 对顶点, 共形成n-2个三角形,求方案数.

  5. 一个 \(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 课件.