单调队列和单调栈

版权声明:不得未经允许转载,否则后果自负 https://blog.csdn.net/qq_41814502/article/details/84966759

目录

单调栈

什么是单调栈

单调栈的应用

排队递减单调栈

最大长方形递增单调栈

单调队列

什么是单调队列

单调队列的应用

单调队列的基本模板

单调队列的重要应用DP


单调栈

什么是单调栈

什么叫做单调栈?

什么是单调?

单调也就是序列中的元素是递增或递减的,也就是从大到小或者是从小到大的,不会有一个拐点。

单调栈自然就是栈中的元素维护着单调性。

单调栈的应用

排队递减单调栈

现在有一些小朋友排队,一个小朋友可以看到右边比他矮且没有比自己高的小朋友遮挡的小朋友(有一点绕),求每个小朋友能看到的小朋友之和。

样例

输出

3

我们看到,1号小朋友(1)右边比他矮的都没有,所以看到了0个

2号小朋友(5)右边比他矮的有3号小朋友(2)和5号小朋友(4)

但是5号小朋友被4号小朋友遮住了(9)(因为4号小朋友比2号小朋友高),所以2号小朋友看不到5号小朋友,只能看到1个小朋友(即3号小朋友(2))

依次我们可以得出

3号小朋友看到0个

4号小朋友看到2个

5号小朋友和6号小朋友都看到0个,所以一共看到了1+2=3个

解决1.暴力枚举

我们每次从i开始,直到循环到j比i大或者相等,那么i号小朋友可以看到j-i-1个小朋友,时间复杂度为O(n^2)

如果我们的N是N<=100000,那这个算法就不行了!

解决2.单调栈

我们发现,我们用了一个循环来找到i后面第一个大于等于他的元素,如果我们能一下子就知道,不就少了一个循环了吗

就可以把时间降成O(n)了。

我们想一想,我们的栈如果是递减的,那么当一个数进入栈时,一定要把比他大的弹出去,这个时候就可以知道位置了!

1 5 2 9 4 5

第一步

将1放入栈中

第二步

将5放入栈中,由于我们维护的单调递减栈,如果放入5后,1 5就没有维护递减了,所以为了维护递减,我们把1弹出去,也就可以顺便知道比1号同学大于等于的第一个同学就是2号同学了,ans+=(2-1-1) ans=0

第三步

将2放入栈中,发现可以维护递减,直接放入,栈中的元素就是5 2了

第四步

将9放入栈中,发现不能维护递减栈,所以依次弹出比9小的数来维护递减栈,最后我们发现5和2都被弹出去了,也就说明了2号和3号同学右边第一个比自己大于等于的同学是4号同学 ans+=(4-2-1)+(4-3-1) ans=1

第五步

将4放入栈中,发现可以维持递减栈,直接放入,序列为9 4

第六步

将5放入栈中,发现不能维持递减栈了,将比5小的数都弹出去,就把4弹出去了,也就说明了5号同学右边第一个比自己大于等于的同学是6号同学,ans+=(6-5-1) ans=1,这时序列就是9 5了

最后一步

由于后面没有同学了,但是栈中却还有元素,所以我们要结尾,人工的在后面加一个极大值,并且假设是第n+1个同学

ans+=(7-4-1)+(7-6-1)  ans=3了(9代表的是4号同学的身高嘛,这里减去的自然是编号啦)

恩,我相信你们懂了!!!

​
#include <cstdio>
#include <stack>
using namespace std;

struct node{
    int x, id;
};
int n, x, ans;
stack<node> q;

int main(){
    scanf("%d",&n);
    for(int i = 1; i <= n; i++){
        scanf("%d",&x);
        node h;
        h.x = x;
        h.id = i;
        while(!q.empty()){
            node t = q.top();
            if(t.x < x){
                ans += (i - t.id - 1);
                q.pop();
            }
            else {
                break;
            }
        }
        q.push(h);
    }
    while(!q.empty()){
        node t = q.top();
        q.pop();
        ans += n - t.id;
    }
    printf("%d\n",ans);
    return 0;
}

