单调栈&单调队列概述

这俩货好像有那么点关系,就把它们一起讲了。

首先讲一讲单调栈,这个玩意儿似乎没有什么板子,但是这一类题目看懂了其实都比较板。还是日常先来看一道题:音乐会的等待

题目描述

N个人正在排队进入一个音乐会。人们等得很无聊,于是他们开始转来转去,想在队伍里寻找自己的熟人。队列中任意两个人 A A B,如果他们是相邻或他们之间没有人比 A A B高,那么他们是可以互相看得见的。

写一个程序计算出有多少对人可以互相看见。

输入输出格式

输入格式:

输入的第一行包含一个整数 N(1N500000) N ( 1 ≤ N ≤ 500 000 ) , 表示队伍中共有N个人。

接下来的N行中,每行包含一个整数,表示人的高度,以毫微米(等于 109 10 − 9 次方米)为单位,每个人的调度都小于 231 2 31 毫微米。这些高度分别表示队伍中人的身高。

输出格式:

输出仅有一行,包含一个数 S S ,表示队伍中共有S对人可以互相看见。

输入输出样例

输入样例#1
7 
2 
4 
1 
2 
2 
5 
1
输出样例#1
10






不得不说,这道题有一点坑,难点不在写单调队列上,而在特判情况上。

不过先说一下单调栈是什么。

我们要维护一个单调上升的栈,比如数据为:1,4,6,3,5。把这些元素一次加入栈内,并保持栈内单调,做法就是把不单调的元素弹出。模拟一下:

1

1,4

1,4,6

1,3

1,3,5

很明显,在3入栈时,我们把栈顶大于3的全部弹出,以此保证栈内单调。

单调栈有什么用?

如果我们维护的是一个单增序列,意思就是若 i<j i < j ai>aj a i > a j ,那么 ai a i 对后面的答案没有影响(贡献)。

最简单的一个应用就是:求每一条红线向左在不碰到黑线的情况下向左延伸的最远距离。
样例

可以发现,在蓝色箭头的右侧,绿色圈起来的黑线就挡不到红线延伸了(这两根线其实已经没有了任何意义)。而这个结论在单调栈中就是用蓝色箭头把这两根线弹出。也就是用小的高度把大的高度弹出。而且在弹完之后,剩下的第一条线(也就是栈顶)刚好是蓝色能到达的最远距离。

我们再回过头来看音乐会的等待这一道题。有了单调栈的思想,如果两个人中间有一个比其中一个人高,那么两个人不能互相看见。那我们顺势可以推出:若 i<j i < j ai<aj a i < a j 那么对于任意的 x>j x > j x x i都不能相互看到。这就变成了单调栈,我们维护一个单减序列。而且我们发现,如果一个人把另一个人从栈中弹出,那么它们两个一定能互相看见。如果弹完之后栈中还有元素,这个人还能看到栈顶的人。(想想为什么)

这道题需要特别注意的就是相等的情况,如果弹完后栈顶元素和当前元素相等,当前元素不仅仅能看见栈顶。它能看到栈中所有与它相等的元素再加第一个比它大的元素。

然后附上代码:

#include<iostream>  
#include<cstdio>  
#include<cstring>  
#include<stack>  
using namespace std;  
struct lxy{  
    long long num,x;  
};  
int n;  
long long a[500005];  
long long ret;  
stack <lxy> d;  

int main()  
{  
    scanf("%d",&n);  
    for(int i=1;i<=n;i++)  
      scanf("%lld",&a[i]);  
    for(int i=1;i<=n;i++)  
    {  
        while(!d.empty()&&d.top().x<a[i])//弹栈  
        {  
          ret+=d.top().num;  
          d.pop();  
        }  
        if(!d.empty()&&d.top().x==a[i])//如果相等特判  
        {  
          lxy p=d.top();  
          ret+=p.num;  
          p.num++;  
          d.pop();  
          if(!d.empty()) ret++;  
          d.push(p);  
          continue;  
        }  
        else if(!d.empty()) ret++;//栈中不空,还能多看一个  
        lxy p;p.x=a[i],p.num=1;  
        d.push(p);  
    }  
    printf("%lld",ret);  
}  

好,我们单调栈就此完结,下面看一个单调队列的板子: m m 区间内的最小值

题目描述

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

输入格式:

第一行两个数 nm n , m
第二行, n n 个正整数,为所给定的数列。

输出格式:

n行,第 i i 行的一个数ai,为所求序列中第 i i 个数前m个数的最小值。

输入样例#1
6 2
7 8 1 4 3 2
输出样例#1
0
7
7
1
1
3 







个人认为单调队列是单调栈的升级版。思考一个问题:若 i<j i < j ai>aj a i > a j ,那么区间右端点大于 j j 的区间最小值都不会是ai。是不是很像单调栈?但是题目有一个限制,就是区间长度不超过 m m ,所以我们栈底也要弹出。可以想到使用双端队列来实现。

实现其实也很简单了,入队尾,把队尾大于当前元素的弹出。若队首与队尾元素距离差超过m,弹出队首。(这里封装结构体,存位置和值来实现)。队首就是当前区间的最小值。

再附上代码:

#include<iostream>  
#include<cstdio>  
#include<cstring>  
#include<queue>  
using namespace std;  
struct lxy{  
    int x,id;  
};  
int n,k,l;  
deque <lxy> d;  
long long a[2000005];  
int main()  
{  
    scanf("%d%d",&n,&k);  
    for(int i=1;i<=n;i++)  
      scanf("%lld",&a[i]);  
    printf("0\n");  
    lxy p;p.x=a[1],p.id=1;  
    d.push_back(p);  
    for(int i=2;i<=n;i++)  
    {  
        printf("%d\n",d.front().x);  
        if(i-d.front().id>=k) d.pop_front();  
        while(!d.empty()&&d.back().x>a[i]) d.pop_back();  
        p.x=a[i],p.id=i;  
        d.push_back(p);  
    }  
}  

单调队列一般用于优化取最值的dp,有兴趣可以看看这一篇:修剪草坪
假装是数据结构优化dp,233。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Python中,单调栈单调队列是两种不同的数据结构。单调栈是一个栈,它的特点是栈内的元素是单调的,可以是递增或递减的。在构建单调栈时,元素的插入和弹出都是在栈的一端进行的。与此类似,单调队列也是一个队列,它的特点是队列内的元素是单调的,可以是递增或递减的。在构建单调队列时,元素的插入是在队列的一端进行的,而弹出则是选择队列头进行的。 单调队列在解决某些问题时,能够提升效率。例如,滑动窗口最大值问题可以通过使用单调队列来解决。单调队列的结构可以通过以下代码来实现: ```python class MQueue: def __init__(self): self.queue = [] def push(self, value): while self.queue and self.queue[-1 < value: self.queue.pop(-1) self.queue.append(value) def pop(self): if self.queue: return self.queue.pop(0) ``` 上述代码定义了一个名为MQueue的类,它包含一个列表作为队列的存储结构。该类有两个方法,push和pop。push方法用于向队列中插入元素,它会删除队列尾部小于插入元素的所有元素,并将插入元素添加到队列尾部。pop方法用于弹出队列的头部元素。 总结来说,单调栈单调队列都是为了解决特定问题而设计的数据结构。单调栈在构建时元素的插入和弹出都是在栈的一端进行的,而单调队列则是在队列的一端进行的。在Python中,可以通过自定义类来实现单调队列的功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值