刷题日记乘积小于K的子数组

Posted 傅耳耳

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了刷题日记乘积小于K的子数组相关的知识,希望对你有一定的参考价值。

乘积小于K的子数组

给定一个正整数数组 nums 和整数 k ,请找出该数组内乘积小于 k 的连续子数组个数

示例 1:

输入: nums = [10,5,2,6], k = 100
输出: 8
解释: 8 个乘积小于 100 的子数组分别为: [10], [5], [2], [6], [10,5], [5,2], [2,6], [5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于100的子数组。

示例 2:

输入: nums = [1,2,3], k = 0
输出: 0

提示:

  • 1 <= nums.length <= 3 * 10^4
  • 1 <= nums[i] <= 1000
  • 0 <= k <= 10^6

题目链接

思路一:前缀和 + 二分

二分查找常用函数

头文件 #include <algorithm>

  • upper_bound(begin, end, num) :返回在 [begin, end) 二分查找的**第一个大于 num** 的数的索引,不存在返回 end
  • lower_bound(begin, end, num) :返回在 [begin, end) 二分查找的**第一个大于等于 num** 的数的索引,不存在返回 end

算法思路:

  • 使用乘积作为二分的依据,会出现 整形溢出,防止溢出,等式两边取对数

    • mul[i+1] = mul[i] * nums[i]; //乘积会整型溢出
      // 等式两边取对数
      log(mul[i+1]) = log(mul[i] * nums[i]) = log(mul[i]) + log(nums[i]);
      // 乘积变为对数和
      // 令 sum[i] = log(mul[i])
      sum[i+1] = sum[i] + log(nums[i]);
      
  • 题目所求为 乘积小于k的子数组,遍历每个数字 nums[i],枚举区间左端点 i ,找最小边界 bound,满足:

    • i <= bound
    • nums[i]*num[i+1]*...*nums[bound] >= ksum[bound] - sum[i-1] >= log(k)
    • 找 第一个boundsum[bound] >= log(k) + sum[i-1] ==> 使用 lower_bound
  • 注意:因为涉及到对数,因此 sum[] 数组使用 double 类型

    double 类型只保证15位小数精确,为防止计算误差:

    1. 题目中 1 <= nums[i] <= 10001 <= nums.length <= 3 * 10^4,可得最大乘积为1000^3*10^4,取对数为 30000*ln(1000)ln(1000)=6.9 ,整数数位最大为6位

    2. 防止计算误差(即15位后省去的小数),小数最少为9位,加上 1e-10

      sum[bound] - sum[i-1] + 1e-10 >= log(k)

    条件:sum[bound] >= log(k) + sum[i -1] - 1e-10

  • 对每个区间左端点 i ,找到的边界 bound,则以下区间均满足条件:

    [i, i], [i, i+1], [i, i+2],..., [i, bound-1]

    bound - 1 - i + 1 = bound - i 个区间

代码:

class Solution 
public:
    int numSubarrayProductLessThanK(vector<int>& nums, int k) 
        int n = nums.size();
        // long long mul[n+1];
        double sum[n+1];
        for(int i = 0;i < n;i ++ ) 
            // mul[i+1] = mul[i] * nums[i]; //乘积会整型溢出
            // 等式两边取对数 log(mul[i+1]) = log(mul[i] * nums[i]) = log(mul[i]) + log(nums[i])
            // 令 sum[i] = log(mul[i])
            sum[i+1] = sum[i] + log(nums[i]);
        

        int cnt = 0;
        for(int i = 1;i <= n;i ++ ) 
            int l = lower_bound(sum + i, sum + n + 1, sum[i-1] + log(k) - 1e-10) - sum;
            cnt += (l - i);
        
        return cnt;
    
;

// 遍历每个i  二分找第一个乘积大于等于k的边界bound 则i...bound-1都满足条件

时间复杂度:O(nlogn)

空间复杂度:O(n)

思路二:滑动窗口

双指针 left, right

算法思路:

  • 窗口右边界逐个递增(迭代)
  • [left, right] 内乘积 prod >= k 时,窗口左边界右移,减小窗口,直到 prod < k 为止
  • 过程中不断叠加统计 [left, right] 内的新增的子区间数量

【统计子区间】

例:10 5 2 6    k = 101

      10  5  2  6
区间1:l,r				[10]							+1
区间2:l   r			[10][5][10,5]					+1+2
区间3:l      r		[10][5][10,5][2][5,2][10,5,2]	+1+2+3
区间4:l         r     10*5*2*6=600 > 101
区间5:    l     r     [6][2,6][5,2,6]					+1+2+3+3
 		  l        r  结束
    
注:
1.对每个区间[l,r]只统计包括nums[r]的子区间 ===> 防止重复
2.对每个满足乘积prod < k的区间统计子区间个数
3.[规律]每个区间增加的子区间个数为 <窗口长度>

代码:

class Solution 
public:
    int numSubarrayProductLessThanK(vector<int>& nums, int k) 
        if(k == 0) return 0;
        
        int n = nums.size();
        int l = 0, r = 0;
        int prod = 1;
        int res = 0;
        
        while(r < n) 
            prod *= nums[r];
            while(l <= r && prod >= k) 
                prod /= nums[l];
                l ++ ;
            
            // 满足 prod < k 统计子区间个数
            if(l <= r) 
                res += (r - l + 1);
                // 加窗口长度
            
            r ++ ;
        
        return res;
    
;

时间复杂度:O(n)
空间复杂度:O(1)

以上是关于刷题日记乘积小于K的子数组的主要内容,如果未能解决你的问题,请参考以下文章

刷题日记乘积小于K的子数组

713. 乘积小于K的子数组

python-leetcode713-双指针乘积小于k的子数组

滑动窗口解乘积小于K的子数组

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

*Leetcode 713. 乘积小于 K 的子数组