单调栈及单调队列
单调栈和单调队列的关系
单调栈和单调队列的本质,顾名思义,就是单调:利用单调性来解决一些问题。
由于所有元素只会入栈/队1次,所以其复杂度为O(n)
单调队列是单调栈的升级版
单调栈
单调栈就是一个栈,栈中元素有单调的特性。我们向栈中加入元素时,依照单调性,弹出加入新元素后不符合单调性的元素,从而维护栈的单调。
举个例子 单调栈S:1 2 4 9 想要加入的元素是 3
那么我们先弹出 9 再弹出 4 最后把 3 加在栈顶,一个操作就完成了
这时 S:1 2 3 维护了其单调性
那么利用单调栈可以解决什么问题呢?
P1901 发射站
题目描述
某地有 N 个能量发射站排成一行,每个发射站 i 都有不相同的高度 Hi,并能向两边(当 然两端的只能向一边)同时发射能量值为 Vi 的能量,并且发出的能量只被两边最近的且比 它高的发射站接收。
显然,每个发射站发来的能量有可能被 0 或 1 或 2 个其他发射站所接受,特别是为了安 全,每个发射站接收到的能量总和是我们很关心的问题。由于数据很多,现只需要你帮忙计 算出接收最多能量的发射站接收的能量是多少。
输入输出格式
输入格式:
第 1 行:一个整数 N;
第 2 到 N+1 行:第 i+1 行有两个整数 Hi 和 Vi,表示第 i 个人发射站的高度和发射的能量值。
输出格式:
输出仅一行,表示接收最多能量的发射站接收到的能量值,答案不超过 longint。
分析一下数据范围:我们发现只有O(n)的复杂度才可以过这题
因为能量只能被最近的比他高的雷达站吸收,所以我们构建一个单调栈:对于每一个信号塔(元素),它发射的电波能被最近的比他高的接受,所以加入这个信号塔(元素)入栈时,弹出高度比他小的(因为弹出的矮,起码比新信号塔矮,所以有能量也只会传到新元素,至少不会是它,所以我们可以将其弹出),完成弹出之后,将能量加到第一个比他高的(也就是此时的栈顶),最后将新元素加到栈里,这次操作就完成了
对于这题,要从左到右,从右到左做两遍,具体依照题意
#include<iostream>
#include<cstdio>
#include<algorithm>
#define ll long long
using namespace std;
int RD(){
int out = 0,flag = 1;char c = getchar();
while(c < '0' || c >'9'){if(c == '-')flag = -1;c = getchar();}
while(c >= '0' && c <= '9'){out = out * 10 + c - '0';c = getchar();}
return flag * out;
}
const int maxn = 10010010;
int num;
ll sum[maxn];
struct S{int h;int v;}I[maxn];
struct Que{int index,h;}que[maxn];
void getmax(){//做两遍单调栈累计能量
int tail = 0;
for(int i = 1;i <= num;i++){
while(tail > 0 && I[i].h >= que[tail].h)tail--;
if(tail != 0){
sum[que[tail].index] += I[i].v;
}
que[++tail].h = I[i].h;
que[tail].index = i;
}
}
void getanothermax(){
int tail = 0;
for(int i = num;i >= 1;i--){
while(tail > 0 && I[i].h >= que[tail].h)tail--;
if(tail != 0){
sum[que[tail].index] += I[i].v;
}
que[++tail].h = I[i].h;
que[tail].index = i;
}
}
int main(){
num = RD();
for(int i = 1;i <= num;i++){
I[i].h = RD();
I[i].v = RD();
}
getmax();
getanothermax();
ll ans = -1;
for(int i = 1;i <= num;i++){
ans = max(ans,sum[i]);
}
cout<<ans<<endl;
return 0;
}
单调队列
前面提到过,单调队列是单调栈的升级版。单调队列是有限制的单调栈。
试想向一个队列里加入元素:元素的加入除了大小关系,肯定有先后之分。若题意要求我们按一定规则弹出旧元素,这时我们就得用到单调队列了。
比如说最典型的问题:区间长度确定的最值求解问题
P2032 扫描
题目描述
有一个 1 ∗ n 的矩阵,有 n 个正整数。
现在给你一个可以盖住连续的 k 的数的木板。
一开始木板盖住了矩阵的第 1 ∼ k 个数,每次将木板向右移动一个单位,直到右端与
第 n 个数重合。
每次移动前输出被覆盖住的最大的数是多少。
输入输出格式
输入格式:
从 scan.in 中输入数据
第一行两个数,n,k,表示共有 n 个数,木板可以盖住 k 个数。
第二行 n 个数,表示矩阵中的元素。
输出格式:
输出到 scan.out 中
共 n − k + 1 行,每行一个正整数。
第 i 行表示第 i ∼ i + k − 1 个数中最大值是多少。
直接是区间最值得模板
因为题意需要我们弹出比较旧的元素,所以我们给每个元素除了值之外的另一个属性:序号
队尾加入新元素操作与单调栈相同,升级了的是:在每次新元素插入后,利用元素的序号判断队首元素是否需要弹出
以这题为例:每次加入新的元素(方法同单调栈),然后判断队首是否应该被弹出(若新元素的序号 - 队首的序号 > k 则弹出)
(注意,不同的题目的弹出队首方法依题意不同,具体请审题)
通过分析可以看到:这里的单调队列并不是一个真的队列,他可以双头弹出,是个双端队列(不过这里没用到其全部功能,双端队列可以将元素插到队首),是可以用来玄学优化SPFA的,并且有STL里的deque可以实现,这里就不加赘述了。
#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
int RD(){
int out = 0,flag = 1;char c = getchar();
while(c < '0' || c >'9'){if(c == '-')flag = -1;c = getchar();}
while(c >= '0' && c <= '9'){out = out * 10 + c - '0';c = getchar();}
return flag * out;
}
const int maxn = 2000100;
struct MAX{int index,v;}que[maxn];
int a[maxn];
int num,k;
void getmax(){
int head = 1,tail = 0;
for(int i = 1;i <= k;i++){
while(head <= tail && a[i] >= que[tail].v)tail--;
que[++tail].v = a[i];//赋值
que[tail].index = i;//给与其序号
}
for(int i = k + 1;i <= num;i++){
printf("%d\n",que[head].v);
while(head <= tail && a[i] >= que[tail].v)tail--;//操作同单调栈
que[++tail].v = a[i];
que[tail].index = i;
while(que[head].index <= i - k)head++;//是否弹出队首
}
printf("%d\n",que[head].v);
}
int main(){
num = RD();k = RD();
for(int i = 1;i <= num;i++)a[i] = RD();
getmax();
return 0;
}
类似的题目还有
P1440 求m区间内的最小值
P3088 [USACO13NOV]挤奶牛Crowded Cows
P2251 质量检测
P2947 [USACO09MAR]向右看齐Look Up
带有技巧的单调队列
有些题目单用单调队列是不能解决问题的,看这一题:
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能够得到的最大幸运值。
这是一道好题(认真脸)
刚开始看的时候,觉得一个单调队列就可以解决了,后来死活过不了样例,再仔细看题发现:是最多吃M块蛋糕,考虑到蛋糕有负的幸运值,吃完M块不一定就是幸运值最大的方案。
那么怎么办呢?
思考一下,怎么样才能得到Fmax呢?我们可以预处理一下前缀和:sum[1] ~ sum[n],因为对于某块蛋糕来说,其前缀和是固定的,那么Fmax不就等于sum[i] - (不超过范围,满足M块这一条件的)sum[j]min了吗?
然后就开始明朗的:利用前缀和的思想求解前缀和,再利用单调队列求某一区间内前缀和的最小值,相减即为本次的答案,对于每块蛋糕都有一个不超过M块的最大值,每次更新一下求最大的最大就行了
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int RD(){
int flag = 1,out = 0;char c;c = getchar();
while(c < '0' || c > '9'){if(c == '-')flag = -1;c = getchar();}
while(c >= '0' && c <= '9'){out = out * 10 + c - '0';c = getchar();}
return flag * out;
}
const int maxn = 500100;
struct Que{int index,v;}que[maxn];
int num,a[maxn],k;
int ans = -999999999;
void getmax(){
int head = 1,tail = 0;
for(int i = 1;i <= num;i++){
while(head <= tail && a[i] <= que[tail].v)tail--;
que[++tail].v = a[i];
que[tail].index = i;
while(i - k > que[head].index)head++;
ans = max(ans,a[i] - que[head].v);//队首元素的值即为区间最小值
}
}
int main(){
num = RD();k = RD();
int temp;
for(int i = 1;i <= num;i++){
temp = RD();
a[i] = a[i - 1] + temp;
}
getmax();
cout<<ans<<endl;
return 0;
}