news 2026/10/9 5:58:36

洛谷P1614题解:前缀和与滑动窗口求最小连续子段和

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
洛谷P1614题解:前缀和与滑动窗口求最小连续子段和

昨天有朋友私信问我洛谷 P1614“爱与愁的心痛”这道题,说样例能过,但交上去总是 WA,心态有点崩。我点开题目一看,这不就是一道很典型的“固定长度连续子段和”问题吗?对于刚学数组和循环的初学者来说,它是非常友好的入门题;但另一方面,它又把“枚举连续区间”这个基础操作考得很细。如果你只是照着别人的代码抄一遍,可能永远不知道里面那几个边界条件为什么那么写。这篇文章我就把这道题的完整拆解、解题思路、代码实现、常见误区一次讲清楚,顺便往后延伸一下,让你以后遇到类似的题也能举一反三。

1. 题目到底在说什么

1.1 题意拆解:别被“心痛”带偏了

题目背景讲的是爱与愁大神出了一道“简单题”,实际上题目结构很短:先给两个整数 n 和 m,然后给 n 个数字,要求在这 n 个数字里,找出连续 m 个数,使得这 m 个数的和最小,最后输出这个最小的和。

注意几个关键词:

  • 连续:不是随便挑 m 个数,而是下标必须相邻。比如序列3 1 4 1 5,m=2 时,连续的两个数可以是(3,1)、(1,4)、(4,1)、(1,5),而不是把任意两个数加起来比大小。

  • m 个数:窗口长度固定,不能多也不能少。这一点很容易被初学者忽略,有人会下意识觉得“找几个最小的数加起来就行”,那叫“贪心选数”,和本题完全不是一回事。

  • 最小和:对每个可能的连续段求和,再在所有和中取最小值。

这题本质上是:在一个长度为 n 的数组上,滑动一个长度为 m 的窗口,计算每个窗口内的元素和,求最小值。

1.2 用一个具体例子理解题目

假设输入是:

5 3 1 2 3 4 5

n=5,m=3,数组是[1, 2, 3, 4, 5]。长度为 3 的连续子段有:

  • 下标 0~2:1 + 2 + 3 = 6
  • 下标 1~3:2 + 3 + 4 = 9
  • 下标 2~4:3 + 4 + 5 = 12

所有连续三段的和分别是 6、9、12,最小值是 6,所以输出 6。

如果题目改成“求最小值”而不是“求最小和”,那结果会完全不同。这里务必看清楚输出格式:输出的是整数,说明求的就是最小和,不是平均值,也不是最小值。

2. 从暴力到高效:两种思路的取舍

2.1 暴力枚举:最直观,也最容易出错

刚学循环的同学,看到这道题的第一反应通常是两层循环:外层遍历起始位置 i,内层从 i 加到 i+m-1,统计和,然后打擂台求最小值。

#include <bits/stdc++.h> using namespace std; int main() { int n, m; cin >> n >> m; vector<int> a(n); for (int i = 0; i < n; i++) { cin >> a[i]; } int ans = INT_MAX; for (int i = 0; i + m - 1 < n; i++) { int sum = 0; for (int j = i; j < i + m; j++) { sum += a[j]; } ans = min(ans, sum); } cout << ans << endl; return 0; }

这段代码能 AC 吗?在 P1614 这道题的数据范围下,大概率能过。因为题目的 n 范围通常只有 3000 左右,m 也最多是 n,两层循环最坏情况下是 O(n*m),也就是大概 900 万次操作,对于 C++ 来说完全没压力。

但我不建议你只满足于暴力,因为这里有两个隐患:

  • 如果 n 变成 10^5,m 变成 10^5,暴力就直接超时了。很多 OJ 题的数据范围会比洛谷原题更大,只会暴力的话,遇到数据加强版就完蛋。

  • 内层循环每次都从 i 重新累加,这里面存在大量重复计算。比如已经算过a[i]~a[i+m-1]的和,下一个窗口a[i+1]~a[i+m]跟上一个窗口只差一个头和一个尾,理论上可以复用。

