当前位置: 首页 > article >正文

算法通关--单调栈

单调栈

单调栈是在栈的先进后出的规则基础上,要求从栈底到栈顶的元素满足单调的关系。如果是大压小,那么从栈顶观察是递减栈,如果是小压大,那么从栈顶观察使递减栈

经典用法:

判断一个数组每个位置都求:
1.当前位置的 左侧比当前位置的数字小(大),且距离最近的位置在哪
2.当前位置的 右侧比当前位置的数字小(大),且距离最近的位置在哪

如果按照暴力求解,那么时间复杂度来到了O(N^2),但是对于单调栈来说,时间复杂度为O(N)

1.数组无重复值的情况

下面有数组[2,5,6,1,4,7,5],找出数组中比当前位置数字小的左右的位置。我们建立一个栈,栈内的元素服从数值大压小的规则,并且栈中放的是下标,因为可以通过下标来找到数组中对应位置元素的值。注:0-->2表示的是下标0位置的元素2

0-->2进栈,1-->5进栈,2-->6进栈,1-->4准备进栈,发现栈顶元素6比自己大,那么元素6出栈,元素6压的是元素5,因此元素6左边离自己最近且小于自己的位置是1-->5,3-->1使自己出去,那么右边离自己最近且小于自己的位置是3-->1,如图:

 但是2-->6出栈后,3-->1可以进栈吗,不能!!!因为1-->5比1大,0-->2比1大,当它们两个出栈后,3-->1就可以进栈了,此时可以得到:

2-->61-->53-->1
1-->50-->23-->1
0-->2-13-->1

之后4-->4进栈,5-->7进栈,6-->5进栈前弹出5-->7,最终得到栈中的元素以及列表如下:左边为-1,表示为栈底元素,在数组中的表现是左边没有比自己元素小的数字

2-->61-->53-->1
1-->50-->23-->1
0-->2-13-->1
5-->74-->46-->5

 数组中没有可以压栈的数字了,进入清算阶段,清算阶段栈中的右边均为-1,(没有元素使自己弹出),右边为自己压的元素。那么最终形成的列表为:

当前位置
2-->61-->53-->1
1-->50-->23-->1
0-->2-13-->1
5-->74-->46-->5
6-->54-->4-1
4-->43-->1-1
3-->1-1-1

 2.数组有重复值的情况

当遇到有重复值的情况,对于重复的数值先进栈的元素进行弹栈,后进栈的元素进行压栈,对于进行弹栈的元素记录左右位置的元素,即使右边的元素可能不对,但是在最后的阶段进行修正。

例如:数组[2,4,3,5,2,6],求每个位置的数字离自己最近的最小值元素,按照上面的操作,最终得到以下列表

当前位置
1-->40-->22-->3
3-->52-->34-->2
2-->30-->24-->2
0-->2-14-->2
5-->64-->2-1
4-->2-1-1

比较当前位置的元素和表格中右边位置的元素,如果比自己小,那么没有问题,如果和自己相等,那么取表格右边位置的下标的右边的值。例如:0-->2的右边是4-->2,这个有问题,那么0-->2右边是4位置右边填的数值-1。由于栈中的元素是大压小,因此左侧求出的元素没有问题

例题1:

单调栈结构(进阶)_牛客题霸_牛客网

我们利用数组进行栈的实现,其中r表示栈中的剩余的元素,ans[][]表示答案的二维数组:代码如下:

#include <iostream>
using namespace std;
const int MAXN = 1000001;

int arr[MAXN];
int ans[MAXN][2];
int n, r;
// 数组模拟实现栈
void compute() {
	int stack[MAXN];
	r = 0;        // 表示栈的状态
	int cur;      // 答案数组的指针

	for (int i = 0; i < n; i++) {
		// 构建一个大压小的单调栈,如果栈不为空,且不满足大压小,进行while循环(弹栈)
		while (r > 0 && arr[stack[r - 1]] >= arr[i]) {
			cur = stack[--r];
			// cur当前弹出的位置,左边是栈顶元素,右边是让自己弹出位置的元素
			ans[cur][0] = r > 0 ? stack[r - 1] : -1;
			ans[cur][1] = i;
		}
		// 要么栈为空 要么是 arr[i] > 栈顶元素,进栈
		stack[r++] = i;
	}
	// 清算阶段 -- 栈里还有剩余
	while (r > 0) {
		cur = stack[--r];
		ans[cur][0] = r > 0 ? stack[r - 1] : -1;
		ans[cur][1] = -1;
	}
	// 修正阶段 -- 数组中有重复元素
	// 左侧的答案不需要修正一定是正确的,只有右侧答案需要修正
	// 从右往左修正,n-1位置的右侧答案一定是-1,不需要修正
	for (int i = n - 2; i >= 0; i--) {
		if (ans[i][1] != -1 && arr[ans[i][1]] == arr[i]) {
			ans[i][1] = ans[ans[i][1]][1];
		}
	}
}

