Maximum Subarray Sum II最大连续区间和(CSES- P1644)

在处理"连续区间和"问题时,前缀和是我们最常用的武器。但如果题目加上了"区间长度必须在 [l,r] 之间"的死命令,传统的 O(N^2) 暴力枚举就会瞬间超时。今天,我们用单调队列这把尖刀,从"正向"和"逆向"两个极端的视角,把这道极其经典的滑动窗口问题彻底扒光,实现 O(N) 的降维打击。

一、 题目描述

【题目大意】 给你 n 个数 a1​,a2​,...,an​ 和两个数 l,r。你需要在这 n 个数中选出一段连续区间的数,满足区间长度 ∈[l,r]。 求出满足条件的最大连续区间和。

【样例输入】

复制代码
8 1 2
-1 3 -2 5 3 -5 2 2

【样例输出】

复制代码
8

【数据范围】 1≤l≤r≤n≤200000 −10^9≤a[i]​≤10^9

二、 题目分析与解题思路

求区间和的第一反应必须是前缀和 。设S[i]为前i个元素的和。 任意一段以L为起点、R为终点的区间和,可以转化为:S[R]−S[L−1]

既然要让差值最大,我们有两种截然不同的战术思路:

  • 战术一(固定起点,向未来找最高售价): 假设买入成本S[L−1]已固定,我们在合法的未来窗口内,找一个最大的卖出价S[R]。

  • 战术二(固定终点,向历史找最低成本): 假设卖出价S[R]已固定,我们在合法的历史窗口内,找一个最小的买入成本S[L−1]。

这两种战术,对应了两种双指针滑动的写法。

三、 算法设计与思考过程

做法一:固定起点,正向找最大终点

  1. 外层循环: 枚举起点i(也就是公式里的 L),范围是从1到n−l+1。

  2. 侦察兵x: 代表终点R。它从最短合法距离x=L开始,一路向右扫描。

  3. 单调队列维护: 我们要找最大的S[x],所以维护一个单调递减队列。遇到前缀和比自己大、且寿命更长的新人,队尾的老将直接被物理抹杀。

  4. 过期清理: 合法终点至少要达到i+l−1。如果队首的老将距离起点太近,直接踢出队列。

做法二:固定终点,逆向找最小起点

  1. 外层循环: 枚举终点i(公式里的R),采用逆向倒序,从n一路退到L。

  2. 侦察兵 x: 代表被减数的位置(起点的前一位 L−1)。它从最靠右的合法位置x=n−l开始,一路向左退。

  3. 单调队列维护: 我们要找最小的成本S[x],所以维护一个单调递增队列。遇到成本更低的新人,队尾老将出队。

  4. 逆向过期逻辑(全场最核心): 因为我们是从右往左扫描,先入队的老将,其坐标x大于 后入队的新人。当终点 i 不断向左退时,右边的老将就会变成"距离终点太近"的违规品。因此,过期条件是q[front]>i-l。

四、 易错点总结

  1. 极小值初始化: 最大和ma不能初始化为0。如果数组全是负数,答案也会是负数。必须初始化为极小值(如-1e18)。

  2. 循环的精确边界: * 正向写法的起点边界:最大只能走到n-l+1,多一步都走不了。

    • 逆向写法的终点边界:最小只能退到l,退到l−1就凑不够长度l了。
  3. 前缀和的-1障眼法: 单调队列里存的到底是谁?在做法二中,侦察兵x本身代表的就是"起点的前一位",所以结算时直接用 s[i]-s[q[front]] 即可,绝对不能 再去写 s[q[front]-1],否则会发生下标漂移。

五、 时空复杂度分析

  • 时间复杂度:O(N) 。无论是正向还是逆向,外层指针和内层侦察兵 x 都是单向行驶、绝不回头的。每个下标最多入队一次、出队一次,均摊复杂度严格为O(N)。

  • 空间复杂度:O(N)。需要开辟长度为N的前缀和数组S以及单调队列数组q。


六、 完整代码

解法一:固定起点(从左往右扫描,维护递减队列)