暴力不是错,但我们要从暴力里看到优化的机会,这才是刷题的意义。

2.2 前缀和:把区间和变成两次减法

前缀和是处理“静态数组区间求和”问题的经典武器。

思路很简单:额外开一个数组pre,其中pre[i]表示原数组前 i 个元素的和,也就是:

pre[0] = 0 pre[i] = a[0] + a[1] + ... + a[i-1] = pre[i-1] + a[i-1]

这样,区间[l, r)(左闭右开)的和就可以用pre[r] - pre[l]在 O(1) 时间内算出来。

对应到本题,固定窗口长度 m,我们要枚举所有长度为 m 的区间。如果当前区间是[i, i+m),那么它的和就是:

sum(i, i+m) = pre[i+m] - pre[i]

所有可能的 i 从 0 到 n-m,取最小值即可。这样只需要 O(n) 时间计算前缀和,再 O(n) 时间枚举窗口,总复杂度 O(n)。

这里有个看似不起眼但很关键的点:前缀和数组的下标含义。如果你定义pre[i]是“前 i 个数的和”,那么下标从 0 到 n;数组 a 的下标从 0 到 n-1。求a[i]到a[i+m-1]的和时,千万别写成pre[i+m-1] - pre[i],正确的写法是pre[i+m] - pre[i]。因为pre[i+m]包含的是从a[0]到a[i+m-1]的所有元素,减掉pre[i](包含a[0]到a[i-1])之后,剩下的正好是从a[i]到a[i+m-1]这 m 个数。

很多新手在这个下标上翻车,样例能过是因为恰好窗口从 0 开始,后续窗口一多就乱套了。

2.3 为什么不能排序后取前 m 个

这个问题我问过不少初学者,他们的想法是:既然要求 m 个数的和最小,那把数组从小到大排序,取前 m 个最小的数加起来不就完了?

听上去很美,但题目有一个前提是“连续”。排序会破坏元素原来的相对顺序,排序后取到的那 m 个数在原数组里往往根本不相邻。比如:

1 100 2 99 3

m=2,排序后前两个数是 1 和 2,它们的和是 3,但它们在原数组里不相邻,所以不能这样取。真正的连续段有(1,100)、(100,2)、(2,99)、(99,3),和分别是 101、102、101、102,最小是 101。两者差距非常大。

所以遇到“连续子数组/子段”问题,第一反应应该是枚举区间端点,或者用滑动窗口,而不是排序。这个思维方式如果不纠正,后面学“最大子段和”“最长连续子序列”等问题时会一直踩坑。

3. 代码实现与细节打磨

3.1 前缀和版本的完整代码

我直接把前缀和版本的代码写出来,并加上注释,这是最容易 AC 的写法:

#include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; vector<int> pre(n + 1, 0); for (int i = 1; i <= n; i++) { int x; cin >> x; pre[i] = pre[i - 1] + x; } int ans = INT_MAX; // 枚举长度为 m 的区间 [i, i+m),右端点(不含)最大为 n for (int i = 0; i + m <= n; i++) { int sum = pre[i + m] - pre[i]; ans = min(ans, sum); } cout << ans << '\n'; return 0; }

这段代码里,我让pre[k]表示前 k 个数的和,所以读入时直接累加到pre[i]。注意循环读入时,下标从 1 开始,这样可以天然对齐pre[0]=0的设定。

然后枚举窗口的起点 i,范围是0到n-m,也就是i + m <= n。为什么不是i < n-m?因为当i = n-m时,窗口是[n-m, n),正好到数组末尾,是合法的最后一个窗口。如果写成i < n-m,就会漏掉最后一个窗口,直接 WA。

这就是典型的边界问题,建议每次写这种枚举区间时,先在草稿纸上把小例子画一下。比如 n=5, m=3,i 能取 0,1,2,对应窗口[0,3)、[1,4)、[2,5),所以条件是i + m <= n,而不是i + m < n。

3.2 滑动窗口版本:另一种优雅写法

