\(AcWing\) \(1087\). 修剪草坪
一、题目描述
给定一个长度为 \(n\) 的数组 \(w\),其中 \(w_i\) 是第 \(i\) 个元素的 贡献
我们可以选择的 数组 中的一些 元素,这些元素的 贡献总和 表示我们该种 方案 的 价值
但是,如果方案中出现选择了 连续相邻 且超过 \(m\) 个元素,则这些 连续相邻 的元素 贡献 归零
求解一种 方案,使得选择的 元素贡献总和 最大
二、题目分析
考虑用 动态规划 来求解本问题
由于 连续选择 超过 \(m\) 个元素时,这些元素的 贡献 为 \(0\) (相当于没选)
而本题,所有的元素值都是 正整数,故我们的方案中,连续选择的元素数量 一定是 不超过\(m\)的
状态表示:
设\(f[i]\)为以\(i\)为结尾的所有数字中,合法(连续长度不超过\(m\)),并且贡献总和最大值。我们来分析一下这个最优状态可能由哪些状态转移而来。
举个栗子,秒懂:
假设\(m=3\),现在讨论一下第\(6\)头牛,它这个位置有两种选择,就是选与不选:
-
不选:那么毫无疑问,由于第\(6\)头没有发挥作用,现在的最大贡献和与前面的最大贡献和一致:\(f[6]=f[6-1]=f[5]\)
-
选择:如果它选了,那么它前面的就不是随意的了,需要有范围限制,我们先用人脑模拟一下:
最大连续选择长度不能超过\(3\),所以讨论以下情况:- 假设本轮取\(3\)个最优:\[\large f[6]=a[6]+a[5]+a[4]+ ? \]
- 假设本轮取\(2\)个最优:\[\large f[6]=a[6]+a[5]+?' \]
- 假设本轮取\(1\)个最优:\[\large f[6]=a[6]+?'' \]
- 假设本轮取\(3\)个最优:
为啥要加\(?\)呢?以\(3\)个为例,如果取\(3\)个最优,那么此时取到了\(4\)号结点位置,\(3\)号肯定是不能取的!原因很简单,如果取上,就是连续\(4\)个了,超过了\(m\)限定!那\(3\)左侧是随意的,这时,\(f[2]\)的含义是在前\(2\)个元素中合法并可以获取到的最大值,现在的情况可以直接表示为\(f[2]\),同理得到:
\(\large f[6]=a[6]+a[5]+a[4]+f[2] \\ \large f[6]=a[6]+a[5]+f[3] \\ \large f[6]=a[6]+f[4]\)
\(a[6]+a[5]+a[4]\)这样的东东,很显然是前缀和的基本表示式,提示我们引入前缀和进行思考。如果预处理了前缀和,那么就有:
利用前缀和思路优化一轮:
\(\large f[6]=s[6]-s[3]+f[2]\)
\(\large f[6]=s[6]-s[4]+f[3]\)
\(\large f[6]=s[6]-s[5]+f[4]\)
通用化处理一下:(\(6\)就是固定的当前值\(i\),而\(3,4,5\)是可变的,我们设为\(j\),同时\(j\)是有范围的,也就是\(i-m \sim i-1\))
在\(i\)确定的情况下,\(s[i]\)是固定的,变化的就是\(f[j-1]-s[j]\),要想求\(f[i]\)最大值,就是求\(f[j-1]-s[j]\)的最大值,而\(j\)是有范围的,这就转化为一个区间内取极值问题,可以用单调队列来优化。
单调队列优化办法
-
我们维护一个队列,记录距离\(i\)前面的\(m\)个区间范*围内,\(f[j-1]-s[j]\)取值最大,就完成了优化
-
这个最优的序号\(j\),一般就是队列头的记录序号\(q[hh]\)
-
因为队列中不包含\(i\)结点,所以需要在\(i\)进入队列前,\(while\)上方进行计算更新结果。
四、实现代码
时间复杂度: \(O(n)\)
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 100010;
typedef long long ll;
int q[N];
ll s[N], f[N];
int main() {
//加快读入
ios::sync_with_stdio(false);
cin.tie(0);
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> s[i], s[i] += s[i - 1];
//由于滑动窗口在i的左边,需要讨论前缀和的s[l-1],第1个没有前序,不符合整体的代码逻辑,添加了一个哨兵
int hh = 0, tt = 0;
for (int i = 1; i <= n; i++) {
// 1、年龄大于m的老家伙们,不管是不是实力够强大,一概去死
while (hh <= tt && i - q[hh] > m) hh++;
//因为滑动窗口在i 左侧,先使用再加入
f[i] = max(f[i - 1], f[max(0, q[hh] - 1)] + s[i] - s[q[hh]]);
// 2、不如我年轻,并且,不如我有实力的老家伙们去死
while (hh <= tt && f[i - 1] - s[i] >= f[max(0, q[tt] - 1)] - s[q[tt]]) tt--;
// 3、i入队列
q[++tt] = i;
}
//输出结果
printf("%lld\n", f[n]);
return 0;
}
五、代码解读
1、代码片段I
f[i] = max(f[i - 1], f[max(0, q[hh] - 1)] + s[i] - s[q[hh]]);
不选\(i\): \(f[i]=f[i-1]\)
选择\(i\):
看一下题解中总结的状态转移方程:\(f[i]=s[i]−s[j]+f[j−1]\)
此时,单调队列\(q[hh]\)中保存的就应该是\(j\)的最优值,将\(j=q[hh]\)代入上式得到
\(f[i]=s[i] -s[q[hh]]+f[q[hh]-1]\)
可以选 也可以 不选,需要取\(max\)
\(f[i] =max(f[i-1],s[i] -s[q[hh]]+f[q[hh]-1])\)
这里有一个小技巧,或者说有一个小坑,就是\(f[q[hh]-1]\)
我们知道,如果\(i=1\),就算我们加了哨兵进来,\(q[hh]=0\),那么\(0-1=-1\),这个是无法用数组下标的。
为什么会出现这个问题呢?我们需要从现实意义出发去思考:
\(f[k]\)的含义:在前\(k\)个元素中,连续长度不超过\(m\)的最大值,如果 \(i=1\)时,\(f[1]=s[1]-s[0]\)
现在的式子是:\(s[i] -s[q[hh]]+f[q[hh]-1] =s[1]-s[0]+f[0-1]\),其实这种情况如果发现是负数,
就直接给\(f[0]\)就行了,为啥直接给\(0\)呢?因为现实含义是:能取\(m\)个就一直向前尝试取\(m\)个,如果不够长,不能创造后再取吧?
即:\(f[max(0,q[hh]-1)]\),如果实在想不明白,也可以拆开写:
if (q[hh] - 1 >= 0)
f[i] = max(f[i - 1], f[q[hh] - 1] + s[i] - s[q[hh]]);
else
f[i] = max(f[i - 1], s[i] - s[q[hh]]);
2、代码片段II
while (hh <= tt && f[i - 1] - s[i] >= f[max(0, q[tt] - 1)] - s[q[tt]]) tt--;
\(Q:\)什么样的老东西需要去死呢?我们要思考一下我们与老家伙们PK的是什么内容:
看一下题解中总结的状态转移方程:\(f[i]=s[i]−s[j]+f[j−1]\)
当\(i\)固定时,\(s[i]\)是定值,\(f[j-1]-s[j]\)是变化的,我们需要看看队列中的每个队尾开始的老家伙们,
是不是这个值比如我小,比我还小,就没有必要继续活下去了。
我是\(i\),我准备入队列,入队列的目的是给下一个\(i+1\)提供数据支撑,对于\(i+1\)而言,我其实就是\(j\)
\(f[j-1]-s[j]==> f[i-1]-s[i]\) 就是\(i\)的本项\(PK\)对比取值
队尾用\(q[tt]\)表示,也代入\(f[j-1]-s[j]=f[q[tt]-1]-s[q[tt]]\)
也就是: \(f[i-1]-s[i] >= f[q[tt]-1]-s[q[tt]]\)的话,老家伙去死~
这里面也有一个小坑坑,就是\(q[tt]-1\),有了上面的经验,我们知道可以直接\(max(0,q[tt]-1)\)即可
六、复盘与总结
- 动态规划的状态表示技巧
设\(f[i]\)为完成了前\(i\)个元素,并且合法,预示结果最大或最小的值。 - 通过举栗子\(m=3,i=6\)来进行具体的思考,总结出一些规律后再进行通用化总结,人总是对具体的数字理解的快,对于抽象的公式理解的慢,我们就通过举栗子的方式来思考。
- 尝试用\(f[j]+xx+yy\)的方式来表示\(f[i]\),来试图找出\(f[]\)数组之间的递推关系。
- 可以尝试是否能用前缀和进行优化,这是一种非常常见的技巧。
- 当讨论到\(i\)是,它的前缀和\(s[i]\)是固定值,我们一般对不确定值进行\(PK\),用单调队列记录离我在一定的距离内,最大或最小值。
- 要注意数组下标的负数情况处理,从现实意义出发,思考负数是无意义的,当出现负数时,用\(0\)替代,即\(max(0,负数)\)
- \(i\)入队列的目的是给下一个\(i+1\)提供支持,\(i+1\)视\(i\)就是\(j\),所以,\(PK\)函数值时,\(i\)直接代入状态转移方程中\(j\)的位置即可。