题目描述&链接
Leetcode 907 : 给定一个数组,找到所有子数组最小值,并返回所有最小值之和
题目思路
1. 暴力枚举
直观思路就是通过暴力枚举出所有的子数组,并求所有子数组最小值之和,因为题目子数组是指连续子数组,所有暴力枚举就是枚举起始点和终止点,两层for loop。然后遍历的过程中打擂台保存当前最小值,并求和。直接上代码:
public int sumSubarrayMins(int[] arr) {
// 暴力求解起点终点
int res = 0;
// 枚举出所有范围
for(int st=0; st<arr.length; st++) {
int min = arr[st];
for(int ed=st; ed<arr.length; ed++) {
if(arr[ed]<min) min = arr[ed];
res = (res+min)%100_000_0007;
}
}
return res;
}
暴力枚举需要枚举所有起始点和终止点,时间复杂度:;空间复杂度:
。
2. 逆向思维 + 单调栈
这道题思路与Leetcode 828比较类似,我们正向思维会想办法查找出所有的子数组然后求他的最小值,那么能不能换一种思路,我们找到一个值求解这个值能作为最小值作用的范围。
举例:有一个数组[3,1,2,4],我去找每一个点作为最小值作用的范围
Array : [3, 1, 2, 4]
当前值为3,左边前一个<3的位置-1(没有),右边下一个<3的位置1
- 起始点属于(-1, 0],终止点属于[0, 1),只有子数组为[3]时最小值为3 => (0-(-1))*(1-0) = 1种情况
当前值为1,左边前一个<1的位置-1(没有),右边下一个<1的位置4(没有)
- 起始点属于(-1, 1]任选一个,终止点属于[1, 4)任选一个,子数组为最小值为1 => (1-(-1))*(4-1) = 6种情况
当前值为2,左边前一个<2的位置1,右边下一个<2的位置4(没有)
- 起始点属于(1, 2]只能选2,终止点属于[2, 4)任选一个,子数组最小值为2 => (2-1)*(4-2) = 2种情况
当前值为4,左边前一个<4的位置2,右边下一个<4的位置4(没有)
- 起始点属于(2, 3],终止点属于[3, 4),子数组为最小值为4 => (3-2)*(4-3) = 1种情况
总共子数组个数: 1+6+2+1 = 10种情况
- 验证数组长度为4的子数组个数:
子数组长度=1(4种) +
子数组长度=2([3,1],[1,2],[2,4]3种情况) +
子数组长度=3(两种) +
子数组长度=4(1种)
= 10种
所有只要知道了每一个元素X前一个<X和后一个<X的位置,我们就可以在时间内完成求和。那么如何求解一个元素前一个比他小和后一个比他小的位置呢,这正是单调栈解决的问题。
- 单调栈: 是指一个栈内保存的元素满足单调递增或者递减的关系
- 单调栈 - 概念模板题目:Leetcode 496. Next Greater Element
- 单调栈最典型的应用就是解决这两种问询:
- 求当前元素前一个大于或者小于他的值位置
- 求当前元素下一个大于或者小于他的值位置
通过单调栈如果记录下一个小于当前值的位置,具体思路维护一个单调递增栈:遍历数组,如果下一个元素比当前stack中元素大那么加入stack中,如果比栈顶小那么,将stack中所有大于该值的元素pop出来,该值就是pop出来的这些元素的下一个小于这些元素的位置,代码如下:
int n = arr.length;
int[] nextSmall = new int[n];
Arrays.fill(nextSmall, n); // 如果没有下一个更小值那么默认值arr.length
Stack<Integer> stk = new Stack<>();
stk.push(0); // 将第一个元素位置压入栈
for(int i=1; i<arr.length; i++) {
while(!stk.isEmpty() && arr[i]<arr[stk.peek()]) {
// 如果出现小于栈顶元素,stack pop
int idx = stk.pop();
nextSmall[idx] = i;
}
stk.push(i);
}
同理求解前一个小于当前值位置,从后向前遍历求解即可。
- 当数组中出现重复元素
之前的例子没有重复元素,我们需要考虑如果有重复元素,那么会不会引入重复子数组,下面这个例子可以看出,我们应该寻找的是前一个小于等于当前值的位置,从而避免引入等于当前值位置而造成重复解问题。
加入数组 [3, 1, 4, 1, 2]
3作最小值范围 (0-(-1))*(1-0) = 1种
index=1上的1作为最小值范围 (1-(-1))*(5-1) = 8种
4作最小值范围 (2-1)*(3-2) = 1种
index=3上的1作为最小值范围 (3-(-1))*(5-3) = 8种
- !!!这里就出现了问题,这里面起始点包含了index=1处的1导致有重复解,所以选取起始点时应该上一个1之前的点不应该选
2作为最小值范围 (4-3)*(5-4) = 1种
这样这道题整体思路也就比较明朗了,代码如下:
class Solution {
public int sumSubarrayMins(int[] arr) {
// 遍历数组,找到每一个位置前面和后面比他小元素的位置
int n = arr.length;
int[] lmin = new int[n];
Arrays.fill(lmin, -1);
int[] rmin = new int[n];
Arrays.fill(rmin, n);
Stack<Integer> stk1 = new Stack<>();
Stack<Integer> stk2 = new Stack<>();
stk1.push(0); // add first element
stk2.push(arr.length-1);
// 前后更小的元素,单调递增栈
for(int i=1; i<arr.length; i++) {
// 如果递增加入递减就是找到下一个比他小的
while(!stk1.isEmpty() && arr[i]<arr[stk1.peek()]) {
int idx = stk1.pop();
rmin[idx] = i;
}
stk1.push(i);
}
for(int i=arr.length-2; i>=0; i--) {
// 如果递增加入,如果小就找到前面一个比他小的值
while(!stk2.isEmpty() && arr[i]<=arr[stk2.peek()]) {
int idx = stk2.pop();
lmin[idx] = i;
}
stk2.push(i);
}
// 然后寻找每一个元素作为最小值的范围 (i-l)*(r-i)*val
long res = 0;
int mod = 1000_000_007;
for(int i=0; i<arr.length; i++) {
int l = lmin[i];
int r = rmin[i];
long sum = (long)(i-l)*(r-i)*arr[i];
res = (res + sum)%mod;
}
return (int)res;
}
}
时间复杂度:;空间复杂度:
, 开辟
来保存单调栈和前后小于当前值位置。