前缀和把子段和变成两数之差,固定右端点用单调队列维护窗口内最小前缀和,O(n) 求长度不超过 m 的最大子段和。
OJ: roj
题目 ID: 3034
难度:普及+/提高-
标签:
创建: 2026-10-01 11:16
更新: 2026-10-07 13:50
形式化题目
给定一个长度为
样例:1 -3 5 1 -2 3,[5, 1, -2, 3],和为
暴力解法
思路
最直接的做法:枚举右端点
更具体地,设前缀和
其中
复杂度
时间
代码
暴力代码本文不单独给出:它只是推导的台阶,正式提交代码见下方正解的 main.py。
瓶颈
枚举右端点
正解
思路
沿用上面的式子,对每个右端点
- 队列里存的是下标
,从队头到队尾下标严格递增,对应的 值也严格递增。这样队头永远是窗口内 最小的下标。 - 计算右端点
的答案前,先检查队头:若队头下标 ,它对应的子段长度超过 ,从队头弹出(可能连续弹多次,不过每个下标一生只被弹一次)。 - 用队头更新答案:
。 - 把
入队前,从队尾弹出所有满足 的下标。原因:这些下标比 靠左(更早滑出窗口),前缀和又不比 小——论「值小」比不过 ,论「活得久」也活不过 ,后续任何窗口都轮不到它当最小值,可以安全丢弃。 - 初始时把
(即 )放进队列,覆盖 时「窗口里只有 」的情况。
每个下标恰好入队一次、出队至多一次,主循环均摊
下面用样例 1 -3 5 1 -2 3(
| 本轮开始时的队列(下标 → |
队头过期弹出 | 队尾弹出与入队 | ||||
|---|---|---|---|---|---|---|
| 1 | 1 | 1 | [0] → 0 | 无 | 1 − 0 = 1 | 无弹出,入队 1 → [0, 1] |
| 2 | −3 | −2 | [0, 1] → 0, 1 | 无 | −2 − 0 = −2 | 弹出 1、0( |
| 3 | 5 | 3 | [2] → −2 | 无( |
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 − (−2) = 4 | 弹出 4、3( |
| 6 | 3 | 5 | [2, 5] → −2, 2 | 无( |
5 − (−2) = 7 | 无弹出,入队 6 → [2, 5, 6] |
观察重点:每一行「队尾弹出与入队」里,被弹出的都是「值不更小、还更早过期」的下标——
代码
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++ 版(同一算法):
/**
* 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;
}复杂度
- 时间
:每个下标入队、出队各至多一次;前缀和 。 - 空间
:前缀和数组与单调队列。 - 实测
的最大数据点:约 、峰值内存约 ,远小于 限制(Python 侧无 TLE/MLE 风险)。
总结
- 本题是「带长度上限的最大子段和」:把子段和写成前缀和之差
后,长度限制变成窗口约束 。 - 固定右端点
时只需窗口内的最小前缀和,这是标准的滑动窗口最值问题,用单调队列把内层枚举从 次降到均摊 次。 - 两个易错点:子段长度至少为
(不能输出 );全负序列的答案是最大的单个元素,单调队列做法天然处理。 - 复杂度
时间、 空间, 轻松通过。