int main() {
	cin >> n;
	for (int i = 0; i < n; i++) {
		cin >> arr[i];
	}
	compute();
	for (int i = 0; i < n; i++) {
		cout << ans[i][0] << " " << ans[i][1] << endl;
	}
	return 0;
}

例题2:

https://leetcode.cn/problems/daily-temperatures/

对于temperatures=[73,74,75,71,69,72,76,73]来说,如果建立大压小的栈,那么求出每一个位置的值使小于当前位置的天数,与题意要求的不符,因此建立小压大的栈,使当前位置弹出的位置减去当前位置即为答案,在清算阶段,直接为0。例如,1位置的74使0位置的73弹出,那么答案数组中的ans[0]就是1.代码如下

vector<int> dailyTemperatures(vector<int>& temperatures) {
	int n = temperatures.size();
	vector<int> ans(n);
	stack<int> s;
    // 构建小压大的栈,栈中记录的是下标!!!
	for (int i = 0; i < n; i++) {
    
		while (!s.empty() && temperatures[i] > temperatures[s.top()]) { // 不为空且不能大压小
			int top = s.top();    
			ans[top] = i - top;	// 记录应该放那个数字,哪个位置的比他大
			s.pop();    // 弹出
		}
        // 为空or小压大
		s.push(i);
	}
    // 数组初始化时每个位置为0,因此直接返回
	return ans;
}

例题3:

907. 子数组的最小值之和 - 力扣(LeetCode)

如果是分别求出每一个子数组,在从子数组中挑选最小值相加的话,太慢了,利用单调栈怎么解?
既然是子数组,那就是一个区间,求一个区间的最小值,一个区间往外扩,未扩充之前的最小值一定大于等于扩充之后的最小值。那么在一个区间内的最小值,它的区间左边界的左边的值一定小于等于当前区间的最小值,它的区间右边界的右边的值一定小于等于当前区间的最小值,例如:
2|9 7 6 8|1,对于| |区间的最小值6来说,2和1均比6小。那么这个和单调栈有什么关系?
我们继续看:只要过在这个数组区间内,过6的子区间,最小值一定是6 ,即[9,7,6],[9,7,6,8],[7,6],[7,6,8],[6],[6,8]这些子区间都是以6为最小值,那么这些子区间的最小值为6*6=36;这不就求出和了吗,6这个区间长度怎么快速得到,就是单调栈,(当前位置的下标值-它的左边)*(它的右边当前位置的下标值),即(3-0)*(5-3) = 6;

const int MOD = 1000000007;
const int MAXN = 30001;
int stack[MAXN];
int r;

int sumSubarrayMins(vector<int>& arr) {
    long long ans = 0;
    r = 0;
    for (int i = 0; i < arr.size(); i++) {
        while (r > 0 && arr[stack[r - 1]] >= arr[i]) {
            int cur = stack[--r];
            int left = r == 0 ? -1 : stack[r - 1];
            ans = (ans + (long long)(cur - left) * (i - cur) * arr[cur]) % MOD;
        }
        stack[r++] = i;
    }
    while (r > 0) {
        int cur = stack[--r];
        int left = r == 0 ? -1 : stack[r - 1];
        ans = (ans + (long long)(cur - left) * (arr.size() - cur) * arr[cur]) % MOD;
    }
    return (int)ans;
}

例题4:

84. 柱状图中最大的矩形 - 力扣(LeetCode)

这个题怎么看,找以每个位置为高度,求可以拼成的最大矩形,左边对于比自己高的,可以扩充,比自己低的不可以扩充,那么这个就是求得每一个位置左比自己小的数,例如[2,1,5,6,2,3],对于0位置的2来说,左边没有,扩大不了,右边比自己小,扩不了,那么面积就是2;对于1位置的1来说,左边扩1个,右边扩4个,面积是6*1;对于2位置的5来说,左边不能扩,右边扩1个,面积为2*5=10;对于3位置的6来说,左边不能扩,右边不能扩,面积1*6,对于4位置的2来说,左边扩2个,右边扩1个,面积为4*2=8,对于5位置的3来说,左边不能扩,右边不能扩,面积为3;最终的整体为10.还是根据单调栈的模板,求得左边和右边比自己小值的位置
注意区间的长度,当地位置为i,由于是减小就往左扩,当while循环进不去的时候,左边的下标不在“可扩增区间内”,例如[2,1,5,6,2,3],当i在5位置的2时,i==4,对数值6得出left==2,因为此时3位置的6已经弹栈,那么获取的top就是2位置,i-left-1==1,符合条件。注意:弹出谁是才能算出该位置的矩形面积,那么最终代码是:

