单调栈
定义:栈内元素单调按照递增(递减)顺序排列的栈
基本作用:可以从数组中找到左右两边比x大(小)的数,时间复杂度为O(n)
单调栈的基本操作:
●为了维护栈的单调性,在进栈过程中需要进行判断,具体进栈过程如下:假设当前进栈元素为e,
●对于单调递减栈,从栈顶开始遍历元素,把小于e或者等于e的元素弹出栈,直至遇见一个大于e的元素或者栈为空为止,然后再把e压入栈中,这样就能满足从栈底到栈顶的元素是递减的。
●对于单调递增栈,则每次弹出的是大于e或者等于e的元素,直至遇见一个小于e的元素或者栈为空为止。
代码模板
a[n+1]=2e+9;
for (int i = 1; i <= n; i ++ )
{
while (tt && a[s[tt]]>=a[i]) tt -- ;
stk[ ++ tt] = i;
}
模板题
洛谷题库P5788[单调栈]
#include <iostream>
#include <cstdio>
#define INF 3000005
//头文件+预处理名。
using namespace std;
int n,a[Inf],q[INF],r,f[INF];
//定义变量,其中a数组表示输入的数,q数组表示存下标的单调栈,f数组是存结果。
int main() {
scanf("%d",&n);
for (int i=1; i<=n; i++) scanf("%d",&a[i]);
//上面是读入。
for (int i=n; i>=1; i--) {
//从最后开始枚举。
while (a[i]>=a[q[r]] && r>0) r--;
//如果说它找到了比矮高的人并且不是最后,那么将那个矮的人弹掉,毕竟后面也不会有人看到它了,因为如果看到它的话那么必定可以看到比它前面的还比它高的。
f[i]=q[r];
//存结果,因为要正向输出。
q[++r]=i;
//将它入栈。
}
for (int i=1; i<=n; i++) printf("%d ",f[i]);
//最后正着输出。
return 0;
}
专项题
poj3494[求最大全1子矩阵]
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n,m,h[100005],top,ans;
struct Node {
int h,sta;//sta表示高度h的起始下标
}s[100005];
int main() {
int hh,t;
while(scanf("%d%d",&n,&m)==2) {
memset(h,0,sizeof(h));
ans=0;
for(int i=1;i<=n;++i) {
for(int j=1;j<=m;++j) {
scanf("%d",&hh);
h[j]=hh==0?0:h[j]+1;
}
t=m;
h[++t]=-1;//令最后一个元素的下一个高度为-1,避免循环完毕后还要弹出栈中所有元素
s[0].h=-1;
s[0].sta=top=0;
for(int k=1;k<=t;++k) {
if(h[k]>=s[top].h) {
s[++top].h=h[k];
s[top].sta=k;//其起始下标就是自己的下标
}
else {
while(h[k]<s[top].h) {
ans=max(ans,(k-s[top].sta)*s[top].h);
--top;//弹出栈顶元素
}
s[++top].h=h[k];//其起始下标是弹出的最后一个元素的起始下标
}
}
}
printf("%d\n",ans);
}
return 0;
}
单调队列
基本作用:解决滑动窗口的问题,这个问题是这样的:有一数列{an}和m个区间[L(i),R(i)],满足L(i)<=L(i+1),
R(i)<=R(i+1),对每个区间求区间最大值。时间复杂度O(n)
模板题
洛谷题库P1886[滑动窗口]
以求最小值为例观察队列中元素离开队列的情况:
1. 元素Vi从队尾离开队列:
i < j 且 Vi >= Vj,Vi没有参与求min的必要
2. 元素Vi从队首离开队列:
Vi超出了当前窗口的范围。
#include<cmath>
#include<queue>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
int a[1000001],n,wide,q[1000001],head,tail;
int main(){
scanf("%d%d",&n,&wide);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
head=1;tail=0;
for(int i=1;i<=n;i++)
{
while(tail>0&&head<=tail&&i-q[head]>=wide)//对于超出窗口范围的元素,出队
head++;
while(tail>0&&head<=tail&&a[q[tail]]>a[i])//对于比新元素更大的元素,出队
tail--;//通过这个操作能始终保持范围内最小的元素位于队首,且队列里的元素是单调递增的
q[++tail]=i;//入队
if(i>=wide) printf("%d ",a[q[head]]);//在元素已经足够多个时输出
}
head=1;tail=0;
memset(q,0,sizeof(q));
printf("\n");
for(int i=1;i<=n;i++)//同上寻找最小值的方法寻找最大值
{
while(tail>0&&head<=tail&&i-q[head]>=wide)
head++;
while(tail>0&&head<=tail&&a[q[tail]]<a[i])
tail--;
q[++tail]=i;
if(i>=wide) printf("%d ",a[q[head]]);
}
return 0;
}
经典题
一本通1598[最大连续和]
●把前缀和当做值跑单调队列。
●求出前缀和sum[i],计算[l,r]区间和,就转化成了求sum[r]-sum[l-1]。
●枚举右端点i,则问题变为:找到一个左端点j,i−m<=j<=i−1且sum[j]最小。
●找到一个最小的前缀和(这样就可以保证这个区间内的和最大),用
sum[i]-这个最小前缀和sum[j]就是这个区间的最大值了,用ans来记录一下最大值即可。
#include <bits/stdc++.h>
using namespace std;
typedef int ll;
inline ll read()
{
ll s=0;
bool f=0;
char ch=' ';
while(!isdigit(ch))
{
f|=(ch=='-');
ch=getchar();
}
while(isdigit(ch))
{
s=(s<<3)+(s<<1)+(ch^48);
ch=getchar();
}
return (f)?(-s):(s);
}
#define R(x) x=read()
inline void write(ll x)
{
if(x<0)
{
putchar('-');
x=-x;
}
if(x<10)
{
putchar(x+'0');
return;
}
write(x/10);
putchar((x%10)+'0');
return;
}
inline void writeln(ll x)
{
write(x);
putchar('\n');
return;
}
#define W(x) write(x),putchar(' ')
#define Wl(x) writeln(x)
const int N=200005,inf=0x3f3f3f3f;
int n,m,Qzh[N];
struct Record
{
int Shuz,Weiz;
}Ddq[N];
int main()
{
// freopen("sum12.in","r",stdin);
int i,Head=0,Tail=0,ans=-inf;
R(n); R(m);
for(i=1;i<=n;i++)
{
int x=read(); Qzh[i]=Qzh[i-1]+x;
while(Head<Tail&&Ddq[Head].Weiz<i-m) Head++;
ans=max(ans,Qzh[i]-Ddq[Head].Shuz);
while(Head<=Tail&&Qzh[i]<=Ddq[Tail].Shuz) Tail--;
Ddq[++Tail]=(Record){Qzh[i],i};
}
Wl(ans);
return 0;
}
大家可以自行再搜些题目练习