一个数组的 最小乘积 定义为这个数组中 最小值 乘以 数组的 和 。
- 「最小乘积」的定义为「最小值」乘以「和」,由于「和」较难进行枚举,我们可以考虑枚举「最小值」。
- 我们可以枚举数组中的每个元素 nums 作为最小值
由于数组中的元素均为正数,那么我们选择的包含 nums 的子数组是越长越好的。
我们选择子数组的限制 nums 必须是子数组中的最小值」。那么我们应当找到
- nums 之前 距离 nums最近的且小于nums的元素 标记为 left
- nums 之后 距离 nums 最近的且小于nums的元素 标记为 right
如果没有上述说法中的最小值的这样的元素,就是说左边没有最小值 例如 nums=1 [1,2,3] 那么1 的left 就记为 -1 如果右边也没有最小值 例如 3 和 1 右边都没有最小值 那么就记为 right = n
此时 闭区间[left + 1 ,right - 1] 就是nums作为最小值 且 最长的子数组
可以使用单调栈来计算每个nums 的left 和right 值
有了这个子数组 再使用前缀和 来计算这个闭区间的和 是O(1) 的
生成前缀和是O(N) 获取每个nums的left和right 也是O(N) 的 那么 此题目时间复杂度就在 O(N)
生成的前缀和和数组长度一致,最坏情况下单调栈可能存储N个元素 所以空间复杂度为 O(N)
例: [1,2,3] 单调栈会全部存储进去 最后弹出时候计算 left和right
本题细节: 如果数组内无重复值 构建的单调栈存储的是元素的索引,如果有重复值 里面存放的是链表。
但是本题数组有重复值 但是我们采用的是数组内无重复值 这种方法,其中left是严格定义必须是nums之前距离nums最近且小于nums的元素,但是right 定义为nums之后 距离nums最近且小于等于 nums 的元素这样子对正确的答案并不会造成影响。在严格遵守定义的条件下,答案对应的子数组中,每一个最小的元素都对应着正确的答案,但是 在right 不遵守定义条件下,答案对应的数组中,只有最后出现的最小元素对应正确答案,我们要求的是最大值,所以保证只要有一个最大值即可。
举例: [7,8,3,4,3,2,5] 其中有两个3 第一个3的区间计算出来的是 [0,3] 第二个3 的区间计算的是[0,4] 只有最后出现的那个相同的数字才是正确的答案。
提示:
- 1 <= nums.length <= 10^5
- 1 <= nums[i] <= 10^7
请注意,最小乘积的最大值考虑的是取余操作 之前 的结果。题目保证最小乘积的最大值在 不取余 的情况下可以用 64 位有符号整数 保存。(那这么说 使用long类型 最后取余即可)
前缀和注意溢出问题,以及最后对结果取模
public static int maxSumMinProduct(int[] nums) {
// 1. 寻找一个子数组的和 使用前缀和数组
if (nums == null || nums.length == 0) {
return -1;
}
long[] prefixSum = new long[nums.length];
prefixSum[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
prefixSum[i] = prefixSum[i - 1] + nums[i];
}
// 例: [2,3,5,1,7,6,4] nums
// 0 1 2 3 4 5 6
// 前缀和[2,5,10,11,18,24,28] prefixSum
// 0 1 2 3 4 5 6
// 想要获取 索引 2~4 的子数组的和 那么0~4的和是18 使用 0~4的和 减去 0~1的和 就是 子数组2~4的和 18 - 5 = 13
// 5 + 1 + 7 也是等于 13
// 使用单调栈 以数组中每个元素当做最小值的话 可以找到每个元素 子数组的左边界和右边界
// 通过前缀和数组算出 这个子数组的sum * min 就是最少乘积 获取最大的那个乘积
Stack<Integer> stack = new Stack<>();
long max = Long.MIN_VALUE;
for (int i = 0; i < nums.length; i++) {
while (!stack.isEmpty() && nums[stack.peek()] >= nums[i]) {
Integer minIndex = stack.pop();
max = Math.max(max, (stack.isEmpty() ? prefixSum[i - 1] : prefixSum[i - 1] - prefixSum[stack.peek()]) * nums[minIndex]);
}
stack.push(i);
}
while (!stack.isEmpty()) {
Integer minIndex = stack.pop();
max = Math.max(max, (stack.isEmpty() ? prefixSum[nums.length - 1] : prefixSum[nums.length - 1] - prefixSum[stack.peek()]) * nums[minIndex]);
}
return (int) (max % 1000000007);
}
上述代码 25 行讲解 当时学习有疑惑!
结算的是:minIndex位置的数字作为最小值情况下,nums[minIndex] * 扩展最大的子数组累加和
(stack.isEmpty() ? prefixSum[i - 1] : prefixSum[i - 1] - prefixSum[stack.peek()]) = 扩展最大的子数组累加和
分为两种情况 m代表minIndex
1. m 的左边没有比m小的数字。也就是在m位置弹出之后,stack是空的,比如
5 3 3 4 2 5 6 1 ...
0 1 2 3 4 5 6 7 ...
m i
比如m==4位置,因为i==7位置的数字,m 从单调栈弹出。
那么,m左边没有比nums[4]小的数字 那么此时就是
prefixSum[i-1] * nums[minIndex],也就是:prefixSum[6] * nums[4]
2. m的左边有比m小的数字。也就是在m位置弹出之后,stack不是空的,比如
5 1 3 4 2 5 6 1 ...
0 1 2 3 4 5 6 7 ...
m i
比如m==4位置,因为i==7位置的数字,m从单调栈弹出。
此时,m左边有比nums[4]小的数字,在1位置。此时就是 : (prefixSum[i-1] - prefixSum[stack.peek()]) * nums[m],
也就是:(prefixSum[6] - prefixSum[1]) * nums[4],也就是nums[2…6]的累加和 * nums[4]
public static int maxSumMinProduct(int[] arr) {
int size = arr.length;
long[] sums = new long[size];
sums[0] = arr[0];
for (int i = 1; i < size; i++) {
sums[i] = sums[i - 1] + arr[i];
}
long max = Long.MIN_VALUE;
// 数组模拟实现栈
int[] stack = new int[size];
int stackSize = 0;
for (int i = 0; i < size; i++) {
while (stackSize != 0 && arr[stack[stackSize - 1]] >= arr[i]) {
int j = stack[--stackSize];
max = Math.max(max,
(stackSize == 0 ? sums[i - 1] : (sums[i - 1] - sums[stack[stackSize - 1]])) * arr[j]);
}
stack[stackSize++] = i;
}
while (stackSize != 0) {
int j = stack[--stackSize];
max = Math.max(max,
(stackSize == 0 ? sums[size - 1] : (sums[size - 1] - sums[stack[stackSize - 1]])) * arr[j]);
}
return (int) (max % 1000000007);
}