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

算法:单调栈和单调队列

  • 一.单调栈和单调队列
  • 二.单调栈例题
    • 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
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,直到当前元素大于栈顶元素或者栈空为止,很容易就可以证明/看出来这个栈依照这样的操作一定能保持单调。那么这样的单调栈到底有什么作用呢 ?比如下面这道题。
【数据结构】单调栈和单调队列 详解+例题剖析_第1张图片
输入样例:

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

输出样例:

8
4000

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

#include
#include
#include
#include
#include
#include
#include

#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次方约等于趋近为0

ll 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;
}

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

【数据结构】单调栈和单调队列 详解+例题剖析_第2张图片
首先图一是一组数据,画成图的样子。我们维护单调栈的单调性,直到遇见违反单调性的数,我们pop栈顶元素并更新ans。栈顶大于要入栈的元素,就pop,ans=max(ans,s1)。第二个还是大于,同样的操作,ans=max(ans,s2)。注意这里的s2就是第二个高度乘以2,因为第一个栈顶虽然pop了但是对于第二个栈顶来说增加了它的面积,所以用cnt加上这里的宽度,更新答案。最后恢复单调性,最新的栈顶元素的宽度就是被删除的元素的宽度与自己的总和。删完之后虽然栈里的元素少了几个但是画成图确实没有少元素,只是高度变了。如下图图二:
【数据结构】单调栈和单调队列 详解+例题剖析_第3张图片
然后我们继续上面的操作。最后求得最大值,如图3。注意我们要在最后加一个高度为0的数据,为了避免如图4的情况出现。
【数据结构】单调栈和单调队列 详解+例题剖析_第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

#include
#include
#include
#include
#include
#include
#include
#define debug cout<<"ok"<
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。
【数据结构】单调栈和单调队列 详解+例题剖析_第5张图片

输入格式 输入一共有两行,第一行有两个正整数 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
#include
#include
#include
#include
#include
#include
#include
#define debug cout<<"ok"<
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
#include
#include
#include
#include
#include
#include
#include
#define debug cout<<"ok"<
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();//越界就pop
        ans=max(ans,sum[i]-sum[q.front()]);
        while(!q.empty()&&sum[q.back()]>=sum[i])//递减就pop
            q.pop_back();
        q.push_back(i);
    }
    printf("%d\n",ans);
    return 0;
}

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

你可能感兴趣的:(【算法总结】合集,#,单调队列)