算法36:单调栈结构、子数组最小乘积的最大值问题(力扣1586)----单调栈

文章讲述了如何使用单调栈解决查找数组中每个元素左右两侧最近小于或大于其值的元素问题,包括无重复元素和存在重复元素两种情况下的算法设计。在无重复元素时,提供了一个O(N)时间复杂度的解决方案;对于存在重复元素的数组,通过栈和列表操作实现了快速查询。
摘要由CSDN通过智能技术生成

单调栈:就是在栈中实现数据的单调性。即从栈底到栈顶,要么递增,要么递减。

那么,使用单调栈,可以解决什么问题呢?

给定一个可能含有重复值的数组arr,i位置的数一定存在如下两个信息

1)arr[i]的左侧离i最近并且小于(或者大于)arr[i]的数在哪?

2)arr[i]的右侧离i最近并且小于(或者大于)arr[i]的数在哪? 如果想得到arr中所有位置的两个信息,怎么能让得到信息的过程尽量快

题目一:给定一个一维数组,数据都为正整数并且无重复值,要求设计一个O(N)时间复杂度的算法,找出任意位置的数据,左侧小于当前位置最近的数在哪,右侧小于当前数最近的的数在哪?

假设: 这个数组是 {1,3,5,4}。栈的单调性从栈底到栈顶递增。

那么如下:

5
3
1

也就是说,前3个数符合预期的栈的单调性,可以正常的放入栈中。那么,当最后一个数据4想要放入栈中的时候,发现栈顶元素为5,比自己大。直接放入就破坏了栈的单调性了。

1. 我们需要把栈顶元素5弹出,这5就知道右侧小于自己的并且距离最近的数为4,而左侧离自己最近并且小于自己的数为3.

2. 此时,栈顶元素为3,小于4. 那么4直接放入栈顶。整个数组全部结束

4
3
1

3. 栈循环弹出,4是最后一个元素,并且栈具有单调性。因此,弹出4可以知道,左侧离自己最近的数为3. 而右侧没有比自己更小的数

4. 弹出3,左侧比自己小的数是1,而右侧没有比自己小的数

5. 弹出1,此时栈空了。左侧、右侧都没有小于自己的数。

以上一切,只是为了直观的体现栈整个操作流程。而实际的算法设计肯定是不能用数据来直接使用的,而是需要使用每个数据对应的位置,即下标

//无重复元素
    public int[][] dp(int[] arr)
    {
        if(arr == null || arr.length == 0) {
            return null;
        }

        int[][] dp = new int[arr.length][arr.length];
        Stack<Integer> stack = new Stack<>();
        for (int i = 0; i < arr.length; i++)
        {
            while (!stack.isEmpty() && arr[stack.peek()] > arr[i])
            {
                int cur = stack.pop();
                // -1代表不存在左侧比cur下标对应的值更小的值
                int leftIndex = stack.isEmpty() ? -1 : stack.peek();
                dp[cur][0] = leftIndex;
                dp[cur][1] = i;
            }
            //放入下标
            stack.push(i);
        }

        //栈中剩余元素,保持单调增
        while (!stack.isEmpty()) {
            int cur = stack.pop();
            // -1代表不存在左侧比cur下标对应的值更小的值
            int leftIndex = stack.isEmpty() ? -1 : stack.peek();
            dp[cur][0] = leftIndex;
            //因为单调增、所有右侧不存在比自己还小的值了
            dp[cur][1] = -1;
        }
        return dp;
    }

题目二:数组存在重复元素,设计一个栈,要求能够快速找到任意位置左、右侧比自己小、位置最近的数据。