前缀和适合“静态区间查询”,而本题因为窗口是连续滑动的,也可以用滑动窗口的思路:

  • 先计算第一个窗口a[0]到a[m-1]的和sum。
  • 然后每次窗口右移一步:减去离开窗口的元素a[i-m](其实是 a[i-1]?我们看实现),再加上进入窗口的新元素a[i]。
  • 每次更新最小值。

实现如下:

#include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; vector<int> a(n); for (int i = 0; i < n; i++) cin >> a[i]; int sum = 0; for (int i = 0; i < m; i++) sum += a[i]; int ans = sum; // 窗口现在覆盖 [0, m-1],下一次滑动到 [1, m] for (int i = m; i < n; i++) { sum -= a[i - m]; // 移除窗口最左边的元素 sum += a[i]; // 加入窗口最右边的元素 ans = min(ans, sum); } cout << ans << '\n'; return 0; }

这个版本的优点是空间复杂度是 O(1)(除了存数组),不需要额外的前缀和数组。而且它更直观地体现了“窗口在滑动”这个过程:每移动一步,只变化两个元素。

两种写法的本质是一样的,前缀和适合处理“多次询问任意区间和”的场景,滑动窗口适合“窗口长度固定且连续滑动”的场景。这道题用哪个都行,但我个人更喜欢滑动窗口版,因为它逼着你理解窗口移动的每一个细节,而不是机械地套前缀和公式。

3.3 下标越界与数据类型这些坑

有些同学写的暴力版会这样写:

for (int i = 0; i < n; i++) { int sum = 0; for (int j = i; j < i + m; j++) { sum += a[j]; } }

如果 i 取到 n-1,m 又是 2,内层循环会访问a[n]、a[n+1],直接越界。所以暴力的外层循环一定要保证i + m - 1 < n。这是个死条件,写代码的时候要在脑子里模拟一个最靠后的窗口。

还有数据类型的坑。P1614 的 n 只有几千,数字也不大,int 就够了。但如果你以后遇到 n 是 10^5、数字是 10^9 的题,前缀和累加可能会超过 int 范围。比如10^5 * 10^9 = 10^14,必须用long long。所以我在写前缀和类题目时,习惯性用long long定义pre和ans,避免数据一加强就溢出。这道题虽然 int 能过,但养成用 long long 的习惯不是坏事。

另一个小坑是INT_MAX作为初始值的问题。INT_MAX在<climits>里定义,bits/stdc++.h已经包含了它,所以能用。如果你不用万能头文件,记得引入<climits>。当然也可以用pre[n]或者a[0]之类的值来初始化ans,但INT_MAX是最保险的。

4. 踩坑记录与常见问题排查

4.1 样例过了但 WA?先检查枚举范围

我见过太多同学样例过了但提交 WA,然后跑来问为什么。绝大多数情况是枚举范围的边界条件错了。

最容易错的地方有两个:

  • 前缀和写法中,枚举起点 i 写成了i < n。这会导致窗口右端点i + m超出前缀和数组的有效范围,读到的pre[i+m]是未定义值,或者数组越界。

  • 暴力写法中,外层循环写了i <= n - m或i < n - m + 1,这些写法如果没想清楚,很容易差 1。我推荐直接使用for (int i = 0; i + m <= n; i++),这个条件读起来就是“窗口起点加上窗口长度不超过数组长度”,含义最清晰,几乎不会错。

下次再遇到类似题,先写一句注释:// 枚举所有合法窗口 [i, i+m),然后问自己:i 最大能取多少?取到最大值时窗口右端在哪?这样边界就不会含糊。

4.2 用 cin/cout 会超时吗?

有些同学听说 C++ 的cin很慢,于是纠结要不要用scanf。实际上 P1614 的数据量非常小,n 只有 3000 左右,用cin完全没问题。但在其他题里,如果输入量到了 10^5 甚至 10^6,cin默认的同步机制确实会拖慢速度。

我的习惯是写一行:

ios::sync_with_stdio(false); cin.tie(nullptr);

然后放心大胆地继续用cin、cout。这样能让cin的速度接近scanf,又保持书写方便。如果你不想记这两行,那直接用scanf和printf也行,只是要注意格式控制符,别写串了。

