DP笔记 2021.2.4 下午

Preview

今下午的主要内容是

  • 序列 DP

  • 区间 DP

可以说是动态规划里面最简单的一种类型, 通过学习和思考此类问题, 大家能够加深对动态规划本身的理解.

序列 DP

序列上的 DP 状态设计最基本的形式

  • Fi 表示以 i 结尾的最优值或方案数

  • Fi,k 表示以 i 结尾附加信息为 k 的最优值或方案数, 转移的话往往是枚举上一个断点.

Fi=max(Fj+wj+1,i) (j 是一个满足转移条件的断点)

这是最简单的一类序列上的 DP

Luogu P1772 [ZJOI2006]物流运输

m 个码头和 e 条航线, 每条航线有成本. 有连续 n 天需要从 1 号码头到 m 号码头运输货物. 每个码头会在某些天数区间内不许经过. 每更换一次运输路线, 要付出 k 的成本求这 n 天的最小总成本.

m20, n100

其实就是分成很多段, 每一段选同一个运输路线, 然后得到一个最优的划分方案, 使得成本最小.

fi 表示前 i 天的运输最小成本。

fi=min(fj+k+wj+1,i(ij)) (j<i)

其中 w(x,y) 表示最短的在第 x 天到第 y 天都能用的路线长度, 把能在则几天一直走的点加进图中, 跑最短路径即可.

O(N2mlogm)

bzoj1296粉刷匠

n 条木板要被粉刷, 每条木板分为 m 个格子, 每个格子需要被刷成蓝色或红色.

每次粉刷可以在一条木板上给连续的一段格子刷上相同的颜色. 每个格子最多被刷一次.

问若只能刷 k 次, 最多正确粉刷多少格子.

n,m<=50, k<=2500

n==1

如果只有一条木板, 那么设 gi,j表示前 i 个格子刷 j 次的最多正确格子

gi,j=max(gk,j1+wk+1,i) (k<i)

wx,y 为第 x 到第 y 个格子的最多同色格子数, 哪个颜色出现的多刷哪个, 直接记一个前缀和即可.

有多条木板, 设 fi,j 表示前 i 个木板刷 j 次的最大答案.

fi,j=max(fi1,k+gm,jk) (kj)

其实像这种般的 DP, 就是把影响答案的信息用多维状态来表示, 什么必要什么就放在状态里.

CF 314E

给定一个长度为 n 的仅包含左括号和问号的字符串, 将问号变成左括号或右括号使得该括号序列合法, 求方案总数. 例如 (())()() 都是合法的括号序列.

n<=3000

括号序列问题, 往往就是把左括号看成 +1, 右括号看成 1,我们只需要保证任意一个前缀大于等于 0, 且总和为 0, 就代表是个合法括号序列了.

dpi,j 表示当前到第 i 个字符, 现在还有 j 个左括号.

那么分 3 种情况考虑:

  • 若第 i+1 个字符是左括号, 则能转移到 dpi+1,j+1.

  • 若第 i+1 个字符是右括号, 则能转移到 dpi+1,j1.

  • 若第 i+1 个字符是问号, 则能转移到 dpi+1,j1dpi+1,j+1.

最终 dpn,0 就是方案总数啦.

O(n2)

bzoj4922

给出一些括号序列,要求选择一些括号序列拼接成一个合法的括号序列, 使得总长最大.

1n300, 表示括号序列的个数

括号序列的长度 len 不超过 300.

