题目描述

✅ 713. 乘积小于 K 的子数组

image-20260928224549981

题意分析

统计乘积严格小于 k 的连续非空子数组数量。每个数组元素都是至少为 1 的正整数,因此加入新因子不会让乘积减小,移除左侧因子也不会让乘积增大,可以利用滑动窗口。

题目要求统计所有满足条件的区间,而不是只找最长或最短的一段。固定右端点后,若已经找到最靠左的合法起点,那么从它到右端点之间的所有起点,都能各形成一个合法子数组。

解法:正数乘积滑动窗口

核心思路

[!blue]

用 left、right 表示窗口,用 product 保存其中所有元素的乘积。乘法的初始值为 1。每次右端加入一个数后,如果 product >= k,就不断除去当前左端因子并右移 left,直到严格小于 k。

左边界不需要回退。之前被移除的更早起点,当时已使乘积至少为 k;继续往右加入大于等于一的因子后,只会维持或增大这个乘积,不可能重新合法。因此收缩完成时,left 就是以当前右端点结尾的最早合法起点。

窗口合法后,它的任意非空后缀也合法,因为继续删除若干左侧因子不会使乘积增大。合法起点恰好是 left 到 right,一共有 right - left + 1 个,将这部分一次性加入答案即可。每个子数组只在自己的右端点被处理时计数,不会重复。

当 k <= 1,任何非空正整数子数组的乘积都至少为一,直接返回零。这样后续保证 k > 1,即使某个单独元素太大,窗口也能一直缩到空,此时乘积恢复为 1,循环停止,长度贡献自然为零。

因子等于一时,除掉它不会让乘积下降,但左指针仍在前进,所以必须持续使用循环,而不能只删除一次。左右指针都只向右移动,每个元素最多加入和移出一次。

解题步骤

  1. 若 k <= 1,直接返回 0。
  2. 初始化 left = 0、product = 1、答案为 0。
  3. 每次移动右端点,先将新值乘入 product。
  4. 只要乘积大于等于 k,先除掉 nums[left],再推进左边界。
  5. 将恢复合法后的 right - left + 1 加入答案,遍历结束返回总数。

代码实现

class Solution {
    // 当窗口乘积大于等于 k 时,持续移动左指针并除掉左端元素,直到窗口重新满足条件。
    public int numSubarrayProductLessThanK(int[] nums, int k) {
        // 非空正整数乘积至少一,此时没有合法子数组
        if (k <= 1) {
            return 0;
        }

        int left = 0;
        long product = 1;
        int res = 0;

        for (int right = 0; right < nums.length; right++) {
            product *= nums[right];

            while (product >= k) {
                // 先移除旧左端因子,再推进左边界
                product /= nums[left];
                left++;
            }

            // 合法窗口内每个起点都产生一个以当前右端结尾的子数组
            res += right - left + 1;
        }

        return res;
    }
}
func numSubarrayProductLessThanK(nums []int, k int) int {
    // 当窗口乘积大于等于 k 时,持续移动左指针并除掉左端元素,直到窗口重新满足条件。
    // 非空正整数乘积至少一,此时没有合法子数组
    if k <= 1 {
        return 0
    }

    left := 0
    product := 1
    res := 0

    for right, val := range nums {
        product *= val
        for product >= k {
            // 先移除旧左端因子,再推进左边界
            product /= nums[left]
            left++
        }

        // 合法窗口内每个起点都产生一个以当前右端结尾的子数组
        res += right - left + 1
    }

    return res
}

复杂度分析

  • 时间复杂度:$O(n)$,两端各自最多向前移动 n 次。
  • 空间复杂度:$O(1)$,只维护窗口与计数。

关键点总结

[!green]

  • 移除一不一定让乘积下降,但左指针仍前进,最终能收缩为空。
  • 空窗口乘积为一,当前贡献自然为零。

易错点总结

[!yellow]

  • 严格小于误写为允许相等,会多计边界乘积。
  • 只收缩一次,可能留下仍不合法的窗口。
  • 先移动左下标再除,会除掉错误的因子。

相似题目

题目 难度 关联与区别
209. 长度最小的子数组 中等 同样利用正数建立窗口单调性,原题求最短长度,本题通过合法后缀长度累计数量。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/17160017
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!