给定一个正整数数组 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的子数组。
需要注意到数组内每一个数字都是正数,所以随着子数组的中数字个数增多,乘积也是单调递增的,这是这个题的关键。
找到这个单调递增的关系之后,有两种办法解决这个问题。
方法一:
首先是二分+前缀和,前缀和如不知请自行点击,利用前缀和可以在O(1)时间内求得一个子数组的乘积,那么对于数组的每一位i,我们可以从后向前找到最长的以i位为结尾子数组使得该子数组的乘积小于k。
注意到随着数组变长乘积单增,那么用一下二分枚举中点加上前缀和就能解决问题。
不过还有一点,这里前缀和保存的是乘积,容易爆掉long int的范围,为了避免这一点,可以取对数将乘积变成加法运算。
class Solution {
public int numSubarrayProductLessThanK(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;
}
}
方法二:
注意到上文说的单增的性质,假如[L,R]区间的子数组乘积>=k,那么无论如何扩大R,得到的子数组都不可能满足题意。要得到满足题意的子数组只能扩大L,想到这点很关键。
那么我们要维护一个last[i]数组,last[i]代表从第i位开始满足题意的子数组最远可以到达哪一位。
从L=1开始,让R不断扩大,直到子数组乘积>=k,那么就得到了last[1],然后让L++,再去判断子数组乘积决定是否要移动R,注意R不可能缩小,以此类推就能得到所有的last[i]。
得到last数组之后,累加每一个(last[i]-i)就是答案,这个也很显然,画画图就明白。
class Solution {
public int numSubarrayProductLessThanK(int[] nums, int k) {
int len = nums.length;
int sum = 1;
int last = -1;
int ans = 0;
for(int i=0;i<len;i++)
{
if(i>0)
sum = sum / nums[i-1];
while(sum<k&&last<len)
{
last++;
if(last>=len)
break;
sum = sum * nums[last];
}
ans = ans + Math.max(last-i,0);
}
return ans;
}
}