cpp 复制代码
//最大连续区间和 先求前缀和
//先求前缀和然后两种做法 1固定起点 找满足区间长度要求值最大终点
//2固定终点,找满足区间长度要求值最小起点
//这里先写第一种做法
#include <iostream>
#include <algorithm>//对应min max函数
using namespace std;
int n,l,r;
long long s[200010];//前缀和数组
int q[200010];//队列
int front=1,rear=0;//队首 队尾
long long ma=-1e18;//初始化最大连续区间和为极小值

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>l>>r;
    //求前缀和数组
    for(int i=1;i<=n;i++){
        int x;
        cin>>x;
        s[i]=s[i-1]+x;
    }
    //x为终点,起点从1开始,终点最小为L才能满足区间长度不小于L
    int x=l;
    //i代表起点,从1开始,最大为n-l+1
    for(int i=1;i<=n-l+1;i++){
        //i+r-1是满足区间长度要求终点最大值,不能超过这个范围
        //n是终点最大值
        while(x<=i+r-1&&x<=n){
        //如果当前队列非空
        //且当前终点的值(x)大于等于队尾的值,队尾元素出队
        //因为队尾元素既不大于x又更老,未来不可能用到
            while(front<=rear&&s[x]>=s[q[rear]]){
                rear--;
            }
            //直到队列为空或x小于队尾元素
            //把x入队
            q[++rear]=x;
            x++;
        }
        //接下来判断队首是否已经过期(不满足区间长度要求)
        //如果已经过期就队首出队
        //i+l-1是终点至少要大于等于这个值,才满足区间长度要求
        //即没过期
        while(q[front]<i+l-1) front++;
        //队首就是区间内最大值
        ma=max(ma,s[q[front]]-s[i-1]);
    }
    cout<<ma;
    return 0;
}

解法二:固定终点(从右往左逆向扫描,维护递增队列)

cpp 复制代码
//最大连续区间和 先求前缀和
//先求前缀和然后两种做法 1固定起点 找满足区间长度要求值最大终点
//2固定终点,找满足区间长度要求值最小起点
//这里写第二种做法
#include <iostream>
using namespace std;
int n,l,r;
long long s[200010];//前缀和数组
int q[200010];//队列 存最小起点
int front=1,rear=0;//队首 队尾
long long ma=-1e18;


int main(){
    cin>>n>>l>>r;
    //求前缀和
    for(int i=1;i<=n;i++){
        int x;
        cin>>x;
        s[i]=s[i-1]+x;
    }
    //起点从n-L+1开始(当终点为n时,起点至多是n-L+1才能满足区间长度要求)
    //但是求终点和起点之间的和,要用终点-(起点前面一位)
    //起点前面一位是n-L
    int x=n-l;
    //i代表终点,从右往左,终点至少要大于等于L才能满足区间长度最小要求
    for(int i=n;i>=l;i--){
        //当起点前一位不小于能取到的最小值且起点满足区间长度要求时
        while(x>=0&&x>=i-r){
            //如果x比队尾更小,就把队尾出队
            //因为老队尾值又大,进栈时间又早,再也不会被后面使用到
            while(front<=rear&&s[x]<=s[q[rear]]){
                rear--;
            }
            //当队列为空或x大于队尾元素时
            q[++rear]=x;
            x--;
        }
        //把过期队首出队
        while(q[front]>i-l) front++;
        ma=max(ma,s[i]-s[q[front]]);
    }
    cout<<ma;
    return 0;
}

相关推荐
小年糕是糕手2 小时前
【C++】string类(三)
开发语言·数据结构·c++·程序人生·算法
胖祥2 小时前
onnx之NodeComputeInfo
开发语言·c++·算法
无限空间之王2 小时前
我让三个 AI 互相竞争进化,两天后它们发明了一个我看不懂的算法
算法
sinat_255487812 小时前
为 System.out 编写我们自己的包装类
java·开发语言·算法
阿Y加油吧3 小时前
力扣打卡——盛最多水的容器、三数之和
算法·leetcode·排序算法
Barkamin3 小时前
快速排序非递归实现
java·算法·排序算法
gihigo19983 小时前
距离角度解耦法的MIMO-OFDM雷达波束形成及优化MATLAB实现
开发语言·算法·matlab
WolfGang0073213 小时前
代码随想录算法训练营 Day12 | 二叉树 part02
算法·深度优先
2401_853576503 小时前
代码自动生成框架
开发语言·c++·算法