给定正数数组arr,求任意子数组最小值与子数组累加和的乘积最大值是多少?
提示:单调栈使用的经典题目
咱们学过俩重要的单调栈的基础知识:
之前咱们刚刚学过单调栈1:
(1)单调栈1:o(1)复杂度求数组arr在窗口w内的最大值或者最小值
(2)单调栈2:寻找数组arr中i位置左边距离i最近的比i小的位置L,右边距离i最近的比i小的位置R
咱们今天就用这里的单调栈解决问题
题目
给定正数数组arr,求任意子数组最小值min与子数组累加和sum的乘积的最大值是多少?
不妨设子数组为L–R范围上的数组
累加和sum,窗口内min知道
求sum×min的最大值是多少?
一、审题
示例:
arr=1 1 1 1 1
不妨设子数组为L–R范围上的数组,
由于数组arr都是1,任意子数组的最小值min=1
minsum=sum,则找sum最大值就行,由于arr大于0,所以L–R长度越长越好,即N长度=5最好
这样的话,minsum的最大值就是1*5=5
这要是arr不是全1呢
比如arr=1 2 3 3 2 1
暴力解,不可,o(n^2)复杂度
比如arr=1 2 3 3 2 1
咱们可以这样做,让每一个arri都做一次最小值min,看看它能向左,向右扩展多大范围,形成的子数组,累加和求上来,再求乘积
每一个i遍历之后,必然最大值就在其中
(0)这样的话,外围o(n)调度
(1)内部,每次i,向左搜索最左边最近的比i小的那个位置L,向右搜索右边最近的比i小的那个位置R,图中累加和放入sum
因为只有找到左边L,右边R,中间L–R范围内,i才能做最小值!!
(2)计算sum*[i],更新给max结果–需要o(n)速度
(3)最后返回max即可
整体复杂度就是o(n^2),显然者不可取!!!
单调栈解决,将其优化到o(n)复杂度
既然你暴力解都想到了宏观调度,这个想法很好,就让每个i做一次最小值,看看往左右能扩多大范围子数组L–R
因为arr大于零,这个扩的越远,越好,sum越大
但是最重要的优化:求累加和这个事,可以o(1)速度搞定的,
咱们老早就学过了,因为arr大于零
求arr任意范围上的L–R的累加和sum,是可以用前缀累加和数组来加速的
也就是前缀累加和数组pre,是arr从0–N-1不断地累加得到的前缀数组。【下图pre】
因为有了前缀累加和数组pre,则L–R累加和就是:sum[L–R] = pre[R]-pre[L-1]
好,只要咱们来到arr的i位置,只要知道了L,R,则再以o(1)速度拿到L–R的累加和,岂不是很爽?整个过程就o(1)
L,R上面说了,arr中距离i左边最近的比i小的位置L,距离i右边最近的比i小的位置R
这敏感度你需要有:(2)单调栈2:寻找数组arr中i位置左边距离i最近的比i小的位置L,右边距离i最近的比i小的位置R
看到了吗,你储备的知识,单调栈可以用了,它就是让你o(1)速度拿到i处左右的最近比i小的L和R位置的。
这速度够快吧!这基础知识,是不是很重要?所以继续往下看之前,你必须学会上面的基础知识(2)文章
okay,总结一波!
解决本题的代码逻辑流程:
(0)arr准备累加和数组pre
arr外围o(n)调度遍历每一个位置i,将[i]拿来做最小值,求得的max=[i]×sum[L–R]
(1)内部,每次i,用单调栈,o(1)拿到:
向左搜索最左边最近的比i小的那个位置L,向右搜索右边最近的比i小的那个位置R,
然后再o(1)速度拿到L–R上的累加和放入sum【注意L,R位置的那俩数不要哦】
(2)计算sum*[i],更新给max结果–这就需要o(1)速度了哦
(3)最后返回max即可
整体就被优化为o(n)了!爽吧
有了单调栈的代码,咱直接它来求本题题目的结果!!!
单调栈的代码,返回的是pos数组,每个i的左右LR
//复习:
//真正的单调栈,肯定要考虑数组arr是重复的情况,所以咱们来手撕代码实现上述流程吧!
public static int[][] getLeftRightNearestLessiPosition(int[] arr){
if (arr == null || arr.length == 0) return null;
int N = arr.length;
int[][] ans = new int[N][2];//结果0L,1R
//用栈搞定
Stack<List<Integer>> stack = new Stack<>();
//(0)遍历每一个位置i,方便收集答案
for (int i = 0; i < N; i++) {
//(1)一上来,先看看需要弹出栈顶收集答案吗?因为每次我们面对的i都可能收集答案,除了第一次i=0
while (!stack.isEmpty() && arr[i] < arr[stack.peek().get(0)]){
// 需要的话,看栈空吗?空:L=-1,否则L就是此时新栈顶pNew的末尾位置那个i
//他们的R是此刻的i;
List<Integer> list = stack.pop();//先弹出来
int R = i;
int L = stack.isEmpty() ? - 1 : stack.peek().get(stack.size() - 1);//新栈顶的末尾位置
for(Integer j:list){
ans[j][0] = L;
ans[j][1] = R;
}
}
//(2)(1)走完,咱们就要压栈i进去
//注意,如果栈顶已经存在了,添加即可,否则要新建list
if (!stack.isEmpty() && arr[i] == stack.peek().get(0)){
//有了
stack.peek().add(i);
}else {
//没有的话
List<Integer> list = new ArrayList<>();
list.add(i);
stack.push(list);
}
}
//(3)如果i=N了那越界出来了,还要处理栈中剩下的元素们,结算他们,
// 他们所有的R=-1;如果最新的栈顶pNew不空,则L就是pNew的末尾位置那个i,空的话L=-1;
while (!stack.isEmpty()){
// 需要的话,看栈空吗?空:L=-1,否则L就是此时新栈顶pNew的末尾位置那个i
//他们的R是此刻的i;
List<Integer> list = stack.pop();//先弹出来
int R = -1;
int L = stack.isEmpty() ? - 1 : stack.peek().get(stack.size() - 1);//新栈顶的末尾位置
for(Integer j:list){
ans[j][0] = L;
ans[j][1] = R;
}
}
return ans;
}
本题的解题代码:
再看看宏观调度:
(0)arr准备累加和数组pre
arr外围o(n)调度遍历每一个位置i,将[i]拿来做最小值,求得的max=[i]×sum[L–R]
(1)内部,每次i,用单调栈,o(1)拿到:
向左搜索最左边最近的比i小的那个位置L,向右搜索右边最近的比i小的那个位置R,
然后再o(1)速度拿到L–R上的累加和放入sum
(2)计算sum*[i],更新给max结果–这就需要o(1)速度了哦【L,R处的数不要】
(3)最后返回max即可
手撕代码,中间有个小逻辑要搞清楚,不容易!!!
//给定正数数组arr,求任意子数组最小值与子数组累加和的乘积最大值是多少
//再看看宏观调度:
public static int maxMultiplyOfMinAndSubSum(int[] arr){
if (arr == null || arr.length == 0) return 0;
//(0)arr准备累加和数组pre
int N = arr.length;
int[] pre = new int[N];//0位置当没有
pre[0] = arr[0];//默认
for (int i = 1; i < N; i++) {
pre[i] = pre[i - 1] + arr[i];//累加上来
}
//L--R累加和就变为pre[R]-pre[L-1]
int max = Integer.MIN_VALUE;//ans
//向左搜索最左边最近的比i小的那个位置L,向右搜索右边最近的比i小的那个位置R
int[][] pos = getLeftRightNearestLessiPosition(arr);
//arr外围o(n)调度遍历每一个位置i,将[i]拿来做最小值,求得的max=[i]×sum[L--R]
// (1)内部,每次i,用单调栈,o(1)拿到:
for (int i = 0; i < N; i++) {
// 向左搜索最左边最近的比i小的那个位置L,向右搜索右边最近的比i小的那个位置R,
int L = pos[i][0] == -1 ? 0 : pos[i][0];
int R = pos[i][1] == -1 ? N - 1 : pos[i][1];//为了方便待会求累加和的,左边-1换成0,右边-1换为N
// 然后再o(1)速度拿到L--R上的累加和放入sum
int sum = 0;
if (pos[i][0] == -1) {//左边L是-1的话,0--R-1就是sum
sum = pos[i][1] != -1 ? pre[R - 1] : pre[R];//正常L--R的L,R不能算哦,R=N时要N-1位置
}else {
//L不是-1的话,左边的L不能要
sum = pos[i][1] != -1 ? pre[R - 1] - pre[L]: pre[R] - pre[L];//正常L--R的L,R不能算哦,R=N时要N-1位置
}
//如果R==N,arr[i]找补回来
// (2)计算sum*[i],更新给max结果--这就需要o(1)速度了哦
max = Math.max(max, arr[i] * sum);
}
//(3)最后返回max即可
return max;
}
public static void test(){
int[] arr = {1,2,5,2,1};
//1:1*1==1
//2:2*2=4
//3:3*(3+3)=9
//4:4*4=16
//5:5*(5+5)=50
System.out.println(findTarget(arr));
System.out.println(maxMultiplyOfMinAndSubSum(arr));
}
public static void main(String[] args) {
test();
}
里面的
int L = pos[i][0] == -1 ? 0 : pos[i][0];
int R = pos[i][1] == -1 ? N - 1 : pos[i][1];//为了方便待会求累加和的,左边-1换成0,右边-1换为N
是这样的,pre因为没有-1位置,
所以L=-1的话,你要换为0,但是求sum时,咱们要算上arr[0],正常真的是L=0时,求sum时arr[L]不能要的
同样的,R=-1时,咱们要换位N-1,但是sum要算上arr[N-1]位置那个数,正常真的是R=N-1时,求sum时arr[N-1]不能要的
如果一般情况下,L和R找到了,我们求累加和是不能要L和R位置的数的。(就像上图粉色那个括号的sum)
如果pos[i][0] = -1,则L=0,此时我们要arr[0],如果此刻,pos[i][1] != -1,说明R不能要,则取绿色那个括号为sum
如果pos[i][1] != -1,说明arr[N-1]得要,就像橘色那个括号代表的sum
如果pos[i][0] != -1,我们不能要L,如果此刻,pos[i][1] != -1,说明R不能要,则取粉色那个括号为sum
如果pos[i][1] != -1,说明arr[N-1]得要,就像蓝色那个括号代表的sum
这也就是为啥咱们要大费周章控制好下标的原因:
// 然后再o(1)速度拿到L--R上的累加和放入sum
int sum = 0;
if (pos[i][0] == -1) {//左边L是-1的话,0--R-1就是sum
sum = pos[i][1] != -1 ? pre[R - 1] : pre[R];//正常L--R的L,R不能算哦,R=N时要N-1位置
}else {
//L不是-1的话,左边的L不能要
sum = pos[i][1] != -1 ? pre[R - 1] - pre[L]: pre[R] - pre[L];//正常L--R的L,R不能算哦,R=N时要N-1位置
}
好好理解这个下标的事情,然后本题测试:
25
25
理解了解题思路,抠代码就好办了,自己要花点时间coding清楚。
总结
提示:重要经验:
1)单调栈的敏感度:求滑动窗口内的最大值,最小值,或者,求i左边右边距离i最近比i小的位置L,R
2)前缀累加和数组pre的敏感度,要随时可以想到,尤其是arr大于0时,这个可以加速算法。
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。