我一直觉得,滑动窗口是算法题里最接近“套路”的那一类:右指针往前扩,左指针在条件不满足时跟着缩,来回一两次扫完数组,答案就出来了。直到一道“求最小子数组和”的题把我卡住——注意,不是“和至少为 target 的最短子数组”,而是直接问所有满足长度约束的连续子数组里,和最小是多少。如果数组里全是正数,这套双指针收缩逻辑勉强还能应付;一旦出现负数,窗口向右扩的时候和反而变小,左指针往右缩的时候和反而变大,两根指针瞬间不知道该听谁的。这篇文章就把这条从简单到变难的完整路径走一遍,顺便把这类题真正背后的工具——前缀和加单调队列——讲清楚,适合正在刷题卡壳、或者想在面试里把滑动窗口变体讲出深度的同学。
1. 别被“滑动窗口”四个字骗了:这道题比看起来难在哪
1.1 基础题与进阶题:一字之差,解法完全不同
最常见的滑动窗口入门题是“长度最小的子数组”:给你一个正整数数组,和一个目标值 target,找出满足总和大于等于 target 的长度最小的连续子数组。这题被收录在 LeetCode 209,解法是双指针,左右指针维护一个窗口,右指针不断吞元素,左指针在满足条件时收缩,最终在 O(n) 时间内跑完。
把题目换成“最小子数组和”之后,意思是完全不同的:给定一个整数数组(注意,可以含负数),以及一个长度下界 k,找出所有长度至少为 k 的连续子数组中,和最小的那一个。名字上只是把“最短”换成“最小”,但大多数人把 209 那套模板套过来之后,会发现要么跑不出正确答案,要么压根不敢写。
原因在于:209 能双指针收敛,是因为它的数组是“正整数”,窗口扩展时总和只增不减;而“最小子数组和”这类题目里,数组一旦引入负数,窗口内部的增减方向不受控,你自然没法判断什么时候该缩左边界。
1.2 窗口收缩的前提:单调性
很多人刷滑动窗口题时会忽略一个前提,那就是“窗口和必须具备单调性”。对于正整数数组,右指针每前进一步,窗口总和一定变大;左指针每收缩一次,窗口总和一定变小。这种单调性给了双指针一个明确的方向:
- 总和不够大,就扩右边界;
- 总和已经够大,就缩左边界找最短。
整个过程像一盘有规则的游戏,每一步都有确定可走的路线。可一旦数组里面出现负数,这个规则就没了。加一个负数,窗口和可能下降;去掉一个负数,窗口和反而上涨。方向不确定,指针的收缩逻辑也就没有依据。
记住这一点特别关键。很多人不是不会写双指针,而是不知道双指针为什么能这么写。理解了单调性,你才能真正理解滑动窗口的适用范围,也才能理解后面“前缀和+单调队列”这套解法为什么能绕过单调性的限制。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先热身:正整数场景下,最小子数组和怎么用滑动窗口做
2.1 从“最短长度”到“最小和”的转换
先看一个相对简单的热身题:正整数数组,长度至少为 k,求所有满足条件的连续子数组里,和的最小值。比如 nums = [3, 2, 5, 1, 4],k = 3,那长度为 3 的窗口有:
- [3, 2, 5] 的和是 10
- [2, 5, 1] 的和是 8
- [5, 1, 4] 的和是 10
最小是 8,对应的子数组是 [2, 5, 1]。
由于数组全是正数,长度越长,窗口和越大,所以“长度至少为 k”在求最小值的时候,其实等价于“长度恰好为 k”。这是一个很关键的简化:我们只需要把一个固定长度为 k 的窗口从数组左端滑到右端,每滑动一步,窗口的组成就发生变化,记录过程中的最小和即可。
这种固定长度的滑动窗口,代码非常简单:
python复制def min_subarray_sum_fixed_k(nums: list[int], k: int) -> int:
if not nums or k <= 0 or k > len(nums):
return 0
cur = sum(nums[:k])
an
