数据结构之单调队列与单调栈
目录
一:单调队列
我们还是先放上一道题
题目描述
有一个长为 n 的序列 a,以及一个大小为k 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。
例如:
The array is [1,3,-1,-3,5,3,6,7], and k = 3。
输入格式
输入一共有两行,第一行有两个正整数 n,k。 第二行 n 个整数,表示序列 a
输出格式
输出共两行,第一行为每次窗口滑动的最小值
第二行为每次窗口滑动的最大值
输入输出样例
输入
8 3 1 3 -1 -3 5 3 6 7
输出
-1 -3 -3 -3 3 3 3 3 5 5 6 7
说明/提示
【数据范围】
对于 50% 的数据,1≤n≤105;
对于 100% 的数据,1≤k≤n≤1e6,ai∈[−231,231)。
这道题依然有很多方法
方法一:暴力枚举,显然(真的很显然)会爆
方法二:线段树维护区间最值,是可以,但是实现复杂,且常数较大
方法三:st表,同线段树常数大
方法四:单调队列
不知道单调队列是什么?没事,看完这篇,你肯定会了。
1:单调队列的概念
单调队列,顾名思义,首先是一个队列,其次,它具有一定的单调性
怎么单调呢?
1:其内部的存编号(数组下表)是具有单调性(一般是递增)
2:队列存的值具有单调性(单调递增或单调递减);
2:单调队列的模拟
就上题而言
假如说我们需要维护每一个固定长度区间的最小值
上图理解(图丑请见谅)
首先这个窗口在1~3区间
i=1,此时内队列为空,所以1无条件入队
i=2,此时队列有个1,3比1大,所以也入队
i=3,此时队尾是3,-1来了,一看,哎呦,我比你小,你凭什么站到我前面
于是-1把3踢出了队列,此时队列里是1,-1
然后-1又发现,哎呦,前面怎么还有一个比我大的家伙,于是它又把1踢出队列
所以此时队首元素是-1,所以区间最小值是-1
然后区间往后移,2~4
i=4,-3来了,他发现-1比他大,就把-1踢出去,自己站在了第一
区间最小值为队首元素-3
然后区间继续往右移
i=5,5比-3大,所以它默默的排在了后面
区间最小值仍是-3
区间继续右移
i=6,3比5小于是代替了5
此时区间最小值还是-3
然后区间继续往右
i=7,6来了,他发现,哎,你-3都已经不在这个区间里了,凭什么还在这
于是-3被撵了出去,现在队首元素是3
所以区间最小值是-3
区间继续右移
i=8,7跑过来了,他看前面的都比他小,于是就站在了队尾
此时队首是3,所以最小值是3
所以输出序列为:-1 -3 -3 -3 3 3
这就是单调队列的模拟过程,它是O(n)的优秀复杂度
其中心思想就一个:
你在我前面,还没有我小,要你何用?
然后每一步的队首元素就是最小值
如果队首越界,就弹出队首。
我相信,你应该能听懂了
不用着急实现,一步一步,慢慢来。
3:单调队列的实现
你应该已经发现了,这个队列需要支持队首弹出,队尾插入和队尾弹出???
怎么实现呢?
STL大法好
在STL的容器里,有一个队列容器叫做deque(双向队列)
它可以支持队首队尾都能插入,弹出
基本操作如下
q.push_front() 队首插入
q.push_back() 队尾插入
q.pop_front() 队首弹出
q.pop_back() 队尾弹出
q.front() 返回队首元素
q.back() 返回队尾元素
q.size()和q.empty() 不多说了
所以我们的代码就诞生了!
#include<bits/stdc++.h>
using namespace std;
struct node
{
int id; //存的编号,判断越界
int val;
};
deque<node> q1; //最小值队列
deque<node> q2; //最大值队列
int x,k,n;
int a1[1000020],a2[1000020],top1,top2;
int main()
{
cin>>n>>k;
for(int i=1;i<=n;i++)
{
scanf("%d",&x);
while(!q1.empty()&&x<=q1.back().val)
q1.pop_back(); //弹出队尾元素
while(!q2.empty()&&x>=q2.back().val)
q2.pop_back();
q1.push_back(node{i,x}); //压入队列
q2.push_back(node{i,x});
if(i-k+1>q1.front().id)
q1.pop_front(); //如果队首越界,弹出
if(i-k+1>q2.front().id)
q2.pop_front();
if(i>=k)
{
a1[++top1]=q1.front().val; //答案数组
a2[++top2]=q2.front().val;
}
}
for(int i=1;i<top1;i++)
cout<<a1[i]<<' ';
cout<<a1[top1];
cout<<endl;
for(int i=1;i<top2;i++)
cout<<a2[i]<<' ';
cout<<a2[top2];
return 0;
}
怎么样懂了吗
上题
4.单调队列的应用
holiday
题目描述
经过几个月辛勤的工作,FJ决定让奶牛放假。
假期可以在1…N天内任意选择一段(需要连续),每一天都有一个享受指数W。但是奶牛的要求非常苛刻,假期不能短于P天,否则奶牛不能得到足够的休息;假期也不能超过Q天,否则奶牛会玩的腻烦。
FJ想知道奶牛们能获得的最大享受指数。
输入格式
第一行:N,P,Q.
第二行:N个数字,中间用一个空格隔开。
输出格式
一个整数,奶牛们能获得的最大享受指数。
样例数据
input
5 2 4
-9 -4 -3 8 -6
output
5
Hint 选择第3-4天,享受指数为-3+8=5。
数据规模与约定
50% 1≤N≤10000
100% 1≤N≤100000
时间限制:1s
空间限制:256MB
用前缀和处理前i天的指数和
其实就是从P的位置开始枚举,每次把i-P压入队列,如果i-Q大于队首元素的位置,弹出队首
每次取出队首让ans=max(sum[i]-sum[Day.front()])
最后输出ans
#include<bits/stdc++.h>
using namespace std;
#define INF 9999999999
long long sum[100300];
int n,p,q;
long long ans=-INF;
deque<long long> Day;
int main()
{
freopen("holiday.in","r",stdin);
freopen("holiday.out","w",stdout);
scanf("%d%d%d",&n,&p,&q);
long long x;
for(int i=1;i<=n;i++)
{
scanf("%lld",&x);
sum[i]=sum[i-1]+x;
}
for(int i=p;i<=n;i++)
{
while(!Day.empty()&&sum[Day.back()]>sum[i-p])
Day.pop_back();
Day.push_back(i-p);
while(!Day.empty()&&i-q>Day.front())
Day.pop_front();
ans=max(ans,sum[i]-sum[Day.front()]);
}
cout<<ans;
return 0;
}
5.补充与说明
单调队列对于处理线性滑动区间最值可谓游刃有余
故此,有一部分动态规划题可用单调队列进行优化
这些我们以后再讨论
单调队列例题:
二:单调栈
有了单调队列的基础,你应该能体会到具有单调性的数据结构是什么样的了
1.单调栈的基本概念与实现
单调栈,和单调栈一样,是具有单调性的栈
但有一点,单调递增(或递减)是指栈内元素的出栈序列递增(或递减),而栈内元素是递减(或递增)的
并且,没有双向栈(。。。。。。)
我们同样也找一道例题
洛谷P2866 [USACO06NOV]Bad Hair Day S
题目描述
Some of Farmer John's N cows (1 ≤ N ≤ 80,000) are having a bad hair day! Since each cow is self-conscious about her messy hairstyle, FJ wants to count the number of other cows that can see the top of other cows' heads.
Each cow i has a specified height hi (1 ≤ hi ≤ 1,000,000,000) and is standing in a line of cows all facing east (to the right in our diagrams). Therefore, cow i can see the tops of the heads of cows in front of her (namely cows i+1, i+2, and so on), for as long as these cows are strictly shorter than cow i.
Consider this example:
=
= =
= - = Cows facing right -->
= = =
= - = = =
= = = = = =
1 2 3 4 5 6 Cow#1 can see the hairstyle of cows #2, 3, 4
Cow#2 can see no cow's hairstyle
Cow#3 can see the hairstyle of cow #4
Cow#4 can see no cow's hairstyle
Cow#5 can see the hairstyle of cow 6
Cow#6 can see no cows at all!
Let ci denote the number of cows whose hairstyle is visible from cow i; please compute the sum of c1 through cN.For this example, the desired is answer 3 + 0 + 1 + 0 + 1 + 0 = 5.
输入格式
Line 1: The number of cows, N.
Lines 2..N+1: Line i+1 contains a single integer that is the height of cow i.
输出格式
Line 1: A single integer that is the sum of c1 through cN.
题意翻译
农夫约翰有N (N≤80000)头奶牛正在过乱头发节。每一头牛都站在同一排面朝东方,而且每一头牛的身高为hi。第N头牛在最前面,而第1头牛在最后面。 对于第i头牛前面的第j头牛,如果hi>hi+1并且hi>hi+2 ⋯hi>hj,那么认为第i头牛可以看到第i+1到第j头牛
定义Ci为第ii头牛所能看到的别的牛的头发的数量。请帮助农夫约翰求出所有Ci的和
输入输出样例
输入 #1复制
6 10 3 7 4 12 2
输出 #1复制
5
废话不多说,直接看正解
我们可以维护一个单调递增的栈(栈内单调递减)
每来一个新的奶牛,如果它比栈顶小,直接入栈,因为他对前面的奶牛造不成视觉障碍(他是最低的嘛)
如果他比栈顶大于等于,就一直弹出栈顶,直到他比栈顶小,并每次用这个新来的奶牛的位置减去栈顶的位置然后+1,并累加。
注意
可能遍历完后栈内依然有一个单调递减的序列,我们以此将他弹出,每次累加(n-栈顶元素的位置+1);
话不多说,上代码
#include<bits/stdc++.h>
using namespace std;
struct node
{
int id;
int val;
};
int n,x;
long long ans=0;
stack<node> st;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
scanf("%d",&x);
while(!st.empty()&&x>=st.top().val)
{
ans+=1ll*(i-st.top().id-1);
st.pop();
}
st.push(node{i,x});
}
while(!st.empty())
{
int t=st.top().id;
st.pop();
ans+=1ll*(n-t);
}
cout<<ans;
return 0;
}