​


最大长方形递增单调栈

有一块草地,宽都为1,长为a[i],先在要找到一个长方形,使得他的面积最大,这个最大的面积是多少呢?

样例

6

1 5 6 5 4 2

输出

16

这个图就很明显了,对不对,你看啊,最大的肯定不是这个15对不对

因为还有更大的

这个就是16了嘛,他肯定最大啦!

怎么做,我们想,我们如果以第a[i]为长度,那么宽应该为多少呢

是不是第i个草左边能延伸的最大长度加上右边延伸的最大长度

就比如说,我们以第2块草地来举例,他最大只能延伸到2了,因为第1个草地比他小,所以只能延伸到2

而他的右边可以延伸到第5块草地,因为之间的草地都比他自己大,所以肯定能延伸过去,恰好第6块草地比他小了,所以就不能延伸了

所以第2块草地可以延伸的宽度就为2-5的长度,即为4

其实也就是说,以第i块草地为长的长方形,他的宽,就是找左右两边第一个比自己小的,从而得到了延伸的最大范围,就可以知道宽度了

是不是和上面的排队很像,排队是求右边最小的,这道题不过是还要求一个左边最小的,再用一个单调栈不就行了

恩,第一种方法就是用两个单调栈

而我们可以只用一个的,递增栈,递增怎么做啊!

我们维护递增时,是不是左边的元素肯定比你小,而恰好也就是左边第一个比你小的,而当你要被踢出去的时候,是不是也一定是后面比你小的元素把你踢出去的,这样我们不就找到了宽度了吗?

#include <cstdio>
#include<stack>
#include<iostream>
#define reg register
using namespace std;
 
struct node{
    int id,chang;
};
int n, x, ans;
stack<node> q;
 
inline void read(int &x){
    int f = 1;
    x = 0;
    char s = getchar();
    while(s < '0' || s > '9'){
        if(s == '-'){
            f = -1;
        }
        s = getchar();
    }
    while(s >= '0' && s <= '9'){
        x = (x << 3) + (x << 1) + (s - '0');
        s = getchar();
    }
    x *= f;
}
 
inline void wrtie(int x){
    if(x < 0){
        putchar('-');
        x *= -1;
    }
    if(x > 9){
        wrtie(x / 10);
    }
    putchar((x % 10) + '0');
}
 
int main(){
    node h;
    read(n);
    for(reg int i = 1; i <= n; i++){
        read(h.chang);
        h.id = i;
        while(!q.empty()){
            node t = q.top();
            if(t.chang >= h.chang){
                ans = max(ans, (i - t.id) * t.chang);
                h.id = t.id;
                q.pop();
            }
            else {
                break;
            }
        }
        q.push(h);
    }
    while(!q.empty()){
        ans = max(ans, (n + 1 - q.top().id) * q.top().chang);
        q.pop();
    }
    wrtie(ans);
    return 0;
}

上面我也顺便把读入优化和输出优化打上了,大家参考参考吧!

单调队列

什么是单调队列

这不是废话嘛!当然是维护递减或递增的队列啦!

但他和栈不同的是,我们的队列可以从队列的头端出去啊,如果你两边都可以进出,就是一个双向队列了,用途当然比单调栈要更多的

单调队列的应用

单调队列的基本模板

滑动窗口

什么意思

 

这就是大概的题目意思了

我们上面的单调栈只是一个元素后面比自己大或者小的元素,然而这里有一个限制,k

怎么搞!

我们维护的思想是和单调栈一样的(不知道先看上面的)

而如何来维护连续k个?

如果队列的头端的下标和当前要放入的元素下标之间距离大于了K,那么队头就不能要了,所以就要把队头弹出去

这里与单调栈不同的地方也就是这里了!

#include <cstdio>
#include <list>
#define reg register
using namespace std;
 
