单调栈与单调队列

article/2025/9/30 22:04:08

文章目录

  • 单调栈与单调队列
    • 一、单调栈
      • 1.单调递增栈
      • 2.单调递减栈
      • 总结
    • 二、单调队列(单调双端队列)
  • 单调栈与单调队列总结:


单调栈与单调队列

单调栈就是栈内元素满足单调性的栈结构。此处的单调性分为单调递增与单调递减

如何维护一个单调栈:
单调递增栈:在保持栈内元素单调递增的前提下(如果栈顶元素大于要入栈的元素,将将其弹出),将新元素入栈。

单调递减栈:在保持栈内元素单调递减的前提下(如果栈顶元素小于要入栈的元素,则将其弹出),将新元素入栈。

什么时候使用单调栈:

  • 给定一个序列,求序列中的每一个数左边/右边第一个比他小/比他大的数;

  • 给定一个序列,求序列中的每一个数左边/右边第一个比他小/比他大的数在什么地方;

    注:寻找位置时(下标时)stk栈存储的是下标,而不是数值

一、单调栈

1.单调递增栈

单调递增栈应用:从左往右遍历——可以找到第一个比它小的元素的位置

单调递增栈就是栈内元素满足单调递增,假设当前元素为 x ,若栈顶元素 < x ,则将x 入栈否则(栈顶元素 >= x)不断弹出栈顶元素,直至栈顶元素 < x

(1)单调递增栈求左边第一个比它小的数

求序列中每一个数左边第一个比它小的数

for(int i = 1; i <= n; i ++)
{int x;cin >> x;while(tt != 0 && stk[tt] >= x) tt --;if(tt == 0) cout << "-1" << " ";else cout << stk[tt] << " ";stk[++ tt] = x;
}

(2)单调递增栈求左边第一个比它小的数的位置

求序列中每一个数左边第一个比它小的数的位置

    stack<int> s;for(int i = 1; i <= n; ++i){while(s.size() && a[s.top()] >= a[i]) s.pop();if(s.empty()) l[i] = 0;else l[i] = s.top();s.push(i);}
// 数组模拟栈    for(int i = 1; i <= n ; i ++ ){while(tt != 0 && a[stk[tt]] >= a[i]) tt--;if(tt == 0) l[i] = 0; // 栈为空,不存在则记为0else l[i] = stk[tt]; // 符合要求l[i]记录对应栈顶下标stk[++ tt] = i; // 最后当前扫描到的下标入栈}

下面以3 1 4 5 2 7为例解释单调递增栈

【文字描述】

image

image

【动图展示】

通过观察原始序列 3 1 4 5 2 7,对比结果的单调栈1 2 7可以发现 1 是 2 左边第一个小于等于它的数,稍加思考后,我们可以得知当一个数字被放入单调递增栈时,其栈内左边的数是它在原始序列中,左边第一个小于等于它的数。

【acwing 单调栈】

给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。

输入格式
第一行包含整数 N,表示数列长度。

第二行包含 N 个整数,表示整数数列。

输出格式
共一行,包含 N 个整数,其中第 i 个数表示第 i 个数的左边第一个比它小的数,如果不存在则输出 −1。

数据范围
1≤N≤105
1≤数列中元素≤109
输入样例:
5
3 4 2 7 5
输出样例:
-1 3 -1 2 2

暴力代码:超时

#include<iostream>using namespace std;
const int N = 1e5 + 10;
int a[N];int main() {int n;cin >> n;for (int i = 0; i < n; i++) scanf("%d", &a[i]);printf("-1 ");for (int i = 1; i < n; i++) {for (int j = i - 1;; j--) {if (j >= 0 && a[j] < a[i]) {cout << a[j] << " ";break;} else if (j < 0) {cout << "-1 ";break;}}}return 0;
}

单调栈:O(n)

