算法日记Day1---【题解C++】[洛谷P1419]寻找段落

148 阅读4分钟

[洛谷P1419]寻找段落

从今天开始勤更算法博客,每天都要坚持写算法,不要去管别人怎么想怎么做,坚持做下来一定会有收获。

实数二分,单调队列,问题转换

来看题目描述: 给定一个长度为LL的序列,a[i]a[i]为第ii个元素的价值。现在需要找出长度LL[S,T][S,T]区间之内的连续序列,这个连续序列的平均值是最大的。

单调性求最大值问题,可以想到用二分去解决,这很容易就可以想到,但所有二分难题的本身不在二分,而在关键的Check()Check()函数部分。笔者也是看了很多题解,才理解其中的精髓。

一.平均值要怎么处理/Check

既然是二分法,那就直接对平均值二分枚举就是。但问题就在于枚举的每一个平均值avgavg,要怎么证明其合理性呢?难道要枚举长度在[S,T][S,T]之中的区间的平均值,挨个检查吗?

这样做,即使利用前缀和也无法阻止超时TLE的悲剧。

思路转换一下,一个区间内的平均值MM可以看成某个区间的所有数都是平均值MM那么当我们对这个区间所有数都减去待检查的avg之后,再对这个区间的总和做一次计算得到sum,如果sum>=0,即说明这个区间的平均值一定满足需求(也就是说,存在一个区间,其平均值大于枚举出的avgavg,可以考虑将avgavg上调),反之就不能(不存在这样一个区间,平均值能够大于或等于枚举的avg,因此就需要将avgavg下调)。 这样做,怎么就好了呢? 想想看,原先我们不仅要计算区间和,还要考虑区间的长度LL来计算平均值;但是现在不同,我们只需要计算出区间和,就完全够了。

计算区间和,当然要用到前缀和优化时间复杂度了。

二.区间的选择和答案的存在性

上面一直在说某个区间,但是长度LL[S,T][S,T]当中的区间到底要怎么选择呢?还是枚举?

如果长度是一个定值,那用单调队列搞就太容易了。但是这题长度也成了一个变量,单调队列就不能用了吗?

不,同样可以,单调队列同样适用在处理长度在[S,T][S,T]区间内的最大值问题,让我们来看看怎么整:

创建一个递增的单调队列,队列元素始终是前缀和数组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;
}