切蛋糕(单调队列Monotone queue)

Description

今天是小Z的生日,同学们为他带来了一块蛋糕。这块蛋糕是一个长方体,被用不同色彩分成了N个相同的小块,每小块都有对应的幸运值。

小Z作为寿星,自然希望吃到的第一块蛋糕的幸运值总和最大,但小Z最多又只能吃M小块(M≤N)的蛋糕。

吃东西自然就不想思考了,于是小Z把这个任务扔给了学OI的你,请你帮他从这N小块中找出连续的k块蛋糕(k≤M),使得其上的幸运值最大。

Input

第一行是两个整数N,M。分别代表共有N小块蛋糕,小Z最多只能吃M小块。

第二行用空格隔开的N个整数,第i个整数Pi代表第i小块蛋糕的幸运值。

Output

只有一行,一个整数,为小Z能够得到的最大幸运值。

Sample Input 1

5 2
1 2 3 4 5

Sample Output 1

9

Sample Input 2

6 3
1 -2 3 -4 5 -6

Sample Output 2

5

要计算连续若干块蛋糕幸运值的总和,相当于求区间和,可以使用前缀和,前缀和之差即区间和。

要求出连续小于等于m块蛋糕幸运值总和的最大值,朴素的想法就是:在长度为m的蛋糕区间里求幸运值区间和的最大值,把所有长度为m的蛋糕区间都枚举一遍即可。

如何枚举长度为m的蛋糕区间?朴素的想法是从左往右,一次挪动一个单位。例如:

a1     a2      a3     a4     a5(枚举长度为2的区间)

枚举顺序{a1,a2},{a2,a3},{a3,a4},{a4,a5}

直接求区间和不如求前缀和之差方便,故我们要转换成前缀和的形式,即

sum[0]     sum[1]     sum[2]     sum[3]     sum[4]     sum[5](枚举长度为2+1的区间)

枚举顺序{sum[0],sum[1],sum[2]},{sum[1],sum[2],sum[3]},{sum[2],sum[3],sum[4]},{sum[3],sum[4],sum[5]}

(因为sum[n]表示a1到an的加和,所以前缀和区间长度为m+1时,才能表示连续m块蛋糕幸运值加和,sum[0]表示未添加任何元素,即0)

问题就转换成了如何在长度为m+1的区间里求前缀和之差的最大值。即是在不改变元素顺序下,找到区间内前缀和的最值,并相减。要求区间和,即是求sum[i]-sum[j](i>j),如果我们让每个sum[k](1≤k≤n)都有作为sum[i]的机会,其实只要求sum[i]之前的m个sum[k](i<m时m=i)的最小值就可以了(被减数不变,减数越小,差越大)。

可以使用单调队列解决这道题,用数组和头指针、尾指针简单实现单调队列。我们需要维护前缀和的单调队列——保证从队头到队尾的元素(即前缀和)依次递增,保证队头不在区间内时出队,即可保证队头对应的前缀和始终是sum[i]前m个元素中最小的。我们要讨论的区间和最大值即(sum[i]-队头对应前缀和)。将前缀和从sum[0]开始按下标升序入队,同样按下标升序从sum[1]开始讨论前缀和之差最大值,最终sum[0]到sum[n]每个元素最多入队一次、出队一次,因此时间复杂度为o(n)。为什么sum[0]要入队呢?因为如果没有sum[0]就会减小我们讨论的解集范围,无法讨论第1块蛋糕到第k块蛋糕的幸运值总和(k≤m)。

那么队列中存什么呢?存前缀和的下标,即sum[k]中的k,因为直接存前缀和的值无法对区间进行讨论。

解题思路已经水落石出,不过还有一个细节需要注意:本题队列的初始状态是否为队空状态?——因为全局数组默认初始化元素为0,所以我们可以省略sum[0]的入队,只不过队头和队尾指针初值必须相等,因为队列初态已经有一个元素0,即sum[0]的下标;如果我们不打算省略sum[0]的入队,头指针初值应比尾指针大1,然后手动将sum[0]入队再开始讨论。(头指针>尾指针时队空,队列从头指针所指位置开始存储元素;头指针=尾指针时队列中有一个元素;头指针<尾指针时队列中有(头指针-尾指针+1)个元素)

具体实现见代码。

#include <stdio.h>
#include <algorithm>

int n,m;//总块数,最大连续块数
int data[500001]={0};//幸运值
int sum[500001]={0};//幸运值前缀和
int que[500001]={0};//前缀和单调队列
int head=0,tail=0;//队列头指针、尾指针
int ans=-0x7FFFFFFF;//答案

int main()
{
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",data+i);
        sum[i]=sum[i-1]+data[i];
    }

    //从sum[1]开始讨论前缀和之差最大值
    for(int i=1;i<=n;i++)
    {
        /*最多可连续m块,则前缀和区间的范围是i-m~i,
         *因为sum[i]-sum[i-m]=data[i-m+1]+data[i-m+2]+...+data[i],
         *即连续了i-(i-m+1)+1=m块*/
        //队中有元素且队头不在区间内
        while (head<=tail&&que[head]<i-m)
            head++;//出队
        //更新答案
        ans=std::max(ans,sum[i]-sum[que[head]]);
        //队中有元素且入队元素比队尾小
        while(head<=tail&&sum[i]<=sum[que[tail]])
            tail--;//出队
        que[++tail]=i;//此时入队元素不破坏队列单调性
    }

    printf("%d",ans);
    return 0;
}
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赴星辰大海

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值