还有一个输出细节:cout << ans << endl;中的endl会额外刷新输出缓冲区,在大量输出时较慢。这里用'\n'就好,养成习惯。

4.3 m=n 的边界情况

如果 m 等于 n,意味着只有一个长度为 n 的窗口,也就是整个数组的和。此时:

  • 前缀和写法:i=0,sum = pre[n] - pre[0] = pre[n],正确。
  • 滑动窗口写法:先算了a[0]~a[n-1]的和,然后循环for (int i = m; i < n; i++),因为 m=n,循环一次都不执行,ans 就是整个数组的和,正确。

但如果是暴力写法,外层循环条件i + m <= n在 m=n 时只有 i=0 满足,也是正确的。所以只要边界条件写对,m=n 不会出问题。我单独提这个是因为有些同学担心“窗口只有一个,是不是要特判”,其实不需要,但你要明白代码为什么能正确处理这种情况。

另外,如果 m 大于 n,这道题的输入不会出现这种情况,因为题目保证 m<=n。但自己写代码时可以做一层防御:

if (m > n) return 0;

不过 OJ 不会给你这种非法数据,加不加都行。

5. 题目变形与扩展思考

5.1 从“固定窗口”到“不定窗口”的扩展

P1614 是固定窗口长度 m,求最小和。如果题目改成:求长度不超过m 的连续子段的最小和,难度就上了一个台阶。因为窗口长度不固定,你枚举子段的起点和终点就变成了 O(n^2)。

更常见的变形是求最大和,比如经典的“最大子段和”问题,可以用 Kadane 算法在 O(n) 时间内解决。它的核心思想是动态规划:维护以当前位置结尾的最大子段和,然后取全局最大。

如果把 P1614 的条件改一改,变成“求长度至少为 m 的连续子段的最大平均值”,那就是另一个经典问题了,通常可以用二分答案加前缀和来做。但这些都是后话,先把手上的固定窗口问题吃透,后面才好往这些方向扩展。

5.2 如果数据范围变大,该怎么继续优化

假设 n 变成 10^6,m 还是固定长度,前缀和依然可以 O(n) 解决,所以前缀和是最稳的方案。

假设 n 变成 10^6,而且有 q 次询问,每次给一个不同的 m,问你最小窗口和是多少。这时候每次重新枚举一遍是 O(nq),可能超时。可以先把所有窗口的和算出来,存到一个数组里,然后用数据结构(比如线段树、ST 表)回答区间最小值问题。但这已经严重超出 P1614 的范围了,不展开。

我想强调的是:一道入门题并不是孤立的知识点,它背后是“前缀和”“滑动窗口”这两个高频工具。你在入门阶段就把它们练熟,后面遇到“和为 k 的子数组数量”“最长无重复子串”等问题时会轻松很多。

5.3 和同类题目的横向对比

洛谷上有很多和 P1614 类似的题,比如:

  • P1046 [NOIP2005 普及组] 陶陶摘苹果:完全就是数组遍历,比 P1614 更基础。
  • P1428 小鱼比可爱:需要对于每个元素往前统计,暴力能过,但可以用树状数组优化。
  • P1047 [NOIP2005 普及组] 校门外的树:区间覆盖问题,常用来练差分。
  • P1048 [NOIP2005 普及组] 采药:0-1 背包,和前缀和没关系,但也是那个年代很多人的入门题。

这些题放在一起看,你会发现 P1614 的特殊之处在于它把“区间”这个概念引入了。从这题开始,你就不再是单纯地“遍历数组”,而是开始关注“连续的一段”。这个思维转变是算法学习路上的一个里程碑。

所以我不建议刷完 P1614 就立刻去刷很难的题,而是可以找几道同样是滑动窗口或前缀和的简单题做做,比如:

  • 给定数组和 k,求所有长度为 k 的子数组的平均值(LeetCode 643)。
  • 给定数组和 k,求长度为 k 的连续子数组的最大和(LeetCode 2461 等)。

做完这些,你对“窗口”和“前缀和”的理解会扎实很多。

6. 最后说几句我自己的习惯

