单调队列是一种队首,队尾都可以执行出队操作的单调递增、递减队列,是一种数据结构。主要用来维护一个区间内最大值、最小值的问题,因为有优化复杂度的作用而被使用。
因为单调队列的题目大多都是队首、队尾出队,队尾入队,因此代码较单调栈相比没有那么灵活多变。
(维护最小值)模板,最大值同理:
for(i=1;i<=n;i++)//最小值
{
while(r>=l&&q[r]>=a[i])//队尾出队,如果入队元素比队中元素小,那么之前的元素肯定不会再次作为最小值了
r--;
q[++r]=a[i];//队尾入队
p[r]=i;//记录元素位置
while(p[l]<=i-k)//队首出队,如果队列中的元素处于范围外,那么执行出队
l++;
}
那么来几道例题:
洛谷P1886 滑动窗口
给出数组大小n和窗口长度k,输出每个窗口的最小值和最大值。
单调队列的板子题,写出维护最小值和最大值的单调队列即可
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
int p[1000001],q[1000001],a[1000001];
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n,k,i,j,l,r;
cin>>n>>k;
for(i=1;i<=n;i++)
{
cin>>a[i];
}
l=1;r=0;//队头,队尾
for(i=1;i<=n;i++)//最小值
{
while(r>=l&&q[r]>=a[i])//队尾出队,如果入队元素比队中元素小,那么之前的元素肯定不会再次作为最小值了。
r--;
q[++r]=a[i];//更新队尾元素
p[r]=i;
while(p[l]<=i-k)//队首出队,如果队列中的元素处于窗口外,那么执行出队
l++;
if(i>=k) cout<<q[l]<<" ";//输出此时单调队列的头元素(因为单调,所以最前面的是最小的)
}
cout<<endl;
l=1;r=0;
for(i=1;i<=n;i++)//最大值同理
{
while(r>=l&&a[i]>=q[r])
r--;
q[++r]=a[i];
p[r]=i;
while(p[l]<=i-k)
l++;
if(i>=k) cout<<q[l]<<" ";
}
cout<<endl;
}
洛谷P2032 扫描
有一个 1×n 的矩阵,有 n 个整数。现在给你一个可以盖住连续 k 个数的木板。一开始木板盖住了矩阵的第 1∼k 个数,每次将木板向右移动一个单位,直到右端与第 n 个数重合。每次移动前输出被覆盖住的数字中最大的数是多少。
滑动窗口的青春版,只要写出最大值的那一半就好了。
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
int a[2000001],p[2000001],q[2000001];
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int k,n,i,j;
cin>>n>>k;
for(i=1;i<=n;i++)
{
cin>>a[i];
}
int l=1,r=0;
for(i=1;i<=n;i++)
{
while(r>=l&&a[i]>=q[r]) r--;
q[++r]=a[i];
p[r]=i;
while(i-k>=p[l]) l++;
if(i>=k)
{
cout<<q[l]<<endl;
}
}
}
洛谷P1714 切蛋糕
简而言之,给出数组的大小n和序列最长长度m,要求找到不超过m长度的一段序列的和最大。
连续序列和,很显然要用到前缀和的知识。数据为500000,不能暴力求解,需要用到单调队列进行优化。前缀和的公式为sum[i]-sum[j-1],sum[i]无法改变,那么我们就是要用单调队列去优化sum[j-1]的最小值使得公式值最大。
#include <bits/stdc++.h>
#define endl '\n'
#define INF 2147483647
using namespace std;
typedef long long ll;
const int N=500001;
int a[N],sum[N],p[N];
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int k,n,i,j;
cin>>n>>k;
for(i=1;i<=n;i++)
{
cin>>a[i];
sum[i]=sum[i-1]+a[i];//前缀和
}
int l=1,r=1;//注意优化sum[j-1]时它的值可能为0,即什么也不取,因此r要初始化为1
int ans=-INF;
p[1]=0;
for(i=1;i<=n;i++)
{
while(r>=l&&sum[i]<=sum[p[r]]) r--;//队尾出队优化最小值
p[++r]=i;
while(r>=l&&p[l]<i-k) l++;//队首出队剃掉超过范围的最小值
ans=max(ans,sum[i]-sum[p[l]]);//递推出答案
}
cout<<ans<<endl;
}
洛谷P2629 好消息,坏消息
切蛋糕的升级版,给出消息数组的大小n。每个元素有一个好坏度,会影响老板的心情。可以选择一个k值,先报告k-n的消息,再报告1-k的消息,期间老板的心情如果下降到0以下,则主人公会被炒鱿鱼。计算有几种方案主人公不会被炒鱿鱼。
k可以取1-n,这样报告消息呈一个环形,所以我们先断环成链让它处在一条线上。然后分析,在每一种报告方案中,我们不能让老板的心情下降到0以下,实际上就是如果报告中最小的前缀和sum[i]-不变的sum[j-1]如果都不小于0,那么这种方案在过程中也不会小于0。那么就变成了我们要去优化sum[i]的最小值就可以了。
#include <bits/stdc++.h>
#define endl '\n'
#define INF 2147483647
using namespace std;
typedef long long ll;
const int N=2000002;
int a[N],sum[N],p[N];
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n,i,j;
cin>>n;
for(i=1;i<=n;i++)
{
cin>>a[i];
}
for(i=n+1;i<=2*n-1;i++)
{
a[i]=a[i-n];//断环成链
}
for(i=1;i<=2*n-1;i++)
{
sum[i]=sum[i-1]+a[i];//前缀和
}
int l=1,r=1;
int ans=0;//方案数
for(i=1;i<=2*n-1;i++)//优化sum[i]的最小值
{
while(r>=l&&sum[i]<=sum[p[r]]) r--;
p[++r]=i;
while(r>=l&&i-n>p[l]) l++;
if(i>=n)
{
if(sum[p[l]]-sum[i-n]>=0) ans++;
}//如果方案中最小的心情数都不小于0,那么整个报告过程中老板的心情也不会小于0,方案数+1。
}
cout<<ans<<endl;
}