题目描述

✅ LCR 009. 乘积小于 K 的子数组

image-20260928234741639

题意分析

统计正整数数组中乘积严格小于 k 的非空连续子数组数量。按左右端点区分子数组,即使内容相同但位置不同,也分别计数。

元素至少为一,所以扩张区间不会减小乘积,移除左端也不会增大乘积。k <= 1 时任何非空区间都不合法;当前代码通过允许窗口缩空统一处理这一边界。

解法:滑动窗口维护乘积

核心思路

[!blue]

固定右端 j,如果以 i 为左端的窗口乘积已经小于 k,那么从 i 到 j 之间任意更靠右的位置开始,乘积只会更小或相同,也都合法。因此找到最靠左的合法起点后,就能一次计入 j - i + 1 个以当前右端结尾的子数组。

用 s 保存当前窗口乘积,初值为一。加入新元素后,如果乘积达到或超过 k,就持续除掉左端元素并右移 i,直到恢复严格小于,或者窗口已经为空。

为什么左端不用回退?一个起点被排除时,它对应的乘积已经不小于 k;以后右端继续加入至少为一的元素,乘积不会下降,所以这个旧起点仍不可能重新合法。当前左端以前的起点都已被安全排除,之后的起点则都合法,正好形成可以批量计数的连续范围。

先收缩、后计数。若单个元素就超标,或者 k <= 1,窗口可能缩到 i == j + 1,贡献自然为零。空窗口乘积回到一,所以循环还要保留 i <= j 的边界条件,不能只看乘积是否仍超标。

移出的数一定是当前乘积的因子,整除能准确还原剩余窗口。按题目 k <= 10^6、元素不超过一千的范围,收缩后的乘积小于 k,下一次扩张的乘积仍小于 10^9;空窗口扩张也只加入一个元素。代码不会无限累积全数组乘积,Java 使用 long,Go 的计算在本题范围内也能由 32 位整数容纳。

解题步骤

  1. 初始化乘积 s = 1、左端 i = 0、答案为零。
  2. 右端每前进一步,乘入当前元素。
  3. 当窗口非空且乘积不小于 k 时,除掉左端元素并右移左端,必要时连续收缩。
  4. 收缩结束后,将 j - i + 1 加入答案;窗口为空时这一项为零。
  5. 扫描全部右端后返回总数。

代码实现

class Solution {
    public int numSubarrayProductLessThanK(int[] nums, int k) {
        // s 是乘积,初值取乘法单位元 1;用 long 兜住中间的一次乘法。
        long s = 1;
        int answer = 0;

        for (int i = 0, j = 0; j < nums.length; ++j) {
            s *= nums[j];

            // i <= j 这个守卫覆盖 k <= 1 与单个元素超标的情形。
            while (i <= j && s >= k) {
                s /= nums[i++];
            }

            // 收缩完成后 i 才是临界左端点,此时一次性计入 j - i + 1 个子数组。
            answer += j - i + 1;
        }

        return answer;
    }
}
func numSubarrayProductLessThanK(nums []int, k int) int {
    s := 1
    answer, i := 0, 0
    for j, x := range nums {
        s *= x
        for i <= j && s >= k {
            s /= nums[i]
            i++
        }
        answer += j - i + 1
    }
    return answer
}

复杂度分析

设数组长度为 $n$。

  • 时间复杂度:$O(n)$,每个元素至多乘入和除出一次,每个右端只做一次批量计数。
  • 辅助空间复杂度:$O(1)$,只维护指针、乘积和计数。

关键点总结

[!green]

  • 正整数使乘积对扩张不减,已排除的左端不会重新合法。
  • 固定右端后,合法起点是一个连续范围,窗口长度就是新增数量。
  • 当前实现允许缩成空窗口,用边界守卫处理 k <= 1 和单元素超标。
  • 乘积从一开始,每次移出通过精确整除维护。

易错点总结

[!yellow]

  • 条件要求严格小于,乘积等于 k 也必须继续收缩。
  • 乘积不能初始化为零,否则窗口状态永远无法正确恢复。
  • 缩窗只做一次可能仍超标,必须反复判断。
  • 在收缩前计入窗口长度,会把不合法的起点算进答案。
  • 空窗口乘积为一,k <= 1 时仍不满足数值条件,所以不能去掉当前写法的 i <= j 守卫。
  • 输入含零或负数时,精确除法和单调性前提改变,不能直接复用这份正整数窗口。

相似题目

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