这道题我每次讲给新手,都会强调一件事:不要背诵模板,而是理解“为什么这个边界要这么写”。你把i + m <= n这个条件亲手推导一遍,把pre[i+m] - pre[i]这个式子用具体数字代一遍,之后再遇到类似题目,你根本不需要刻意记代码,手会很自然地写对。

我自己现在刷题时,遇到连续子段问题,第一反应永远是:窗口长度是否固定?如果固定,滑动窗口;如果不固定,考虑前缀和配合其他算法。这个分类习惯帮我避免了很多无效思考。

还有一个实用的调试技巧:如果答案不对,可以先在代码里把每个窗口的和打印出来,肉眼核对。比如 n=5, m=3,你打印出每个窗口的和,看看是不是 6、9、12,然后确认最小值是不是 6。如果打印出来不对,通常就是窗口范围写错了。等确认逻辑没问题,再删掉调试输出。

另外,不要在ans初始值上偷懒。有人喜欢写成int ans = 0;,但如果所有窗口和都是正数,那答案永远是 0,显然不对。初始化成INT_MAX或者第一个窗口的和是最稳妥的。

最后说个小技巧:提交代码前,把输入里 m=1 和 m=n 这两种极端情况测试一下。m=1 时答案是数组的最小值;m=n 时答案是整个数组的和。这两个极端情况能快速暴露很多边界 bug。我就是靠这个习惯,避免了无数次 WA。

刷题这事,入门题练的是习惯和细节。P1614 虽然只是普及- 难度,但如果你能把它讲给同桌听,让同桌听懂,那你对这一类题的理解就真的到位了。希望这篇拆解对你有所帮助。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 5:58:24

Eclipse视图机制完全指南:从透视图到自定义ViewPart开发

用过 Eclipse 的人大概都经历过这么一幕&#xff1a;同事帮我远程看问题&#xff0c;上来第一句就是“你那边的 Console 在哪”、“Outline 怎么关掉了”、“把 Problems 扯出来看看”。结果我盯着 IDE 找了半天&#xff0c;愣是没找到那个传说中的面板。后来我才意识到&#x…

作者头像 李华
网站建设 2026/10/9 5:58:23

微信小程序+Flask顶岗实习管理系统开发实战

七月初那阵子&#xff0c;我带的计算机专业学生刚结束顶岗实习。收上来的《实习鉴定表》&#xff0c;有交Word的、有交PDF的、有直接微信发截图的&#xff0c;50多个学生&#xff0c;文件夹里已经分不清哪个是最新版。当时我就想&#xff0c;这种场景再传统线下管下去&#xff…

作者头像 李华
网站建设 2026/10/9 5:58:21

Android Q非Go版Launcher3 QSB搜索框移除与定制实战

1. 从 Launcher 里那个"甩不掉的搜索框"说起如果你做过 Android 系统定制或者 Launcher 相关的开发&#xff0c;大概率遇到过这样一个需求&#xff1a;客户或者产品经理指着桌面顶部那个搜索框说&#xff0c;"这个能不能去掉&#xff1f;"或者"这个能…

作者头像 李华
网站建设 2026/10/9 5:57:20

麒麟659适配鸿蒙OS:架构本质与分布式落地实践

1. 项目概述&#xff1a;麒麟659的真实定位&#xff0c;不是参数对比游戏&#xff0c;而是生态适配的起点“麒麟659处理器相当于高通哪款&#xff1f;”——这个问题在鸿蒙OS生态快速铺开的当下&#xff0c;频繁出现在开发者论坛、二手手机交易群和新手刷机社区里。它表面是个芯…

作者头像 李华
网站建设 2026/10/9 5:57:16

基于MATLAB的GARCH-EVT-Copula组合CVaR计量全流程

做组合级别的市场风险计量&#xff0c;最绕不开的模型组合就是“波动率估计 相关结构建模 尾部风险度量”。单一资产的VaR好算&#xff0c;历史模拟、参数法都能凑合&#xff0c;但一旦要算组合的CVaR&#xff0c;问题就来了&#xff1a;资产之间的相关结构怎么定&#xff1f…

作者头像 李华