int largestRectangleArea(vector<int>& heights) {
	int ans = 0;
	stack<int> s;
    n = heights.size();
	for (int i = 0; i < heights.size(); i++) {
		while (!s.empty() && heights[i] < heights[s.top()]) {
			int cur = s.top();
			s.pop();
			int left = s.empty() ? -1 : s.top();
			ans = max(ans, (i - left - 1) * heights[cur]);    // 算面积!!!一定注意!!!
		}
		s.push(i);
	}
	while (!s.empty()) {
		int cur = s.top();
		s.pop();
		int left = s.empty() ? -1 : s.top();
		ans = max(ans, (n - left - 1) * heights[cur]);
	}
	return ans;
}

例题5:

962. 最大宽度坡 - 力扣(LeetCode)

题目要求的满足条件i<j并且arr[i]<arr[j],求j-i的最大值,首先构建一个严格小压大的栈(不进行弹栈,只压栈),严格小压大就是如果遇到和栈顶相同的数值,不压栈,这样子做的好处是如果后面的数值比栈顶大,那么下标减去先压栈的下标一定大于下标减去后压栈的下标,(一句话,先压栈求的结果更大,更符合题意)。
构建完后,从数组右边开始,比栈顶元素大,计算距离,弹出栈顶元素,(这是因为如果不弹出,下一个比栈顶大的算的距离一定比当前距离小,例如x-y,x一直减小对于从右向左走,y不变对应栈顶不弹出。。。)

int maxWidthRamp(vector<int>& nums) {
	int stack[50001] = { 0 };
	r = 1; // 令r=1相当于0位置进栈了
	int n = nums.size();
	// 构建小压大的单调栈 -- 存储的是数组的下标
	for (int i = 1; i < n; i++) {
		if (nums[stack[r - 1]] > nums[i]) {
			stack[r++] = i;
		}
	}
	int ans = 0;
	// 从右往左找答案,比栈顶元素大,计算答案,栈顶弹出
	for (int j = n - 1; j >= 0; j--) {
		while (r > 0 && nums[stack[r - 1]] <= nums[j]) {
			ans = max(ans, j - stack[--r]);
		}
	}
	return ans;
}


http://www.kler.cn/news/364018.html

相关文章:

  • 【随便聊聊】MySQL数据类型详解:从基础到高级应用
  • 高翔【自动驾驶与机器人中的SLAM技术】学习笔记(十二)拓展图优化库g2o(一)框架
  • win11修改桌面默认路径
  • 用户账户与授权UAA与OAuth2
  • HCIP--1
  • C:浅谈数组指针的声明
  • 无废话、光速上手 React-Router
  • Anaconda从旧版本更新
  • 用nginx实现多ip访问多网址
  • Redis缓存实战-使用Spring AOP和Redis实现Dao层查询缓存优化实战
  • 云原生后端概述
  • RabbitMQ 确认模式(Acknowledgements Mode)详解
  • 海外媒体发稿:外媒宣发之《时代》杂志 TIME 的魅力
  • 使用 NumPy 和 Matplotlib 实现交互式数据可视化
  • 【Android】AHandler/AMessage/ALooper机制
  • Java:关于哈希表
  • 2024年808数据结构答案
  • css知识点梳理
  • 如何在服务器上部署开源大模型 GLM-4-9B-Chat 并应用到RAG应用中
  • 【传知代码】机器学习在情绪预测中的应用(论文复现)
  • 虚拟机的 NAT 模式 或 Bridged 模式能够被外界IPping通
  • IDEA无法生成自动化序列serialVersionUID及无法访问8080端口异常的解决方案
  • 计算机毕业设计PySpark+大模型农产品推荐系统 农产品爬虫 农产品商城 农产品大数据 农产品数据分析可视化 PySpark Hadoop
  • 【亚马逊云】基于 Amazon EKS 搭建开源向量数据库 Milvus
  • 【ArcGIS Pro实操第4期】绘制三维地图
  • Java如何自定义线程池