#include<iostream>using namespace std;
const int N = 100000+10;
int stk[N],tt;
int main()
{int n;cin>>n;for(int i = 1; i <= n; i++){int x;cin>>x;while(tt != 0 && stk[tt] >= x) tt--; //如果栈顶元素大于当前待入栈元素,则出栈if(tt == 0) cout<<"-1 "; // 栈为空(无满足要求的数)else cout<<stk[tt]<<" "; // 满足要求输出栈顶元素stk[++ tt] = x; // 当前元素入栈}return 0;
}

寻找位置:

假设有一个单调递增的栈 stk和一组数列: a = { 5 3 7 4}
用数组L[i]表示 第i个数向左遍历的第一个比它小的元素的位置

如何求L[i]?

3.1 朴素的算法 O(n^2)
可以按顺序枚举每一个数,然后再依此向左遍历。 但是当数列单调递减时,复杂度是严格的O(n^2)。

3.2 单调栈 O(n)
我们按顺序遍历数组(i : 1 -> n),然后构造一个单调递增栈。栈中存放的是元素下标,而非元素本身。

    {5 3 7 4}

(1)i = 1时,因为栈为空,L[1] = 0,此时再将第一个元素的位置下标1存入栈中。
此时栈中情况:

在这里插入图片描述

(2)i = 2时,因当前元素a[i] = 3小于栈顶元素下标1对应的元素a[1] = 5,故将下标1弹出栈, 此时栈为空 ,故L[2] = 0 。然后将元素3对应的位置下标2存入栈中。
此时栈中情况:

在这里插入图片描述

(3)i = 3时,因当前元素a[i] = 7大于栈顶元素下标2对应的元素a[2] = 3,故
L[3] = stk[tt] = 2 (栈顶元素的值,说明第一个比它小的元素的下标为多少),然后将元素7对应的下标3存入栈 。
此时栈中情况:

在这里插入图片描述

(4)i = 4时,因当前元素a[i] =4小于栈顶元素下标3对应的元素a[3] = 7,为保持单调递增的性质,应将栈顶元素下标3弹出 ,而当前元素a[i] =4大于弹出元素后的栈顶元素下标2对应的元素a[2] = 3,不需要再继续弹出, 此时 L[4] = stk[tt] = 2;然后将元素4对应的下标4存入栈。
此时栈中情况:
在这里插入图片描述

(5)至此 算法结束

对应的结果:
a : 5 3 7 4
L : 0 0 2 2

下一个单调递减栈的模拟画图过程也大致同上,只不过是从后往左遍历罢了!

【将上题目答案改成寻找位置】

#include<iostream>using namespace std;
const int N = 100000+10;
int stk[N],tt,l[N],a[N];
int main()
{int n;cin>>n;for(int i = 1; i <= n; i++) cin>>a[i];for(int i = 1; i <= n; i++){while(tt != 0 && a[stk[tt]] >= a[i]) tt--; //如果栈顶元素大于当前待入栈元素,则出栈if(tt == 0) l[i] = 0; // 栈为空(无满足要求的数)else l[i] = stk[tt]; // 满足要求输出栈顶元素stk[++ tt] = i; // 当前元素入栈}for(int i = 1; i <= n; i++) cout<<l[i]<<" ";return 0;
}

2.单调递减栈

单调递减栈应用:从右往左遍历————寻找左边边第一个比当前元素大的数/数的位置

(1)单调递减栈求左边第一个比它大的数

求序列中每一个数左边第一个比它小的数

for(int i = 1; i <= n; i ++)
{int x;cin >> x;while(tt != 0 && stk[tt] <= x) tt --;if(tt == 0) cout << "-1" << " ";else cout << stk[tt] << " ";stk[++ tt] = x;
}

(2)单调递减栈求左边第一个比它大的数的位置

    // 单调递减栈:从右往左遍历————寻找右边第一个比当前元素大的数的位置for(int i = n; i >= 0; i -- ){while(tt != 0 && a[stk[tt]] <= a[i]) tt--;if(tt == 0) l[i] = 0; // 栈为空,不存在则记为0else l[i] = stk[tt]; // 符合要求l[i]记录对应栈顶下标stk[++ tt] = i; // 最后当前扫描到的下标入栈}

【acwing 仰视奶牛】

约翰有 N 头奶牛,编号为 1 到 N。

现在这 N 头奶牛按编号从小到大的顺序站成了一排,其中奶牛 i 的身高为 Hi。

现在,每头奶牛都向它的右侧望向那些编号较大的奶牛,对于奶牛 i 如果存在一头奶牛 j 满足 i<j 并且 Hi<Hj,那么我们称奶牛 i 需要仰视奶牛 j。

请你求出每头奶牛的最近仰视对象。

输入格式

第一行包含整数 N。

接下来N 行,每行包含一个整数 Hi,其中第 i 行的数为编号为 i 的奶牛的高度。

输出格式

共 N 行,每行输出一个整数,其中第 i 行的输出整数表示编号为 i 的奶牛的最近仰视对象的编号,如果不存在仰视对象,则输出 00。

数据范围

1≤N≤105,
1≤Hi≤106

输入样例:

6
3
2
6
1
1
2

输出样例:

3
3
0
6
6
0

#include<iostream>using namespace std;
const int N = 100000+10;
int tt, a[N], l[N], stk[N]; //stk[N]: 存储栈顶下标;l[N]记录答案int main()
{int n;cin>>n;for(int i = 1; i <= n; i++) scanf("%d",&a[i]);// 单调递减栈:从右往左遍历————寻找右边第一个比当前元素大的数的位置for(int i = n; i >= 0; i -- ){while(tt != 0 && a[stk[tt]] <= a[i]) tt--;if(tt == 0) l[i] = 0; // 栈为空,不存在则记为0else l[i] = stk[tt]; // 符合要求l[i]记录对应栈顶下标stk[++ tt] = i; // 最后当前扫描到的下标入栈}for(int i = 1; i <= n; i++) printf("%d\n", l[i]);return 0;
}

总结

至此我们可以解答最开始的疑问,单调栈的根本作用在于求得「每一个数字在原始序列中左 / 右边第一个大于 / 小于它自身的数字或者位置」,并且由于每一个数字只会入栈一次且最多出栈一次,因此总的时间复杂度为 O ( n ) 。

一定要手动模拟画出过程图。记住:入栈操作是最后一步,别先入栈了在判断!

二、单调队列(单调双端队列)

说单调队列,那我们就先说说这个单调队列是个什么物种。单调队列从字面上看,无非就是有某种单调性的队列,没错,这就是所谓的单调队列。 单调队列它分两种,一种是单调递增的,另外一种是单调递减的。

在这搬出百度百科的解释:不断地向缓存数组里读入元素,也不时地去掉最老的元素,不定期的询问当前缓存数组里的最小的元素。

用单调队列来解决问题,一般都是需要得到当前的某个范围内(连续的字序)的最小值或最大值

队列中元素之间的关系具有单调性,且队首和队尾都可以进行出队操作,只有队尾可以进行入队操作。
用处:

  • 对于维护好的单调队列,对内元素是有序的,那么取出最大值(最小值)的复杂度为O(1)
  • 可以拿来优化DP(…)

单调队列:通俗的讲就是以一个固定长度的窗口在数列上移动,(这个固定的长度是题目给的,队首元素在队列中的位置与队尾元素在队列的位置之差若大于这个固定长度,就得将队首元素弹出)

【acwing滑动窗口】

给定一个大小为n≤106 的数组。

有一个大小为 kk 的滑动窗口,它从数组的最左边移动到最右边。

你只能在窗口中看到 kk 个数字。

每次滑动窗口向右移动一个位置。

以下是一个例子:

该数组为 [1 3 -1 -3 5 3 6 7],k 为 3。

窗口位置最小值最大值
[1 3 -1] -3 5 3 6 7-13
1 [3 -1 -3] 5 3 6 7-33
1 3 [-1 -3 5] 3 6 7-35
1 3 -1 [-3 5 3] 6 7-35
1 3 -1 -3 [5 3 6] 736
1 3 -1 -3 5 [3 6 7]37

你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。

输入格式

输入包含两行。

第一行包含两个整数 n 和 k,分别代表数组长度和滑动窗口的长度。

第二行有 n 个整数,代表数组的具体数值。

同行数据之间用空格隔开。

输出格式

输出包含两个。

第一行输出,从左至右,每个位置滑动窗口中的最小值。

第二行输出,从左至右,每个位置滑动窗口中的最大值。

输入样例:

8 3
1 3 -1 -3 5 3 6 7

输出样例:

-1 -3 -3 -3 3 3
3 3 5 5 6 7

image

image

#include<iostream>using namespace std;const int N = 1e6 + 10; // 注意题目数组的范围int a[N], q[N];int main()
{int n, k;cin>>n >> k;for(int i = 0; i < n; i++) cin>>a[i];int hh = 0, tt = -1;for(int i = 0; i < n; i++) // 暴力模拟{	// 判断对头下标是否超出了 i - k + 1 ~ i 这个范围,若超出则去掉队头下标// 判断是否在(i - k + 1, i)这个范围内 下标从0开始的因此要加1if(hh <= tt && i - k + 1 > q[hh]) hh ++;// 单调递增队列求当前区间最小值while(hh <= tt && a[q[tt]] >= a[i]) tt --;// 当前下下标入队q[++ tt] = i;//输出窗口(每个区间范围的最小值)if(i >= k - 1) cout<< a[q[hh]]<<" ";}cout<<endl;hh = 0, tt = -1; // 跟上述是对称的for(int i = 0; i < n; i++) // 暴力模拟{// 判断是否滑出窗口if(hh <= tt && i - k + 1 > q[hh]) hh ++; // 单调递减队列求当前区间最da值while(hh <= tt && a[q[tt]] <= a[i]) tt --;// 当前下下标入队q[++ tt] = i;//输出窗口(每个区间范围的最da值)if(i >= k - 1) cout<< a[q[hh]]<<" ";}return 0;
}

单调栈与单调队列总结:

(1)先用栈/队列来暴力模拟问题,即清除暴力朴素算法

(2)观察栈/队列中那些元素是没有用的,将这些没有用的数全部删掉

(3)剩下的元素具有单调性就可以进行优化

  1. 求极值:端点
  2. 找某个数——二分

参考文献:
acwing算法基础课


http://chatgpt.dhexx.cn/article/8Pex7bSc.shtml

相关文章

详解单调栈算法

前言 嘿&#xff01;彩蛋&#xff01;感觉有帮助就三连呗&#xff01; 如果你对这篇文章可感兴趣&#xff0c;可以点击「【访客必读 - 指引页】一文囊括主页内所有高质量博客」&#xff0c;查看完整博客分类与对应链接。 栈属于基础数据结构之一&#xff0c;基础到仅用「后进…

单调栈

定义&#xff1a; 单调栈&#xff0c;顾名思义就是栈内元素单调按照递增(递减)顺序排列的栈。 单调递增栈&#xff1a; ①在一个队列中针对每一个元素从它右边寻找第一个比它小的元素 ②在一个队列中针对每一个元素从它左边寻找第一个比它小的元素 单调递减栈&#xff1a; …

单调栈(C/C++)

目录 1. 单调栈的定义 2. 单调栈的常见用途 3. 案例分析 3.1 暴力解法 3.2 单调栈 4. 单调栈总结 1. 单调栈的定义 单调栈顾名思义&#xff0c;就是栈内的元素是单调的。根据栈内元素的单调性的不同&#xff0c;可以分为&#xff1a; 单调递增栈&#xff1a;栈内元素是单…

【算法】单调栈

目录 单调栈的定义&#xff1a;伪代码&#xff1a;应用1.模板题2.视野总和问题3.柱状图中的最大矩形4.最大区间 碎碎念&#xff1a; 单调栈的定义&#xff1a; 从名字上就能猜出来&#xff0c;这种数据结构在栈的基础上&#xff0c;栈内的元素是单调有序的&#xff0c;所以单调…

单调栈详解

定义&#xff1a; 单调栈&#xff0c;顾名思义就是栈内元素单调按照递增(递减)顺序排列的栈。 适用问题&#xff1a; 要知道单调栈的适用于解决什么样的问题&#xff0c;我们首先需要知道单调栈的作用。单调栈分为单调递增栈和单调递减栈&#xff0c;通过使用单调栈我们可以访…

[数据结构]——单调栈

单调栈 笔者在做leetcode的题(下一个出现的最大数字)时&#xff0c;接触到了单调栈这一种数据结构&#xff0c;经过研究之后&#xff0c;发现单调栈在解决某些问题时出奇的好用&#xff0c;下面是对单调栈的性质和一些典型题目。 什么是单调栈&#xff1f; 从名字上就听的出…

scp出现错误的解决办法

scp往远程主机上传送rmandata时报了如下错误&#xff1a; (看不见的放大) 我的做法是&#xff1a; 在执行scp的主机提示的scp目录 vi ~/.ssh/known_hosts 删除我红色部分遮盖的主机ip那一行&#xff0c;故障解决 来自 “ ITPUB博客 ” &#xff0c;链接&#xff1a;http://blo…

关于 fatal error LNK1158: 无法运行“rc.exe” 的解决方法

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://blog.csdn.net/qq21497936/article/details/110680001 各位读者&#xff0c;知识无穷而人力有穷&#xff0c;要么改需求&#xff0c;要么找专业人士&#xff0c;要么自己研究 红胖子(红模仿…

VS发生RC1107错误的原因

最近MFC程序中&#xff0c;用VS的资源编辑打开时&#xff0c;老是发生 fatal error RC1107: invalid usage; use RC /? for Help 这种错误&#xff0c;记得前几天解决过一次&#xff0c;但是当时忘了怎么解决的了。今天每建一个新的工程都遇到这个问题&#xff0c;郁闷坏了&a…

错误 LINK : fatal error LNK1158: 无法运行“rc.exe”

2019独角兽企业重金招聘Python工程师标准>>> 问题 软件环境&#xff1a;Windows 10 Pro Visual Studio 2015 然后安装了 Windows 10 SDK Windows 10 SDK 是用这个 ISO 文件安装的&#xff1a;17134.12.180419-0858.rs4_release_svc_prod2_WindowsSDK.iso 在 Visual…

[rtsp @ 0x55ba1dae9200] UDP timeout, retrying with TCP的解决办法

使用ffmpeg 进行解码rtsp的时候出现: [rtsp @ 0x55ba1dae9200] UDP timeout, retrying with TCP如下所示: 需要使用接口指定以下tcp连接就可以解决了。 具体的代码如下: // rtsp:tcpAVDictionary* options = NULL;av_dict_set(&options, "rtsp_transport", …

brpc源码解析(二)—— brpc收到请求的处理过程

文章目录 一、基本设计思路二、实现细节三、总结 作为rpc服务器&#xff0c;在启动过后&#xff0c;最主要的一个过程就是收到请求后的处理&#xff0c;而这就牵涉到一个网络编程相关最基本的部分&#xff1a;如何有效地处理socket传过来地数据&#xff0c;这篇文章就来详细聊一…

libpng warning iCCP 错误处理方法

png图片缺乏某些库&#xff0c;导致损坏&#xff0c;或者多余了一些数据会导致以下报错&#xff1a; libpng warning: iCCP: known incorrect sRGB profile libpng warning iccp extra compressed data一些可能的解决方案&#xff1a; 已有方案 来自&#xff1a;https://blo…

创建线程提示SCB_CFSR_BFSR:0x04 IMPRECISERR 错误

在RTthread的编程出现了问题 这种问题并不是系统出现的问题&#xff0c;而是在处理自己的函数内部出现了数组越界&#xff0c;内存出现错误导致的。 关键还是自己指针访问的非法访问导致这些问题。遇到问题还是要多检查自己写的接口问题。

无法运行rc.exe(已解决)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言原因解决一&#xff1a;在C:\Program Files (x86)下搜索rc.exe二&#xff1a;右击&#xff0c;打开找到的rc.exe的文件夹然后进入Visual Studio 2019 前言 解决…

RTCP(一): RR--Receiver Reports 接收者报告

RTCP RR的格式 接受者报告的RTCP类型是201&#xff0c;如图1.1所示。 图1.1 reporter ssrc rr报告发送者的ssrc&#xff0c;也就是rtp报文接受者自己的ssrc. reportee ssrc rr报告接受者的ssrc&#xff0c;也就是rtp报文发送者的ssrc. cumulative number of packet lo…

MFC生成错误msado15.tlh(3991):fatal error C1003: 错误计数超过100;正在停止编译

MFC生成过程产生错误msado15.tlh(3991): fatal error C1003: 错误计数超过 100&#xff1b;正在停止编译 1 问题描述 在MFC生成解决方案过程中&#xff0c;当点击工具栏的生成按钮时&#xff0c; 会出现编译错误的情况&#xff1a; msado15.tlh(3991): fatal error C1003: 错…

webrtc编译中的错误解决

webrtc编译记录 错误1&#xff1a;该错误的意思是python的安装路径要和你此时的webrtc源码的编译路径相同。 解决方法&#xff1a;将python的安装路径和webrtc编译源码的路径放在同一个磁盘下。 错误2&#xff1a;Windows 默认不支持文件名或目录名长于260个字符的&#xff…

rvtptcontrol failed

rvtptcontrol failed RVT00-010:子例行程序 rvtooperation()返回错误。 原因&#xff1a;子例行程序 rvtooperation() 返回内部错误。返回的错误信息为&#xff1a; 措施&#xff1a; 请记下此错误编号以及无法读取例程 &routine 中配置文件选项 INV_DEBUG_TRACE 的值”这…

error C2338: /RTCc rejects conformant code错误解决

在编译一个项目时&#xff0c;发现在调试版本时提示这个出错&#xff1a; 1>------ 已启动生成: 项目: simulation2, 配置: Debug Win32 ------1>precompiled.cpp1>C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\yvals.h(112): error C2338: /RTC…