区间 是什么
关于动态规划,其实说白了,就是一种递推。当我们解决大问题的时候,先把它 分解 为若干个子问题,再把它 合并 成当前所需的结果。有点类似于递归的感觉,而递归会重复计算,普通 与记忆化搜索不会。
而区间 ,就是这类问题中的一小类。在我们的 分解 操作时,它需要取任意区间的子问题结果。这类问题就叫做区间 。
对于大多数问题,区间 的架构像这样: 被拆分为若干个 (其中 ),通过这些小区间计算出来的 值推算 。 而 的两个下标代表一个区间的左端点与右端点。
具体问题具体分析,我们当然需要结合问题来进行学习。然而,区间 是有“套路”的,我们接下来把一个个花里胡哨的题目,透过现象去看它的本质,然后把它们 归类 为一个个套路。
区间 架构
通常,我们在做此类问题时,是由小区间推到为大区间。所以,我们要先枚举小区间,再枚举大区间,也就是 枚举长度 。我们的 顺序应是这样的:
伪代码:
# 初始化 dp[i][i] 或 dp[i][i-1] 因题而异
for len=start to len=end len++ # 枚举长度,start 和 end 分别为最短与最长的长度
for i=1,j=len to j=n i++,j++ # 枚举对应长度的区间 (i,j)
( for k=i to k=j-1 k++ ) # 可能要枚举中间点
# 进行 dp 操作
还有一种方法也是可以的,这篇文章中“关路灯”类题目也用了这种枚举方法。(感谢金钩爷 @SharpnessV)
首先我们看这种方法为什么不行:
for(int i=1;i<=n;i++)
for(int j=i;j<=n;j++)
// dp
假设我们程序运行到 ,转移到区间 和 的时候, 还没有运行。
我们得出结论:将左端点枚举到 的时候,比 编号大的左端点一定要先枚举到。所以一种新的做法出现了:
for(int i=n;i>=1;i--)
for(int j=i;j<=n;j++)
//dp
我们倒序枚举左端点,就可以保证 顺序正确。
神奇的“套路”们
石子合并类套路
此类问题,是常见的找中间点 分割的问题。
首先,对于一个区间 ,我们把这个区间内所有石子给合并起来的上一步是啥?
- 将区间 之间的所有石子全部合并 (其中 );
- 将区间 之间的所有石子全部合并。
我们将这两次合并的结果相加,再加上 之间所有值的和即可。当然,这个 还得枚举来选择最优的。
得转移方程式:
和例 似乎是一样的,然而它是一个环。我们的做法是 破环为链 ,将这个数列重复两遍,取区间长度为 的最值,这样“环”就被我们考虑到了。
因为这题和上题相似,所以我只提供这题代码。
和上述题目十分相似。切断区间 的 ,如果有 ,那么可以合并。得转移方程式:
比较难的题目了。
在区间 内,我们首先确定这个 的不高兴的值。这玩意可以枚举!
假设取中间点 ,区间 的人进栈,显然 在这些人中最后一个出栈,等了 个人,这些人的值我们已经算好了。区间 的人自然需要傻傻地等待 之间的所有人,就是 个人。这个值要加进去。所以 是最小的 ( 表示 这些人的权值和, 为区间内一个数)。
具体看本题提供的代码。
数组要记录区间左右端点的染色情况。
如果左右端点是一对匹配的括号,那么推到 ,再分类讨论。
否则,推到 和 ,其中 是与 匹配的括号。再分类讨论。具体的转移方程不是特别难,根据题意可以自行推导。
提供本题代码。
取一端/两端套路
这题我是真不知道归 还是归类 ,其实都有。
它和石子合并大致相同。可以枚举 中间点 ,得 。但,要注意,如果左端点与右端点括号匹配,长度可以是抹掉左右端点的值加上 ,因为这对括号可以用了。特判一下即可。
取一端的经典问题。从小区间推向大区间。转移方程 会转移到 或者 。也可以是大区间推向小区间: 和 。本题以大区间推向小区间来讲。
问题如何知道一个数字取得时候是第几个取的。如果取的是第 个,则是区间外的数量,即 。
注意这题需要做 次 ,并且需要高精度/__int128
。
也是取一端的经典问题。但是这题要考虑两种情况,一种是一个区间内最后插入左端点,一个是最后插入右端点。当然一个转移为 ,一个转移为 。在转移过去的时候,左/右端点在符合条件的情况下均可以,需要判断。
非常古老的问题了。
如果区间 的左端点与右端点相同,直接推锅给 。
否则分四种情况:
- 在 前插入 ,那么推锅给
- 删掉 ,推锅给
- 在 后面插入 ,那么推锅给
- 删掉 ,推锅给 。
计算一下每次加上的权值,然后就做完了。
恰,很有意思嘛。
首先贪心肯定是不对滴。 :
4
1 6 1000000 5
我们对于每一个区间 开两个数组,一个记录 当前小明取的情况,一个记录 当前小华取的情况 。值是小明能够得到的最多的和。然后分类讨论:
小明则希望自己能得多的,于是在 和 之间取最大值。注意这里的 是小华取的情况。
小华则希望小明得的少,于是在 与 里面取最小值。注意这里的 是小明取的情况。
因为分小华和小明,所以我们要分两个 数组。
关路灯套路
分两种情况:老王在左端点;老王在右端点。转移到的区间决定于老王在的位置。老王这么做显然会让区间外以及现在要关的路灯都等相应的时间。
设 分别为老王在 处,关掉 之间的灯所需最小耗能。转移方程我则以代码形式表示:
f[i][j][0]=min(f[i][j][0],f[i+1][j][0]+(a[i+1]-a[i])*(sum[i]+sum[n]-sum[j]));
f[i][j][0]=min(f[i][j][0],f[i+1][j][1]+(a[j]-a[i])*(sum[i]+sum[n]-sum[j]));
f[i][j][1]=min(f[i][j][1],f[i][j-1][1]+(a[j]-a[j-1])*(sum[i-1]+sum[n]-sum[j-1]))
f[i][j][1]=min(f[i][j][1],f[i][j-1][0]+(a[j]-a[i])*(sum[i-1]+sum[n]-sum[j-1]));
其中,, 则为功率, 为位置。
- 例 ZOJ 3469
和上一题思路一模一样!Code
不给了。
也是关路灯类问题。注意:这题的起点可以是任意位置哦!
设 表示取掉了 之间的零食获得的最大权值。注意我们倒推做, 是第 个零食最后一个拿,长度为 的区间的 是这两个零食最后拿的最大全职。
对于长度为 的区间 ,我们可以求得左/右端点是第 个取的。这样就很好计算了。
“找对象”套路
(相信你现在可以自己推导转移方程了!)
先解释一下:这类题目与三元组有关,所以我们可以给端点 找一个合适的 组成三元组。
对于区间 ,我们找一个点 与其组成三角形,计算出结果,然后讲区间划分为 与 (画图试一下)。然后就是合并果子了?!
注意:需要使用 __int128
。
表示删掉 之间的区间的数字的最大获得权值。我们找到和 一起删掉的 。
首先要删掉 ,然后删掉 (两个 值加上),再加上 ,就是当前 所得到的权值,枚举 即可。 是中间的切断点。
肥肠简单,和上一题一样。还是有些代码细节,所以提供本题代码。
涂色套路
如果左右端点相同,涂 的时候可以顺带涂一下 ;涂 的时候可以顺带涂一下 。也就是,一个端点可以忽略。否则,枚举中转点涂。
先考虑空串变为 串。对于区间 ,如果有中转点 ,它的字母等于 ,那么涂 的时候可以顺带涂一下 。区间 再考虑。
但是在 串的基础上就麻烦了。对于一个本来就匹配的字母,我们可刷可不刷。具体的操作,我们可以用 表示前 个字母转换的值。如果 , 可以是 ;否则仅仅可以是 。
欢迎大家在 这里 提交。
看着不像涂色,可是它就是涂色!
首先,对于区间 ,我们找到要穿的衣服与 相同的 。我们先转移到区间 ,这个值先加上。要注意:此时穿着的衣服是第 次演出所需衣服,和 相同。我们再加上区间 。为啥是 ?因为 的衣服已经被 准备好了,到时候不断脱衣服直到 露出来。
这篇文章结束了!这是博客版文档,所有参考代码请在 这里 下载,或者登录洛谷,在 云剪切板 中查看。
PS:
- 求区间 DP 好题,本文章会不断更新!