滑窗_leetcode.713.乘积小于K的子数组

题目

Your are given an array of positive integers nums.

Count and print the number of (contiguous) subarrays where the product of all the elements in the subarray is less than k.

Example 1:
Input: nums = [10, 5, 2, 6], k = 100
Output: 8
Explanation: The 8 subarrays that have product less than 100 are: [10], [5], [2], [6], [10, 5], [5, 2], [2, 6], [5, 2, 6].
Note that [10, 5, 2] is not included as the product of 100 is not strictly less than k.
Note:

0 < nums.length <= 50000.
0 < nums[i] < 1000.
0 <= k < 10^6.

译:

给定一个正整数数组 nums。

找出该数组内乘积小于 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的子数组。
说明:

0 < nums.length <= 50000
0 < nums[i] < 1000
0 <= k < 10^6

解法一:二分查找

分析

我们可以使用二分查找解决这道题目,即对于固定的 i,二分查找出最大的 j 满足 nums[i] 到 nums[j] 的乘积小于 k。但由于乘积可能会非常大(在最坏情况下会达到 100 0 50000 1000^{50000} ),会导致数值溢出,因此我们需要对nums 数组取对数,将乘法转换为加法,即 l o g ( i n u m s [ i ] ) = i l o g n u m s [ i ] log(∏_inums[i]) = \sum_{i}lognums[i] 这样就不会出现数值溢出的问题了。

算法

对 nums 中的每个数取对数后,我们存储它的前缀和prefix,即 p r e f i x [ i + 1 ] = x = 0 i n u m s [ x ] prefix[i + 1] = \sum_{x=0}^inums[x] ,这样在二分查找时,对于 i和 j,我们可以用 prefix[j + 1] - prefix[i] 得到 nums[i] 到 nums[j] 的乘积的对数。对于固定的 i,当找到最大的满足条件的 j后,它会包含 j−i+1 个乘积小于 k 的连续子数组。

下面的代码和算法中下标的定义略有不同。

	public int numSubarrayProductLessThanK2(int[] nums, int k) {
		if (k == 0)
			return 0;
		double logk = Math.log(k);
		double[] prefix = new double[nums.length + 1];
		for (int i = 0; i < nums.length; i++) {
			prefix[i + 1] = prefix[i] + Math.log(nums[i]);
		}

		int ans = 0;
		for (int i = 0; i < prefix.length; i++) {
			int lo = i + 1, hi = prefix.length;
			while (lo < hi) {
				int mi = lo + (hi - lo) / 2;
				if (prefix[mi] < prefix[i] + logk - 1e-9)
					lo = mi + 1;
				else
					hi = mi;
			}
			ans += lo - i - 1;
		}
		return ans;
	}

解法二: Sliding Window 滑动窗口

分析

对应每个right, 找到满足从(left,right)之间乘积小于k的最小的left。比如(2,6) 也满足,(3,6) 也满足,我们要找的left是2。不过其实这句话不说也行还容易让人误解,因为我们的判断是让right逐渐向右走,当(left, right) 之间的乘积小于k我们就不找了,因此肯定就是最小的left。

核心思想:

始终保证当前窗口中乘积小于k。当前窗口下的乘积小于k等价于当前窗口下子数组的各个连续子数组乘积也小于k。举个例子:当窗口为[10,5,2]时, 10,5,2<k, 那么随便从其中拿出两个或一个或三个是不是都小于k。
接下来这里有个很重要的条件是要连续的子数组也就是说[10,5,2]中不能取[10,2]因为不连续。
思路很明显了:

  1. 使用变量count存储个数
  2. 我们让右指针不断前进,当窗口 乘积小于k时,就让count+=窗口连续子数组个数
  3. 当窗口乘积大于等于k时,我们就让乘积去除左指针对应的值,然后左指针右移,直至乘积小于k。

那么窗口中连续子数组的个数是多少呢?
count+=right-left+1 (此如例子中[10, 5, 2, 6],初始情况窗口中只有10,所以count+1, 之后窗口中加上了5,变成[10,5],其中连续子数组有: {{10},{5},{10,5},, 之前的10已经加过了,因此每次加进去的连续子数组是以当前right对应的数为首的连续子数组,再以[10,5,2], 以2为首就是({2,2,5,2,5,10},对应为right-left+1.

	public int numSubarrayProductLessThanK(int[] nums, int k) {
		 if(k <= 1) {
			 return 0;
		 }
		 int now = 1, left = 0, count = 0;
		 for(int right = 0; right < nums.length; right++) {
			 now *= nums[right];
			 while(now >= k) {
				 now /= nums[left++];
			 }
			 count += right - left + 1;
		 }
		 return count;
	}

最后,不经历风雨,怎能在计算机的大山之顶看见彩虹呢! 无论怎样,相信明天一定会更好!!!!!

猜你喜欢

转载自blog.csdn.net/weixin_45333934/article/details/107632537