算法笔记——左神进阶(3)单调栈结构:数组的最大数、最大子矩阵、数组烽火传递

单调栈结构真的很有用

单调栈结构的使用场景

单调栈解决的问题是在一个数组中每一个数,要求左边离他近的比他大的和右边离他近的比他大的数
思考的问题:如果知道所有数上得到上述要求,同时复杂度满足O(N)。
暴力解的时间复杂度是O(N^2)

单调栈结构:单调栈内,从栈底到栈顶满足从大到小。

逐个入栈出栈: 复杂度为O(N)

大的放下面,如果遇到一个数比栈顶的数大,则栈顶的数出栈,此时他右边最近比他大的数就是让他出栈的,而左边最近比他大的数就是他栈内下方的那个

【ps】如果遇到连续相等的数,则把序号压在一起放入栈内


eg:5(0)4(1)3(2)6(3)后面括号代表所属位置
5(0)压入栈然后4(1)比5(0)小,所以将4(1)压入栈中 第三步因为3(2)比4(1)小,所以也压入栈中
6(3)比3(2)要大,此时弹出栈顶的值3(2),并且将括号内的角标改成3变成3(3),代表对于3(2)这个数而言,右边第一个比他大的数位于3的位置。
3(2)左边离他最近的比他大的值就是在3的栈中的下一个值,也就是4(1)。
比较完之后将当前的6(3)放入数组之中,之前获得了信息的值可以直接进行表示了,不需要再放入栈中。
等到最后栈中还存在内容,则此时的所有数据单独弹出,此时右边没有比他大的数,栈中下面的数值是他的左边的比他大的数值。
如果出现相等数的情况,则此时下标压在一起,弹出是也一起弹出,多次计算。
由于每个数都是进栈一次出栈一次,所以复杂度为O(N)。


题目1:构造数组的MaxTree

在这里插入图片描述
【思路】
1、这道题可以用大根堆来做
2、这道题可以用单调栈结构来做
【单调栈解题步骤】
遍历数组,找到并记录下每个数的左右两边距离最近的比他大的数;
若一个数没有左大数和右大数,
则他就是全局最大值,作为头结点;
如果一个数没有左大数或者没有右大数,则有唯一的父节点;
如果左右两边都有,则选择较小的大数,并挂在底下作为子节点;

这个方法不会形成森林(只有一个父节点);
不会形成多叉树,可以反证。


题目2:求最大子矩阵的大小

在这里插入图片描述
这里有一个直方图的引例,就是求直方图中最大矩形,就是以每一个列为高,往两边扩(比他大就可以扩展),直到扩不动或者到达边界,记录下来,乘以高就是矩形面积。 最终找出直方图的最大面积,对应代码块中的maxRecFromBottom部分。

【思路】
总体思路是将矩阵从第0行到最后一行,依次将每一行当做底,形成一个直方图,求解直方图的面积最大,也就是包含的1最多。 注意这里是把问题按层分解,然后转换成了上方的直方图引例问题

  1. 将第0行作为底,所有的长方形中,哪个长方形中含有的1最多。【1011】
  2. 此时再求以第1行作为底,所有的第0行和第1行所有的长方形中包含的1最多。【2122】
  3. 再将第2行的值打底,此时哪个长方形包含的1最多。【3230】
    。。。
    (依次类推,可以求解出矩阵中所有的矩阵包含1的数目,此时求解其中最大值就是最大的值)
    复杂度:由于每次遍历一行,最后求解的也就是将整个矩阵遍历,所以最后的结果也就是O(n*m)
//数组表示直方图最大的面积
public static int maxRecFromBottom(int[] height){
	if(height == null || height.length ==0){
		return 0;
	}
	int maxArea = 0;
	Stack<Integer> stack = new Stack<Integer>();
	//这个for循环遍历数组的每一个数
	for(int i = 0;i<height.length;i++){
		//当栈不为空,且当前数小于等于栈顶
		while(!stack.isEmpty() && height[i] <= height[stack.peek()]){
			//第一次循环j=0,k表示弹出之后底下的下标,如果没有东西则为-1.
			int j = stack.pop();
			int k = stack.isEmpty() ? -1:stack.peek();
			//左边界为k,有边界为i,乘上j位置上的高,求出当前的值
			int curArea = (i-k-1)*height[j];
			maxArea = Math.max(maxArea,curArea);
		}
		stack.push(i);
	}
	//最后栈内可能还剩东西,这个while在结算栈中剩余的内容
	while(!stack.isEmpty()){
		int j = stack.pop();
		int k = stack.isEmpty() ? -1:stack.peek();
		int curArea = (height.length-k-1)*height[j];
		maxArea = Math.max(maxArea,curArea);
	}
	return maxArea;
}


//原问题的解决
public static int maxRectSize(int[][] map){
	if(map == null || map.length == 0 || map[0].length == 0){
		return 0;
	}
	int maxArea = 0;
	int[] height = new int[map[0].length]; //这是以每一层为底的直方图
	for(int i=0;i<map.length;i++){
		for(int j = 0;j<map[0].length;j++){
			//如果j的位置为0,则将整个高度变为0,否则就在原来的长度基础上加1
			height[j] = map[i][j] == 0? 0:height[j]+1;
		}
		maxArea = Math.max(maxRecFromBottom(height),maxArea);
	}
	return maxArea;
}

