最大子序和

GitHub跳转原题关系图返回列表

前缀和把子段和变成两数之差,固定右端点用单调队列维护窗口内最小前缀和,O(n) 求长度不超过 m 的最大子段和。

OJ: roj

题目 ID: 3034

难度:普及+/提高-

标签:

创建: 2026-10-01 11:16

更新: 2026-10-07 13:50

形式化题目

给定一个长度为 nn 的整数序列 a1,a2,…,ana_1, a_2, \ldots, a_n 和整数 mm,要求选出一段连续子段 al..ra_{l..r}(1⩽l⩽r⩽n1 \leqslant l \leqslant r \leqslant n),满足长度 r−l+1⩽mr - l + 1 \leqslant m,最大化子段和 ∑k=lrak\sum_{k=l}^{r} a_k,输出这个最大值。子段长度至少为 11,元素可能为负。

样例:1 -3 5 1 -2 3,m=4m=4,最优子段是 [5, 1, -2, 3],和为 77。

暴力解法

思路

最直接的做法:枚举右端点 rr,再枚举左端点 ll,用前缀和 O(1)O(1) 算出每个子段的和,取最大值。

更具体地,设前缀和 s0=0s_0 = 0,sj=a1+a2+⋯+ajs_j = a_1 + a_2 + \cdots + a_j,那么子段 al..ra_{l..r} 的和就是 sr−sl−1s_r - s_{l-1}。长度不超过 mm 的限制写成下标语言就是 r−(l−1)⩽mr - (l-1) \leqslant m。于是:

answer=max⁡1⩽r⩽n{sr−min⁡r−m⩽i<rsi}\text{answer} = \max_{1 \leqslant r \leqslant n} \left\{ s_r - \min_{r-m \leqslant i < r} s_i \right\}

其中 i=l−1i = l-1。如果对每个 rr 都把窗口里的 mm 个 sis_i 扫一遍求最小值,总时间 O(nm)O(nm)。n=m=3×105n = m = 3 \times 10^5 时约 9×10109 \times 10^{10} 次运算,必然超时。

复杂度

时间 O(nm)O(nm),空间 O(n)O(n)(前缀和数组)。

代码

暴力代码本文不单独给出:它只是推导的台阶,正式提交代码见下方正解的 main.py。

瓶颈

枚举右端点 rr 无法避免,但内层「在滑动窗口 [r−m,r−1][r-m, r-1] 内找 sis_i 的最小值」被重复做了 nn 次。窗口每次只滑动一格:新下标 r−1r-1 进来,旧下标 r−m−1r-m-1 出去。这种「滑动窗口最值」正是单调队列的拿手好戏——不必每次重新扫描整个窗口。

正解

思路

沿用上面的式子,对每个右端点 rr 用单调队列维护窗口 [r−m,r−1][r-m, r-1] 内前缀和最小的下标:

  1. 队列里存的是下标 ii,从队头到队尾下标严格递增,对应的 sis_i 值也严格递增。这样队头永远是窗口内 ss 最小的下标。
  2. 计算右端点 rr 的答案前,先检查队头:若队头下标 <r−m< r - m,它对应的子段长度超过 mm,从队头弹出(可能连续弹多次,不过每个下标一生只被弹一次)。
  3. 用队头更新答案:answer=max⁡(answer, sr−s队头)\text{answer} = \max(\text{answer},\ s_r - s_{\text{队头}})。
  4. 把 rr 入队前,从队尾弹出所有满足 si⩾srs_i \geqslant s_r 的下标。原因:这些下标比 rr 靠左(更早滑出窗口),前缀和又不比 srs_r 小——论「值小」比不过 srs_r,论「活得久」也活不过 rr,后续任何窗口都轮不到它当最小值,可以安全丢弃。
  5. 初始时把 i=0i = 0(即 s0=0s_0 = 0)放进队列,覆盖 r=1r = 1 时「窗口里只有 s0s_0」的情况。

每个下标恰好入队一次、出队至多一次,主循环均摊 O(1)O(1),总复杂度 O(n)O(n)。

下面用样例 1 -3 5 1 -2 3(m=4m = 4)推演一遍。前缀和 s=[0,1,−2,3,4,2,5]s = [0, 1, -2, 3, 4, 2, 5];按代码顺序,每轮先查队头、再取答案、最后队尾弹出并入队 rr:

rr ara_r srs_r 本轮开始时的队列(下标 → ss 值) 队头过期弹出 sr−s队头s_r - s_{\text{队头}} 队尾弹出与入队
1 1 1 [0] → 0 无 1 − 0 = 1 无弹出,入队 1 → [0, 1]
2 −3 −2 [0, 1] → 0, 1 无 −2 − 0 = −2 弹出 1、0(s=1,0⩾−2s=1,0 \geqslant -2),入队 2 → [2]
3 5 3 [2] → −2 无(2⩾r−m=−12 \geqslant r-m=-1) 3 − (−2) = 5 无弹出,入队 3 → [2, 3]
4 1 4 [2, 3] → −2, 3 无 4 − (−2) = 6 无弹出,入队 4 → [2, 3, 4]
5 −2 2 [2, 3, 4] → −2, 3, 4 无(2⩾r−m=12 \geqslant r-m=1) 2 − (−2) = 4 弹出 4、3(s=4,3⩾2s=4,3 \geqslant 2),入队 5 → [2, 5]
6 3 5 [2, 5] → −2, 2 无(2⩾r−m=22 \geqslant r-m=2) 5 − (−2) = 7 无弹出,入队 6 → [2, 5, 6]