struct node
{
    int id, x;
}a[1000005];
int n, k, ans[1000005];
bool flag;
list<node> sheng;
list<node> jiang;
 
inline void read(int &x){
    int f = 1;
    x = 0;
    char s = getchar();
    while(s < '0' || s > '9'){
        if(s == '-'){
            f = -1;
        }
        s = getchar();
    }
    while(s >= '0' && s <= '9'){
        x = (x << 3) + (x << 1) + (s - '0');
        s = getchar();
    }
    x *= f;
}
 
inline void write(int x){
    if(x < 0){
        putchar('-');
        x *= -1;
    }
    if(x > 9){
        write(x / 10);
    }
    putchar((x % 10) + '0');
}
 
int main(){
    read(n);
    read(k);
    for(reg int i = 1; i <= n; i++){
        read(a[i].x);
        a[i].id = i;
        while(!sheng.empty()){
            node t = sheng.front();
            if(i - t.id + 1 > k){
                sheng.pop_front();
            }
            else {
                break;
            }
        }
        while(!sheng.empty()){
            node t = sheng.back();
            if(t.x > a[i].x){
                sheng.pop_back();
            }
            else {
                break;
            }
        }
        sheng.push_back(a[i]);
        while(!jiang.empty()){
            node t = jiang.front();
            if(i - t.id + 1 > k){
                jiang.pop_front();
            }
            else {
                break;
            }
        }
        while(!jiang.empty()){
            node t = jiang.back();
            if(t.x < a[i].x){
                jiang.pop_back();
            }
            else {
                break;
            }
        }
        jiang.push_back(a[i]);
        if(i >= k){
            if(!flag){
                write(sheng.front().x);
                flag = 1;
            }
            else {
                putchar(' ');
                write(sheng.front().x);
            }
            ans[i] = jiang.front().x;
        }
    }
    putchar('\n');
    flag = 0;
    for(reg int i = k; i <= n; i++){
        if(!flag){
            write(ans[i]);
            flag = 1;
        }
        else {
            putchar(' ');
            write(ans[i]);
        }
    }
    return 0;
}

这个也是供大家参考参考的!

单调队列的重要应用DP

如果我们的单调队列就是来做这种模板题,也就没有存在的意义了

那么这个单调队列主要是来优化DP的!

例如我们要求连续不超过K的长度的最大子段和

恩,你们都知道,可以用前缀和来做对吧?

b[r]-b[l-1]就是l到r子序列的和

b是前缀数组啦,前缀和就是1-i的和,比如b[3]=a[1]+a[2]+a[3]

那么我们输入的时候就可以预处理了 b[i]=b[i-1]+a[i]

那么我们要使得长度不超过k,就说明只能减去b[r-k]到b[r-1]的序列了,对不对,因为这样长度才能是小与等于k的

我们又要使得子序列最大,也就是在b[r-k]到b[r-1]中找到一个最小的嘛

我们直接用单调队列来优化一下,直接在o(1)的时间复杂度找到最小值,避免了用循环来浪费的时间

由于这个还是要大家去自己思考思考的,我就不上代码了!

比如我们再来一个例子吧

dp[i][j][k]=max(dp[i][j][k],dp[i][j][j-k]+a[k]);

我们就假设有一道题的状态转移方程式上面这样的,我们要最大,干嘛不直接知道dp[i][j][j-k]+a[k]中最大的呢,直接和dp[i][j][k]来比较啊,还要用循环去找最大值,太浪费时间了,我们直接用单调队列将dp[i][j][j-k]+a[k]的所有元素维护递减性,那么头元素就一定是最大的了,不用循环,直接找到,使得o(n^3)的时间复杂度变为了o(n^2)次方

哇!如此厉害,就是这样的,有可能大家还不是很理解,只要大家多做几道题,肯定会明白其中的奥秘的!

猜你喜欢

转载自blog.csdn.net/qq_41814502/article/details/84966759