算法——单调栈和单调队列 算法详解+例题剖析

一.单调栈和单调队列

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

二.单调栈例题

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

三.单调队列例题

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实现的单调队列

#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();//越界就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;
}
发布了40 篇原创文章 · 获赞 51 · 访问量 2519

猜你喜欢

转载自blog.csdn.net/weixin_45697774/article/details/103635212