乘积小于 K 的子数组怎么用滑动窗口统计?为什么一次能加 right-left+1 个?
简化版
如果数组元素都是正数,子数组乘积会随着右端扩张而变大,随着左端收缩而变小,所以可以用滑动窗口。
维护窗口乘积 product,右指针加入新元素后,如果 product >= k 就不断移动左指针。
当窗口合法时,以 right 结尾的合法子数组数量是 right - left + 1。
详细版
这题的关键条件是所有元素为正数。正数保证窗口右扩时乘积不会下降,左缩时乘积不会上升,因此满足滑动窗口单调性。
流程:
right从左到右遍历,把nums[right]乘进窗口;- 当
product >= k时,持续除掉nums[left]并移动left; - 调整后,窗口
[left, right]内任意以right结尾的后缀子数组乘积都小于k; - 所以答案加
right - left + 1。
如果 k <= 1,由于元素为正整数,不可能有乘积小于 k 的非空子数组,直接返回 0。
完整版教学
一、为什么这题能用滑动窗口
滑动窗口要求某种单调性:右端扩张会让条件更难满足,左端收缩会让条件更容易满足。本题中数组元素为正数,所以乘积乘上新元素不会变小,除掉左端元素不会变大。这个性质让我们可以用双指针维护一个最大合法窗口。
记忆钩子:正数乘积题能滑窗,混入 0 或负数就要重新分析单调性。
二、窗口维护的含义
窗口 [left, right] 表示当前乘积小于 k 的最长后缀窗口。每次右指针加入一个数后,乘积可能超标,于是移动左指针直到重新合法。调整完成后,left 是让窗口合法的最靠左位置。
while (product >= k) {
product /= nums[left]
left++
}
这个循环不是随便缩,而是在恢复不变量:当前窗口乘积必须小于 k。
三、为什么答案加 right-left+1
调整完成后,窗口 [left, right] 的乘积小于 k。由于所有元素为正数,窗口内部任意以 right 结尾的后缀子数组,乘积只会更小或相等。例如合法窗口是 [2,5,3],以右端 3 结尾的子数组有 [3]、[5,3]、[2,5,3],一共 3 个。
| 窗口长度 | 以 right 结尾的合法子数组数 |
|---|---|
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
所以每轮贡献是 right - left + 1。
四、带数字推演
数组 [10,5,2,6],k = 100。
right=0: product=10,合法,+1 -> [10]
right=1: product=50,合法,+2 -> [5], [10,5]
right=2: product=100,不合法,除 10 后 product=10,+2 -> [2], [5,2]
right=3: product=60,合法,+3 -> [6], [2,6], [5,2,6]
总数是 8。
五、代码模板
实现如下:
function numSubarrayProductLessThanK(nums, k) {
if (k <= 1) return 0
let left = 0
let product = 1
let ans = 0
for (let right = 0; right < nums.length; right++) {
product *= nums[right]
while (product >= k) {
product /= nums[left]
left++
}
ans += right - left + 1
}
return ans
}
这里统计的是连续子数组,不是子序列。滑动窗口只适合连续区间。
六、常见误区与追问
- 误区:忘记处理 k <= 1。 正整数乘积至少为 1,不可能小于等于 1 的阈值。
- 误区:窗口合法后只加 1。 以 right 结尾的所有后缀都合法,应该加窗口长度。
- 误区:数组有负数也直接套模板。 负数会破坏乘积单调性,窗口移动不再可靠。
- 追问:为什么不是统计所有窗口长度组合? 每个子数组按右端点唯一归类,每轮只统计以当前 right 结尾的子数组,避免重复。
- 追问:复杂度为什么是 O(n)? left 和 right 都只单调右移,每个元素最多进出窗口一次。
这些问题的核心都是“正数带来的乘积单调性”。
七、加强记忆
这题记成“正数乘积,超标左缩,合法加长度”。右端加入新数让乘积变大,超过 k 就移动左端恢复合法;恢复后,窗口内所有以 right 结尾的后缀都合法,所以一次加 right-left+1。只要看到正数、连续子数组、乘积或和的阈值统计,就可以先想滑动窗口。