题目3:数组烽火传递

【题目】
一个数组中的数字组成环形山,数值为山高
1 2 4 5 3
规则,烽火传递:
相邻之间的两座山必能看到烽火,
非相邻的山之间有一边的山高都 <= 这两个非相邻山的山高,则这两座山能相互看到烽火。
比如,1和4就不能看到,因为顺时针边,2比1大,挡住了,逆时针边,3和5都比1大,挡住了。
而3与4就能看到,虽然逆时针边5挡住了,但顺时针边1和2没挡住3。
问哪两座山能相互看到烽火;要求时间复杂为O(1)
此题答案为(1, 2)(1, 3)(2, 4)(4, 5)(5, 3)(2, 3)(4, 3)

【题解】

1、【数组中的数字互不相同】
在最高山N, 和次高山M,之间的任何一座山向顺时针,逆时针寻找,
都有可以看到山N、M两座山,即有(n - 2) * 2对,然后再加上N, M这两座山能相互看到
故共有(n - 2) * 2 + 1 = 2 * n - 3对
结论:n个不相同的数,共有(2 * n - 3)对

2、【数组中含有相同的数字】
单调栈:从大到小排序

  1. 从任意一个最大值开始,以一个方向旋转,进行入栈操作
  2. 当一个数被弹出栈时,他找到了2个可以相互看到山,即,将入栈的和他下面的
  3. 当将入栈数与栈顶相等,则将两个数的信息共同记录在同一个栈位置,
  4. 当弹出某个数为n条记录时,即在同一个位置记录了n次某个数,比如,有连续n个4入栈,则在相同位置记录n次4
  5. 此时它弹出时,能看到的山为:Cn2 + n * 2次其中Cn2[n个4之间相互组合] + n * 2[n个4与他两边的高山组合]
  6. 当将遍历完数组后,栈中剩余的数弹出时:
    对于倒数第i > 2个剩余数弹出时,他能看到的山为:Cn2 + n * 2
    对于倒数第二个剩余数弹出时,
    若最后一个数为 > 1个记录他能看到的山为:Cn2 + n * 2
    若最后一个数为 == 1个记录他能看到的山为:Cn2 + n * 1, 因为这是一个环。
  7. 对于倒数第1个剩余数弹出时,他能看到的山为:Cn2

【代码】

public static long communications(int[] arr){
	if(arr == null || arr.length<2){
		return 0;
	}
	int size = arr.length;
	int maxIndex = 0;
	//整个for循环就是在整个数组中找到最大值的位置
	for(int i =0;i<size;i++){
		maxIndex = arr[maxIndex]<arr[i] ? i:maxIndex;
	}
	//value就是最大值
	int value = arr[maxIndex];
	int index = nextIndex(size,maxIndex); //这里代表从maxIndex下一个值开始遍历
	long res = 0L;
	//新建了一个栈,并且将最大值放进去,此时就这一条记录
	Stack<Pair> stack = new Stack<Pair>();
	stack.push(new Pair(value));
	//因为遍历的起始位置是maxIndex如果最后回到了maxIndex,则认为遍历结束
	while(index != maxIndex){
		//拿到数组中的当前值
		value = arr[index];
		//单调栈
		while(!stack.isEmpty() && stack.peek().value<value){
			int times = stack.pop().times;
			//内部山峰对的和,下面两行直接合道一起也可以  即内部组合+两边的2*背部的数量
			//res +=getInternalSum(times)+times;
			//res +=stack.isEmpty() ? 0 : times;
			res +=getInternalSum(times)+2*times; //C(2,times)+2*times
			}
		//如果当前数等于栈顶的值,则将栈顶的times加一,就是说连续出现了几个相等的值
		if(!stack.isEmpty() && stack.peek().value == value){
			stack.peek().times++;
		}else{
			stack.push(new Pair(value));
		}
		index = nextIndex(size,index);
	}

	//这里代表已经全部完成入栈,下面是栈里还存在的数弹出计算
	while(!stack.isEmpty()){
		int times = stack.pop().times;
		res += getInternalSum(times);
		if(!stack.isEmpty()){  //判断你是不是倒数第二个数
			res+= times;
			if(stack.size()>1){
				res +=times;
			}else{
				res += stack.peek().times >1? times :0;
			}
		}
	}
	return res;
}

//在环形的函数中,到底就到0的位置,不到就加一
public static int nextIndex(int size,int i){
	return i<(size-1)?(i+1):0;
}
//存储的是值以及出现的次数
public static class Pair{
	public int value;
	public int times;
	public pair(int value){
		this.value = value;
		this.times = 1;
	}
}
//组合的实现C2,n
public static long getInternalSum(int n){
	return n == 1L ? 0L:(long)n*(long)(n-1)/2L;
}
发布了27 篇原创文章 · 获赞 4 · 访问量 813

猜你喜欢

转载自blog.csdn.net/qq_25414107/article/details/104684220