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 课件.