public static int[][] getNearLess(int[] arr) {
		int[][] res = new int[arr.length][2];
		Stack<List<Integer>> stack = new Stack<>();
		for (int i = 0; i < arr.length; i++) { // i -> arr[i] 进栈
			while (!stack.isEmpty() && arr[stack.peek().get(0)] > arr[i]) {
				List<Integer> popIs = stack.pop();
				int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
				for (Integer popi : popIs) {
					res[popi][0] = leftLessIndex;
					res[popi][1] = i;
				}
			}
			if (!stack.isEmpty() && arr[stack.peek().get(0)] == arr[i]) {
				stack.peek().add(Integer.valueOf(i));
			} else {
				ArrayList<Integer> list = new ArrayList<>();
				list.add(i);
				stack.push(list);
			}
		}
		while (!stack.isEmpty()) {
			List<Integer> popIs = stack.pop();
			int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
			for (Integer popi : popIs) {
				res[popi][0] = leftLessIndex;
				res[popi][1] = -1;
			}
		}
		return res;
	}

题目三:力扣1856. 子数组最小乘积的最大值

https://leetcode.cn/problems/maximum-subarray-min-product/description/

题目详情直接打开连接进行查看,这里直接说解题思路。

1. 给定数组,就存在子数组,并且子数组是连续的

2.子数组中肯定是存在最小值的,也必然会知道子数组累加和。

3. 假设每个数都是最小值,这样就能利用单调栈结构找到左侧、右侧比自己小的位置。那么除了这两个位置以外,中间部分的数据就是自己最小了。利用这个思想,我们来实现代码

数据1354
下标0123
累加和14913

5左侧比自己小的数据为3,对应下标为1;

5右侧比自己小的数据为4,对应下标为3;

也就是说5这个数据想要做最小值,那么下标1到3之间,并且不能包含下标1和下标3的和。

既然不能包含到下标为3的位置,变相的也就是能够包含到下标为2的位置,即累加和为 9 - 4 = 5;

那子数组累加和 * 最小值 =  5 * 5 = 25;

其他的依次类推........

package code04.单调栈_01;

import java.util.Stack;

/**
 * 1856. 子数组最小乘积的最大值
 * https://leetcode.cn/problems/maximum-subarray-min-product/description/
 */
public class Code01_MinSumOfSubArr {

    public int maxSumMinProduct(int[] nums)
    {
        if (nums == null || nums.length == 0) {
            return 0;
        }

        int size = nums.length;
        //前缀和数组。 题目规定要使用64位有符号整数保存
        long[] dp = new long[size];
        dp[0] = nums[0];
        for (int i = 1; i < size; i++) {
            dp[i] = dp[i-1] + nums[i];
        }

        long ans = Long.MIN_VALUE;
        //[0 ......)
        Stack<Integer> stack = new Stack();
        for(int i = 0; i < size; i++)
        {
            while (!stack.isEmpty()
                    && nums[stack.peek()] >= nums[i]) {
                //当前正在处理的数下标
                int cur = stack.pop();
                long sum = stack.isEmpty() ? dp[i-1] : dp[i-1] - dp[stack.peek()];
                ans = Math.max(ans, sum * nums[cur]);
            }
            //放入下标
            stack.push(i);
        }

        //右侧值越来越大
        while (!stack.isEmpty()) {
            //当前正在处理的数下标
            int cur = stack.pop();
            long sum = stack.isEmpty() ? dp[size-1] : (dp[size-1] - dp[stack.peek()]);
            ans = Math.max(ans, sum * nums[cur]);
        }
        return (int) (ans % 1000000007);
    }

    public static void main(String[] args)
    {
        int[] nums = {3,1,5,6,4,2};
        Code01_MinSumOfSubArr test = new Code01_MinSumOfSubArr();
        System.out.println(test.maxSumMinProduct(nums));
    }
}

此处可能会有疑问,此处使用的是无重复元素构造单调栈的算法,这一题不需要考虑重复元素的情况吗?举个例子,假如数组为 {1,2,3,2}

栈中放入1,2 3.  当放入最后一个2的时候,会把栈中的3和2弹出,并且把最后一个2入栈。 而最后一个2右侧没有比他小的值,左侧比他小的值为1,对应的下标为0. 也就是说从下标0到最后一个2的位置,此时最后一个2是最小值。当然,下标0处的1是不包含在内的。

也就是说,重复元素具有连通性,很多时候是不需要考虑重复元素的情况的。

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值