观察重点:每一行「队尾弹出与入队」里,被弹出的都是「值不更小、还更早过期」的下标——r=2r=2 时 s2=−2s_2=-2 一口气把比它大且更早的 0 和 1 全部顶掉。到 r=6r=6 时队列剩 [2, 5],对应 ss 值 −2,2-2, 2 严格递增,队头 s2=−2s_2 = -2 正是最优左端点,5−(−2)=75 - (-2) = 7 与样例输出一致。

代码

Python 版:

python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 11:16
# update_at: 2026-10-01 11:16

import sys
from collections import deque
from itertools import accumulate


def max_subsum_within(pre: list[int], m: int) -> int:
    """长度不超过 m 的连续子段的最大和:枚举右端点 j,单调队列给出 [j-m, j-1] 内的最小前缀和。"""
    queue: deque[int] = deque([0])  # 存前缀和下标,队头到队尾的值严格递增,队头即窗口内最小值
    best = pre[1] - pre[0]          # 子段长度至少为 1,先用第一个数兜底
    for j in range(1, len(pre)):
        while queue[0] < j - m:     # 队头下标太老,对应子段长度超过 m
            queue.popleft()
        best = max(best, pre[j] - pre[queue[0]])
        while queue and pre[queue[-1]] >= pre[j]:  # 不比 pre[j] 小的下标永远当不了后面的最小值
            queue.pop()
        queue.append(j)
    return best


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    n = next(data)
    m = next(data)
    nums = [next(data) for _ in range(n)]

    pre = [0, *accumulate(nums)]  # 前缀和:pre[j] - pre[i] 即子段 a[i+1..j] 的和
    print(max_subsum_within(pre, m))


if __name__ == "__main__":
    solve()

C++ 版(同一算法):

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-06 15:13
 * update_at: 2026-10-06 15:13
 */
#include <cstdio>
using namespace std;

typedef long long ll;

const int MAXN = 300005;

ll pre[MAXN];   // pre[i] = a[1] + ... + a[i],前缀和,pre[0] = 0
int q[MAXN];    // 单调队列,存前缀和下标;队头到队尾 pre 值严格递增
int head, tail; // 队列区间为 [head, tail)

int main() {
    int n, m;
    scanf("%d %d", &n, &m);

    for (int i = 1; i <= n; i++) {
        ll x;
        scanf("%lld", &x);
        pre[i] = pre[i - 1] + x;
    }

    ll ans = pre[1] - pre[0]; // 子段长度至少为 1,先用 a[1] 兜底
    head = 0;
    tail = 0;
    q[tail++] = 0; // 初始把 i = 0(pre[0] = 0)放进窗口

    for (int r = 1; r <= n; r++) {
        // 队头下标太老,对应子段长度超过 m,弹出
        while (head < tail && q[head] < r - m) {
            head++;
        }
        // 队头即窗口 [r-m, r-1] 内最小前缀和的下标
        ll cur = pre[r] - pre[q[head]];
        if (cur > ans) {
            ans = cur;
        }
        // 队尾那些前缀和不比 pre[r] 小、又更早过期的下标再也没用,弹出
        while (head < tail && pre[q[tail - 1]] >= pre[r]) {
            tail--;
        }
        q[tail++] = r;
    }

    printf("%lld\n", ans);
    return 0;
}

复杂度

  • 时间 O(n)O(n):每个下标入队、出队各至多一次;前缀和 O(n)O(n)。
  • 空间 O(n)O(n):前缀和数组与单调队列。
  • 实测 n=292332n = 292332 的最大数据点:约 0.07 s0.07\,\mathrm{s}、峰值内存约 40 MB40\,\mathrm{MB},远小于 1 s/128 MB1\,\mathrm{s} / 128\,\mathrm{MB} 限制(Python 侧无 TLE/MLE 风险)。

总结

  • 本题是「带长度上限的最大子段和」:把子段和写成前缀和之差 sj−sis_j - s_i 后,长度限制变成窗口约束 j−m⩽i<jj - m \leqslant i < j。
  • 固定右端点 jj 时只需窗口内的最小前缀和,这是标准的滑动窗口最值问题,用单调队列把内层枚举从 mm 次降到均摊 O(1)O(1) 次。
  • 两个易错点:子段长度至少为 11(不能输出 00);全负序列的答案是最大的单个元素,单调队列做法天然处理。
  • 复杂度 O(n)O(n) 时间、O(n)O(n) 空间,n=3×105n = 3 \times 10^5 轻松通过。