(((())))  empty)(((())))(  )()())))(  ))))(

首先对于每个括号序列, 把左边的左括号和右边的右括号对消, 最后能得到一坨这样的东西:

))))((((

就是 x 个右括号然后 y 个左括号, 记作 (x,y)

然后考虑假如我们的子集选好了, 我们要按照什么顺序拼接才能拼成一个合法的括号序列呢?

这就转化成了另一个问题:

BZOJ3709

在一款电脑游戏中, 你需要打败 n 只怪物 (从 1n 编号). 为了打败第 i 只怪物, 你需要消耗 di 点生命值, 但怪物死后会掉落血药, 使你恢复 ai 点生命值. 任何时候你的生命值都不能降到 0 (或 0 以下)

请问是否存在一种打怪顺序, 使得你可以打完这 n 只怪物而不死掉.

N105

贪心, NOIp 的贪心很多都是按照某种方式排序, 然后依次选或处理.

我们来看看应该怎么排序.

  • 如果 aidi>0, 说明打掉这个怪兽有血可恢复, 那么血量会变多, 明显我们按照伤害 di 从小到大排序即可, 然后一个个杀下来.

  • 如果 aidi<0, 说明会亏血. 一个精妙的想法就是, 最后剩余的血量值, 假设是 x, 那么 x 是固定的. 然后可以看作初始血量为 x, 怪兽的属性 a, d 交换,这样就和上一种情况一样了.

回到bzoj4922

我们还是把左括号看成 +1, 右括号看成 1, 同样是保证任意一个前缀大于等于 0, 且总和为 0.

那就是每一个给定的序列都是 先 Li+Ri, Li 是对消后左端右括号的数量, Li 是对消后右端左括号的数量. 然后依次拼起来之后任何一个前缀都大于等于 0, 这个其实和刚刚所讲的题目完全一样.

我们按照上一题的做法排序即可, 排序后我们从左往右做 DP.

fi,j 为前 i 个括号序列 1+1 的和为 j 个时选出括号序列最长的长度和.

也就是前 i 个括号序列左括号比右括号多 j 个时的最长的长度和.

转移时考虑下一个括号序列选不选即可.

Leni 为排完序后第i个括号序列的长度.

fi+1,jLi+1+Ri+1fi,j+leni+1 (jLi+1)fi+1,jfi,j

最后答案就是 fn,0. 复杂度 O(nlen2)

卡特兰数

  1. 1,2...n 依次进栈, 求有多少种可能的出栈序列.

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

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

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

  5. 一个 nn 的格子, 从 (0,0) 走到 (n,n), 求不跨过 (0,0)(n,n) 这条直线的路径方案数.

N105

我们设 fn 表示 n 个数依次进栈所能形成的出栈序列数.

似乎和之前不一样, 好像不是划分成一段一段那样的简单形式.

我们可以考虑另一种形式的状态转移方式, 以转移到子问题.

注意一段一段划分我们可以枚举最后一段的起点, 但是这里不是一段一段的, 我们要考虑另外的转移方式.

实际上我们发现我们可以枚举 1 这个数是什么时候出栈的. 那么我们可以得到:

fn=i=0n1(fifn1i)

其实还有一个更简便的形式:

(2nn)(2nn1)=(2nn)n+1

一个经典题

n 个数, 选择其中若干数, 使得每连续 k 个数中都至少有一个数被选中, 且选出的数的和最小.

k<=n<=1000k<=n<=100000

dpi 表示前 i 个数满足题目要求且第 i 个数被选中, 这样的情况下选出的数的和最少是多少.

通过枚举上一个被选出的数 j 在哪里, 有:

dpi=min(dpj)+ai (ijk)

O(n2)

单调队列

我们合法的转移区间不断向右移动, 而这就是一个典型的滑动窗口问题.

对于两个决策 j1, j2, 满足 j1<j2

dpj1<dpj2, 则当 ij1>k, ij2k时, j2 能代替 j1

dpj1dpj2, 则无论何时, j1 都不可能比 j2 优, 可以直接删除.

每次在队列末尾插入删除一个数或者在队首删除一个数, 且该队列始终保持单调递增.

因此称为单调队列优化.

每个数进入队列出队列一次, 转移是 0(1) 的, 总时间复杂度为 O(n)

int l=1,r=0;
for (int i=1;i<=n;i++) {
  while (l<=r&&q[l]<i-m)
    l++; //把已经不在合法转移区间去掉
  dp[i]=a[i]+dp[q[l]] ;
  while (l<=r&&dp[l]<=dp[q[r]])
    r--; //核心,及时弹出对答案贡献一定不如 i 的元素
  q[++r]=i;
}

区间 DP

区间 DP 一般就是设 dpi,j 表示区间 [i,j] 所能形成的最优答案或者方案数.

或者像序列一样, 多加几维表示附加的信息.

最简单的区间dp:合并石子

n 堆石子, 每次只能合并相邻的两堆石子, 并且消耗相邻两堆石子个数的体力值, 问最少消耗多少体力值将 n 堆石子合并为 1 堆.

N100

dpi,j 表示将区间 [i,j] 这段区间内的石子合并为 1 堆的最小体力值.

答案就是 dp1,n.

转移, 考虑对于区间 [i,j] 它一定是由两段区间 [i,k], [k+1,j] 合并成的, 所以转移就考虑枚举 [i,j] 区间内的一个分割点 k 转移即可。

dpi,j=sumi,j+min(dpi,k+dpk+1,j) (ik<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] 内有多少回文子串.

lenS1000

状态: dpi,j 表示区间 [i,j] 有多少回文子串.

dpi,j=dpi,j1+dpi+1,jdpi+1,j1+([i,j])

poj 3280

给你长度为 m 的字符串, 其中有 n 种字符, 每种字符都有两个值, 分别是插入这个字符的代价, 删除这个字符的代价, 让你求将原先给出的那串字符变成一个回文串的最小代价.

dpi,j 代表区间 i 到区间 j 成为回文串的最小代价, 那么对于 dpi.j 有三种情况:

  • dpi+1,j 表示区间 i 到区间 j 已经是回文串了的最小代价, 那么对于 si 这个字母, 我们有两种操作, 删除与添加, 对应有两种代价, dpi+1,j+addsidpi+1,j+delsi, 取这两种代价的最小值.

  • dpi,j1 表示区间 i 到区间 j1 已经是回文串了的最小代价, 那么对于 sj 这个字母,同样有两种操作, dpi,j1+addsjdpi,j1+delsj, 取最小值.

  • 若是 si==sj, dpi+1,j1 表示区间 i+1 到区间 j1 已经是回文串的最小代价, 那么对于这种情况, 我们考虑 dpi,jdpi+1,j1 的大小.

然后 dpi,j 取上面这些情况的最小值.

能量项链

环形问题有一个很常见的处理办法是, 断环为链, 然后把这个链复制一遍接在原链的后面.

然后做区间 DP, 最后取答案就是找 dpi,i+n1 里面取最优的即可.

在项链上有 N 颗能量珠. 能量珠是颗有头标记与尾标记的珠子, 这些标记对应着某个正整数. 并且, 对于相邻的两颗珠子, 前一颗珠子的尾标记一定等于后一颗珠子的头标记.

如果前一颗能量珠的头标记为 m, 尾标记为 r, 后一颗能量珠的头标记为 r, 尾标记为 n, 则聚合后释放的能量为 mrn, 新产生的珠子的头标记为 m, 尾标记为 n.

需要时, Mars人就用吸盘夹住相邻的两颗珠子, 通过聚合得到能量, 直到项链上只剩下一颗珠子为止. 显然, 不同的聚合顺序得到的总能量是不同的, 请你设计一个聚合顺序, 使一串项链释放出的总能量最大.

N100

在读入的时候现将珠子们复制一遍放到后面, 断环成链

fj,i 表示左端点为 j 号珠子, 右端点为 i 号珠子的区间所能得到的最大能量, 转移就枚举最后一步聚合的位置即可.

fj,i=max(fj,i,fj,k+fk+1,i+ajak+1ai+1)Ans=max(f1,n,f2,n,f3,n,...,fn,2n1)

区间dp两类主要的转移套路

一般 n=1000 的区间 DP 问题, 由于状态就是二维的了, 转移一般都是 O(1), DP 转移过程中主要考虑就是 LR 两个边界的情况, 正如之前的 2 道题.

n=100 的区间 DP, 除了边界往往还要枚举这个区间从哪个位置划分.

不能说一定是这样, 但是绝对是一个很好的思考方向. 看数据范围猜算法.

总结

  • 组合数学的经典数列: 卡特兰数

  • 序列 DP 问题一种常见的优化方法: 单调队列优化. 之后背包 DP 和基环树问题中,都要涉及单调队列优化, 我们在此先对其有个初步的了解.

  • 括号序列问题, 把左括号看成 +1 右括号看成 1 的常用套路.

  • 区间 DP 状态设计的一般形式.

  • 区间 DP 处理环形问题.

  • 区间 DP 转移一般考虑区间边界的情况, 或者由两个小区间合起来的情况.

整理自赵和旭的 DP 课件.

posted @   Wild_Donkey  阅读(124)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示