算法:单调栈和单调队列

  • 一.单调栈和单调队列
  • 二.单调栈例题
    • 1.模板题入门
    • 2.不懂不要急,看这道题
  • 三.单调队列例题
    • 1.入门
    • 2.进阶

一.单调栈和单调队列

单调栈和单调队列与普通的栈,队列不同点就是要维护他们元素的单调性(单增或单减),来实现相应的效果。要注意的是单调栈和单调队列即可以用数组模拟,也可以直接使用STL(更方便易于理解),但是如果用STL的话,单调栈/队列要在开始放入元素之前设置边界,单调递增就在边界(栈顶/队首)赋值为负值(<=0),单调递减就在边界赋值为INF(极大值)。因为如果栈/队列内无元素,那么s.top()是不合法的,这样就无法继续进行插入和删除操作来维护单调性。
如何维护单调:
每输入一个新元素就比较它是否符合单调要求,符合就push进去,不符合就把它前面的pop掉。
单调队列:
例如滑动窗口的要求要最多存几个元素,所以一旦越界就pop,一旦不单调就pop;
单调队列里新人是一定要进来的,老人可能都比新人弱而被全员踢出,但是踢干净以后立刻新人就push_back进来了,所以不会为空
具体的代码就直接看例题,相信您一看就懂。

二.单调栈例题

1.模板题入门

P3467 [POI2008]PLA-Postering
Byteburg市东边的建筑都是以旧结构形式建造的:建筑互相紧挨着,之间没有空间.它们共同形成了一条长长的,从东向西延伸的建筑物链(建筑物的高度不一).Byteburg市的市长Byteasar,决定将这个建筑物链的一侧用海报覆盖住.并且想用最少的海报数量,海报是矩形的.海报与海报之间不能重叠,但是可以相互挨着(即它们具有公共边),每一个海报都必须贴近墙并且建筑物链的整个一侧必须被覆盖(意思是:海报需要将一侧全部覆盖,并且不能超出建筑物链)
输入格式
The first line of the standard input contains one integer nnn (1≤n≤250 000), denoting the number of buildings the chain comprises of.
Each of the following nnn lines contains two integers did_idi​ and wiw_iwi​ (1≤di,wi≤1 000 000 000),
separated by a single space, denoting respectively the length and height of the ithi^{th}ith building in the row.
第一行为一个整数n(1≤n≤250000),表示有n个建筑,接下来n行中,第i行表示第i个建筑物的宽di与高wi(1≤di,wi≤1 000 000 000),中间由一个空格隔开
输出格式
The first and only line of the standard output should contain one integer, the minimum number of rectangular posters that suffice to cover the north faces of the buildings.
第一个为一个整数,表示最少需要几张海报.
输入

5
1 2
1 3
2 2
2 5
1 4

输出

 4
#include<bits/stdc++.h>
using namespace std;
int n,temp,a,b,cnt;
stack<int>s;
int main()
{cin>>n;s.push(-1);//设边界cin>>a>>b;//本题中宽度没用s.push(b);for(int i=2;i<=n;i++){cin>>a>>b;while(s.top()>=b)//一旦单调(递增)被破坏就把大与新人的都pop掉{temp=s.top();if(temp==b)cnt++;//若有相同的则可以省一张海报s.pop();}s.push(b);}cout<<n-cnt<<endl;//输出共需多少张海报即可return 0;
}

2.不懂不要急,看这道题

单调栈这个东西还是很容易理解的,就是一个栈,维护好他的单调性,可以是单调递增也可以是单调递减(或者非严格单增等等 )。写起来非常好写, 就是如果当前要入栈的元素大于栈顶就push进去,如果小于就一直pop,直到当前元素大于栈顶元素或者栈空为止,很容易就可以证明/看出来这个栈依照这样的操作一定能保持单调。那么这样的单调栈到底有什么作用呢 ?比如下面这道题。

输入样例:

7 2 1 4 5 1 3 3
4 1000 1000 1000 1000
0

输出样例:

8
4000

