[洛谷P1419]寻找段落
从今天开始勤更算法博客,每天都要坚持写算法,不要去管别人怎么想怎么做,坚持做下来一定会有收获。
实数二分,单调队列,问题转换
来看题目描述: 给定一个长度为的序列,为第个元素的价值。现在需要找出长度在区间之内的连续序列,这个连续序列的平均值是最大的。
单调性求最大值问题,可以想到用二分去解决,这很容易就可以想到,但所有二分难题的本身不在二分,而在关键的函数部分。笔者也是看了很多题解,才理解其中的精髓。
一.平均值要怎么处理/Check
既然是二分法,那就直接对平均值二分枚举就是。但问题就在于枚举的每一个平均值,要怎么证明其合理性呢?难道要枚举长度在之中的区间的平均值,挨个检查吗?
这样做,即使利用前缀和也无法阻止超时TLE的悲剧。
思路转换一下,一个区间内的平均值可以看成某个区间的所有数都是平均值,那么当我们对这个区间所有数都减去待检查的avg之后,再对这个区间的总和做一次计算得到sum,如果sum>=0,即说明这个区间的平均值一定满足需求(也就是说,存在一个区间,其平均值大于枚举出的,可以考虑将上调),反之就不能(不存在这样一个区间,平均值能够大于或等于枚举的avg,因此就需要将下调)。 这样做,怎么就好了呢? 想想看,原先我们不仅要计算区间和,还要考虑区间的长度来计算平均值;但是现在不同,我们只需要计算出区间和,就完全够了。
计算区间和,当然要用到前缀和优化时间复杂度了。
二.区间的选择和答案的存在性
上面一直在说某个区间,但是长度在当中的区间到底要怎么选择呢?还是枚举?
如果长度是一个定值,那用单调队列搞就太容易了。但是这题长度也成了一个变量,单调队列就不能用了吗?
不,同样可以,单调队列同样适用在处理长度在区间内的最大值问题,让我们来看看怎么整:
创建一个递增的单调队列,队列元素始终是前缀和数组preSum的索引。为什么是递增的?
计算区间和,我们都是这样干的对不对:
preSum[i]-preSum[j]
那么队首总是指向最小和的队列,在遍历的时候要想计算区间和就是这样的:
preSum[i]-preSum[que[head]]
那么这样得出的区间和就是范围内的最大和。只要存在一个最大和大于等于0,就证明了存在性,可以返回true。
if(preSum[i]-preSum[que[head]]>=0) return true;
区间要怎么去选,我们用不着去枚举。每一次遍历,都将大于队尾的i-S指针加入队列,又将超出i-T边界的队首指针弹出队列。为什么偏偏是i-S,i-T而不直接是i呢? 再来看看我们怎样计算区间和的
preSum[i]-preSum[que[head]]
。当前的i指针,与队首指针的差值d,肯定是满足如下条件:
i-(i-S)<=d<=i-(i-T)
d就是区间长度L,就是在范围内的区间长度! 这样就既可以在优化时间的情况下保证区间长度的合法性,又维护了单调队列,一次遍历就可以满足!
for(int i=1;i<=n;i++){
if(i>=S){
while(head<=rear && preSum[que[rear]]>preSum[i-S]) rear--;
que[++rear]=i-S;
}
if(head<=rear && que[head]<i-T) head++;
if(head<=rear && preSum[i]-preSum[que[head]]>=0) return true;
}
return false;
四.二分
实数二分,没啥好说的,精度控制在1e-5就够了。
while(right-left>eps){
double mid=left+(right-left)/2;
if(Check(mid)) ans=left=mid;
else right=mid;
}
三.结语
二分总是和其他算法结合起来考查,本题实属一道好题。
源代码:
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e5+5;
const double eps=1e-5;
int values[N]={0};
double done[N];
int que[N];//只保存下标的单调队列
double preSum[N];
int n,S,T;
bool Check(double avg){
for(int i=1;i<=n;i++) done[i]=(double)values[i]-avg;//减去平均值,进行一次问题转换
preSum[0]=0;
for(int i=1;i<=n;i++) preSum[i]=preSum[i-1]+done[i];//计算前缀和
int head=1,rear=0;
for(int i=1;i<=n;i++){
if(i>=S){
while(head<=rear && preSum[que[rear]]>preSum[i-S]) rear--;
que[++rear]=i-S;
}
if(head<=rear && que[head]<i-T) head++;
if(head<=rear && preSum[i]-preSum[que[head]]>=0) return true;
}
return false;
}
int main(){
cin>>n;
cin>>S>>T;
for(int i=1;i<=n;i++){
cin>>values[i];
}
double left=-10000,right=10000;
double ans;
while(right-left>eps){
double mid=left+(right-left)/2;
if(Check(mid)) ans=left=mid;
else right=mid;
}
printf("%.3f\n",ans);
return 0;
}