本次的两个算法题是子数组的积的问题,勉强也能算做子数组和的问题。
问题一:乘积小于k的子数组
问题描述:
给定一正整数数组以及一整数k,要求找到所有乘积小于k的子数组的个数。
input : nums = [3 5 2 8] k = 17
output: 7 解释: [3] [5] [2] [8] [3 5] [5 2] [2 8]
解法一:前缀和 + 二分搜索
看到题的第一反应还是用前缀和数组求解。但是由于是乘积可能会出现越界情况,因此对num求对数。此时的乘法就变为了加法。因此依然可以使用前缀和的结构,遍历出所有的子数组,以O(N^2)的时间复杂度求解。不过由于数组中全是正整数,因此前缀和数组的单调递增的,因此可以使用二分查找优化其时间复杂度降为O(Nlog(N))。
大体思路还是这样,找到依次以当前结点开头的乘积小于k的最长子数组,此时该最长子数组中的以当前结点开头该子数组的所有子数组都满足条件。实现代码如下:
// 方法一 前缀和 + 二分查找
public int numSubarrayProductLessThanK(int[] nums, int k) {
double [] sums = new double[nums.length + 1];
double logK = Math.log(k);
for(int i = 0; i < nums.length; i++){
sums[i + 1] = sums[i] + Math.log(nums[i]);
}
int count = 0;
//以i开头的所有子序列 找到 sums 中小于 logK + sums[i - 1]的最后一个
for(int i = 1; i < sums.length; i++){
int tail = binSearch(sums, i, sums.length - 1, logK + sums[i - 1]);
if(tail == -1){
continue;
}
count += tail - i + 1;
}
return count;
}
public int binSearch(double[] sums, int start, int end, double target){
int mid = 0;
while(start < end - 1){
mid = start + (end - start) / 2;
if(sums[mid] < target){
start = mid;
}else{
end = mid;
}
}
if(sums[end] < target){
return end;
}else if(sums[start] < target){
return start;
}
return -1;
}
解法二:滑动窗口
大体思路为找到遍历过程中以当前结点结尾的最长满足条件的子数组。滑动窗口的左边记做left,右端记做right。该窗口始终维持一right结尾的满足条件的最长子数组。遍历过程中让right右移,窗内的结点维持:left右移直到该窗类之积小于k。实现代码如下:
// 方法二 滑动窗
public int numSubarrayProductLessThanK(int[] nums, int k) {
if(k <= 1){
return 0;
}
int left = 0, right = 0;
int prod = 1;
// 依次找到以right结尾的乘积小于k的最长子数组
int count = 0;
for(;right < nums.length; right++){
prod *= nums[right];
while(prod >= k){
prod /= nums[left++];
}
count += right - left + 1;
}
return count;
}
问题二:乘积最大的子数组
问题描述:
给定一整型数组,找到其乘积最大的子数组,返回其最大乘积。
intput : nums = [3 -3 -2 -4 5 0]
output : 40 ,解释[-2 -4 5]
解决方案:
遍历过程中依然是找以当前位置结尾的积最大子数组。当该整型数组中全为正数,若我们已知以nums[i]结尾的最大乘积为products[i],则以nums[i + 1]结尾的最大乘积有以下两种情况 :
1)在原先子数组的基础上添上该元素,
2)以该元素为新的头
products[ i + 1] = max(products[i] * nums[i + 1], nums[i + 1])。
但是由于该数组中既有正数,又有负数,除了保存最大的乘积外还需保留最小的乘积。由于负负得正嘛。
productMax[ i + 1] = max(productMax[i] * nums[i + 1], nums[i + 1],productMin[i] * nums[i + 1])
productMax[ i + 1] = min(productMax[i] * nums[i + 1], nums[i + 1],productMin[i] * nums[i + 1])
实现代码如下:
public int maxProduct(int[] nums) {
int[] productMax = new int[nums.length];
int[] productMin = new int[nums.length];
productMax[0] = nums[0];
productMin[0] = nums[0];
int ans = nums[0];
for(int i = 1; i < nums.length; i++){
productMax[i] = Math.max(nums[i],
Math.max(nums[i] * productMax[i - 1], nums[i] * productMin[i - 1]));
productMin[i] = Math.min(nums[i],
Math.min(nums[i] * productMax[i - 1], nums[i] * productMin[i - 1]));
ans = Math.max(ans, productMax[i]);
}
return ans;
}
我们发现,其后面的最小值和最大值只依赖于前一个的最小值和最大值,因此该问题可以不使用productMax 和productMin连个数组,只使用两个临时变量即可。其额外空间复杂度O(N) - > O(1),代码如下:
public int maxProduct(int[] nums) {
int productMax = nums[0];
int productMin = nums[0];
int ans = nums[0];
for(int i = 1; i < nums.length; i++){
int tempMax = Math.max(nums[i],
Math.max(nums[i] * productMax, nums[i] * productMin));
int tempMin = Math.min(nums[i],
Math.min(nums[i] * productMax, nums[i] * productMin));
productMax = tempMax;
productMin = tempMin;
ans = Math.max(ans, productMax);
}
return ans;
}
此处需要注意的是,对于productMax和productMin需要使用连个临时变量来赋值,如此为了避免当前的productMin的计算用的是当前的productMax而不是上一个productMax。