这道题要求最大面积,看上去没什么思路,其实就是单调栈的最基本的应用。
我画几个图就能非常直观地感受这道题了,不过在此之前最好先看一下代码,然后再看图。

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<math.h>
#include<stack>
#include<vector>#define ls (p<<1)
#define rs (p<<1|1)
#define over(i,s,t) for(register int i=s;i<=t;++i)
#define lver(i,t,s) for(register int i=t;i>=s;--i)
//#define int __int128
using namespace std;
typedef pair<double,double> PDD;typedef long long ll;//全用ll可能会MLE或者直接WA,全部换成int看会不会A,别动这里!!!
const int N=100007;
const ll mod=1e9+7;
const double EPS=1e-5;//-10次方约等于趋近为0ll q[N],w[N],h;
int n;int main()
{while(scanf("%d",&n)&&n){memset(q,-1,sizeof q);int top=0;ll ans=0;over(i,1,n+1){if(i!=n+1){scanf("%lld",&h);}else h=0;if(h>q[top])//高于栈顶元素,保持递增就入栈q[++top]=h,w[top]=1;else {ll cnt=0;while(h<=q[top]){//单调被破坏就pop,把所有低于这个元素的全部pop并更新答案。ans=max(ans,(cnt+w[top])*q[top]);cnt+=w[top--];}q[++top]=h;w[top]=cnt+1;}}printf("%lld\n",ans);}return 0;
}

看完代码是不是有一点懂了,那么我们来看图:


首先图一是一组数据,画成图的样子。我们维护单调栈的单调性,直到遇见违反单调性的数,我们pop栈顶元素并更新ans。栈顶大于要入栈的元素,就pop,ans=max(ans,s1)。第二个还是大于,同样的操作,ans=max(ans,s2)。注意这里的s2就是第二个高度乘以2,因为第一个栈顶虽然pop了但是对于第二个栈顶来说增加了它的面积,所以用cnt加上这里的宽度,更新答案。最后恢复单调性,最新的栈顶元素的宽度就是被删除的元素的宽度与自己的总和。删完之后虽然栈里的元素少了几个但是画成图确实没有少元素,只是高度变了。如下图图二:

然后我们继续上面的操作。最后求得最大值,如图3。注意我们要在最后加一个高度为0的数据,为了避免如图4的情况出现。

然后就是《算法竞赛进阶指南》这本书上的代码:

a[n + 1] = p = 0;
for (int i = 1; i <= n + 1; i++) {if (a[i] > s[p]) {s[++p] = a[i], w[p] = 1;} else {int width=0;while (s[p] > a[i]) {width += w[p];ans = max(ans, (long long)width * s[p]);p--;}s[++p] = a[i], w[p] = width + 1;}
}

相信看完图您一定能非常直观地理解其中的奥妙。

三.单调队列例题

1.入门

1
P1440求m区间内的最小值
题目描述
一个含有n项的数列(n<=2000000),求出每一项前的m个数到它这个区间内的最小值。若前面的数不足m项则从第1个数开始,若前面没有数则输出0。

输入:
6 2
7 8 1 4 3 2
输出:
0
7
7
1
1
3

用STL中的deque实现的单调队列
注意这道题中新加入的成员是不能用的,必须等一个回合才能使用输出,所以应该模拟这个过程每次加入一个输出的是上一个加入时的最小值,所以只需要i<n即可,第n个数没用

#include<bits/stdc++.h>
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<stdio.h>
#include<cmath>
#define debug cout<<"ok"<<endl
typedef long long ll;
const int maxn=20000010;
const int mod=1e9+7;
using namespace std;
struct node
{int index,vis;//index表示入队时间(序号),vis表示大小(权值)
}a[maxn];
deque<node>q;
int n,m,minn[maxn];
int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++){scanf("%d",&a[i].vis);a[i].index=i-1;}for(int i=1;i<n;i++){if(q.empty())printf("0\n");while(!q.empty()&&q.back().vis>=a[i].vis)//维护队列两端的数据q.pop_back();//题目要求最小值,大于当前的值就直接pop掉q.push_back(a[i]);//每一个都push_back进去while(!q.empty()&&q.front().index+m<i)//超过长度就把前面超的pop掉q.pop_front();printf("%d\n",q.front().vis);}return 0;
}

2
P1886 滑动窗口 /【模板】单调队列
题目描述 有一个长为 nnn 的序列 aaa,以及一个大小为 kkk 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。
例如:
The array is [1,3,−1,−3,5,3,6,7], and k=3。

输入格式 输入一共有两行,第一行有两个正整数 n,kn,kn,k。
第二行 nnn 个整数,表示序列 aaa
输出格式 输出共两行,第一行为每次窗口滑动的最小值

输入
8 3
1 3 -1 -3 5 3 6 7

输出
-1 -3 -3 -3 3 3
3 3 5 5 6 7

第二行为每次窗口滑动的最大值

#include<bits/stdc++.h>
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<stdio.h>
#include<cmath>
#include<deque>
#define debug cout<<"ok"<<endl
typedef long long ll;
const int maxn=1e7+10;
const int mod=1e9+7;
using namespace std;
struct node
{int index,vis;
}f[maxn];
deque<node>q1,q2;
int n,m,ans1[maxn],ans2[maxn],cnt;
int main()
{ios::sync_with_stdio(false);cin>>n>>m;for(int i=1;i<=n;i++)cin>>f[i].vis,f[i].index=i;for(int i=1;i<=n;i++){while(!q1.empty()&&f[i].vis>=q1.back().vis)q1.pop_back();while(!q2.empty()&&f[i].vis<=q2.back().vis)q2.pop_back();q1.push_back(f[i]);q2.push_back(f[i]);while(!q1.empty()&&q1.front().index+m<=i)q1.pop_front();//区间要确定,别忘了加'='号while(!q2.empty()&&q2.front().index+m<=i)q2.pop_front();if(i>=m)ans1[cnt]=q1.front().vis,ans2[cnt++]=q2.front().vis;}for(int i=0;i<cnt;i++)cout<<ans2[i]<<" ";cout<<endl;for(int i=0;i<cnt;i++)cout<<ans1[i]<<" ";cout<<endl;return 0;
}

2.进阶

P1714 切蛋糕
题目描述
今天是小Z的生日,同学们为他带来了一块蛋糕。这块蛋糕是一个长方体,被用不同色彩分成了N个相同的小块,每小块都有对应的幸运值。
小Z作为寿星,自然希望吃到的第一块蛋糕的幸运值总和最大,但小Z最多又只能吃M小块(M≤N)的蛋糕。
吃东西自然就不想思考了,于是小Z把这个任务扔给了学OI的你,请你帮他从这N小块中找出连续的k块蛋糕(k≤M),使得其上的幸运值最大。
输入格式
输入文件cake.in的第一行是两个整数N,M。分别代表共有N小块蛋糕,小Z最多只能吃M小块。
第二行用空格隔开的N个整数,第i个整数Pi代表第i小块蛋糕的幸运值。
输出格式
输出文件cake.out只有一行,一个整数,为小Z能够得到的最大幸运值。

输入:
5 2 1 2 3 4 5
输出:
9

我发现这道题的题解中大多数单调队列用做的都是错的!!(不仅是用单调队列,题解中其他的方法基本都能被hack)

不信你试试5 2 5 4 3 2 1,题解中大多数输出的都是7或者1 1 5,输出的都是0
主要是这道题数据太水了

所以我决定来给出一个用STL做的正确解法,不能误导别人呀

其他题解之所以会被hack是因为他们光顾着维护队列单调递增(前缀和递增才会保证最大),忘了万一数据是单调递减怎么办。所以我们应该在维护递增之前就判断现在的答案是否为最优。为了达到这个目的我们应该先给队列赋初值0,因为sum[i]-sum[q.front()]这一句,不赋初值就出bug了,正好赋初值之后就可以避免第一个值是最大的,其余都是负的(如5 2 1 -10 -10 -10 -10 -10)这种丧心病狂的数据了。

#include<bits/stdc++.h>
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<stdio.h>
#include<cmath>
#include<deque>
#define debug cout<<"ok"<<endl
typedef long long ll;
const int maxn=1e7+10;
const int mod=1e9+7;
using namespace std;
int ans=-233333333,n,m,a,sum[maxn];
deque<int>q;
int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++){scanf("%d",&a);sum[i]=sum[i-1]+a;//前缀和}q.push_back(0);//赋初值for(int i=1;i<=n;i++){while(q.front()+m<i)q.pop_front();//越界就popans=max(ans,sum[i]-sum[q.front()]);while(!q.empty()&&sum[q.back()]>=sum[i])//递减就popq.pop_back();q.push_back(i);}printf("%d\n",ans);return 0;
}

注:如果您通过本文,有(qi)用(guai)的知识增加了,请您点个赞再离开,如果不嫌弃的话,点个关注再走吧,日更博主每天在线答疑 ! 当然,也非常欢迎您能在讨论区指出此文的不足处,作者会及时对文章加以修正 !如果有任何问题,欢迎评论,非常乐意为您解答!( •̀ ω •́ )✧

【数据结构】单调栈和单调队列 详解+例题剖析相关推荐

  1. 【树形DP】树形DP入门详解+例题剖析

    树形DP 树形DP准确的说是一种DP的思想,将DP建立在树状结构的基础上.整体的思路大致就是用树形的结构存储数据. 要学树形DP之前肯定是要先学会树和图的呀,至少先学会链式前向星,不会的话可以看一下我 ...

  2. ❤️数据结构之栈(图文版详解)❤️

    目录 一,写在前面 二,栈的定义 1,栈的定义 2,进栈出栈变化形式 三,栈的抽象数据类型 四,栈顺序存储结构及实现 1,栈的顺寻存储结构 2,栈的顺序存储结构--进栈操作 3,栈的顺序存储结构--出 ...

  3. c语言栈的实现以及操作_数据结构之链栈基本操作的实现详解(C语言描述)

    迎新过后,来带领你好好学习的小软准时归来,快带着上次学习链表操作的记忆和我开启新的旅程吧: 链栈:就是栈的链式存储结构,简称链栈. 首先我们要考虑的就是链栈的存储结构,由于栈只是在栈顶进行插入和删除操 ...

  4. 【基础算法】二分法(二分答案,二分查找),三分法,Dinkelbach算法,算法详解+例题剖析

    目录 一 . 二分法 二分搜索得要求: 二分查找步骤: 二分答案: 玄学的二分(二分答案) 二 . 三分法 例题 三.01分数规划问题相关算法与题目讲解(二分法与Dinkelbach算法) 一 . 二 ...

  5. 【算法】差分与前缀和 算法详解+例题剖析

    目录 一.前缀和 二. 差分思想 1.静态数组的区间求和问题 2.静态维护区间加等差数列的求和问题 三.二维前缀和 二维前缀和例题P2280 [HNOI2003]激光炸弹 四.例题 例题一:差分+前缀 ...

  6. JavaScript数据结构与算法——队列详解(下)

    接下来会借助本人另一篇文章JavaScript数据结构与算法--队列详解(上)中实现的队列类及其方法实现一个应用. 配对问题 需求分析:在一个文件中保存着一份男女混合的数据,名称前以B开头表示男士,以 ...

  7. 刷题关键点总结-单调栈、单调队列

    单调栈,单调队列顾名思义,栈内或者队列内元素具有一致下降或者一致上升的特点. 选用单调栈或者队列的出发点: 使用单调栈或者单调队列一般具有三个出发点: 1.求栈或队列内某个元素左侧或者右侧第一个比它大 ...

  8. 单调栈与单调队列简单例题

    单调栈与单调队列简单例题 单调栈: POJ3250 题意:有n只奶牛排成一列向右看,每头奶牛只能看到比自己矮的奶牛,即会被高的奶牛挡住后面,问共有多少只奶牛能被看到 思路:考虑每头奶牛能被前面牛看到的 ...

  9. 【数据结构】共享栈详解 判断共享栈满条件栈顶指针变化详解记忆方法例题

    摘要:简单易懂,详细地介绍共享栈概念,指针,判断共享栈栈满条件以及记忆方法等 目录 共享栈概念 栈顶指针&变化详解 栈顶指针种类的记忆方法 判断栈满条件 判断栈满条件的记忆方法 例题 解题思路 ...

最新文章

  1. 安卓中事件绑定的写法
  2. SAP Fiori Elements - how to set breakpoint to get converted xml view parsed by f
  3. Java数组,字符串
  4. Java中怎么把文本追加到已经存在的文件
  5. android com.squareup,android – 无法导入com.squareup.okhttp.OkHttpClient;
  6. JavaScript数据结构——字典(Dictionary)
  7. 用python重温统计学基础:描述性统计分析
  8. Kettle使用教程之数据同步
  9. 企业设计图纸 无纸化图纸管理方案
  10. 三维地图之cesium入门使用
  11. 选择H5响应式网站建设的主要原因
  12. CodingTrip - 携程编程大赛-第二题-携程员工运动会场地问题
  13. 你和语言模型,谁的填空能力更强?
  14. java for循环 等待_在forEach循环中使用异步/等待
  15. 转:移动互联:没有新故事没有新大佬
  16. 解决苹果手机滑动卡顿
  17. ئاندرويىد تېلېفۇندىكى ئۇيغۇرچە
  18. 默纳克3000服务器改协议,默纳克3000参数设置说明
  19. php 静态配置文件问题,Nginx+PHP 配置漏洞:静态文件都可以当作 PHP 解析
  20. 管理信息系统【二】之 管理信息系统与组织变革

热门文章

  1. 基于OpenCV实战:对象跟踪
  2. HyperPocket:生成点云网络
  3. 理解shared_ptrT
  4. 拿haojava官方过程
  5. 小shell脚本---查找目录下面包含string的文件
  6. 胳膊上无缘无故起了个大包
  7. iis7 php oracle,PHP+IIS7配置OCI8链接Oracle 10G的方法
  8. centos在文本中搜索字符串_CentOS查找所有文件中某个字符串
  9. 爬虫基础分享Scrapy框架流程图与安装
  10. 被大众群体吹的神乎其神的Python到底都能干什么