题目
- 这里以洛谷的 P1886 滑动窗口 /【模板】单调队列 为例
暴力做法
读完题目后我们立马就能想到暴力的方法,每次循环 [i , i+k-1] (i>=1)这个区间,找到区间内的最小值和最大值。而且代码非常容易写出。
- 暴力代码:
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int maxn = 1e6+9;
int n,k;
int seq[maxn];
int mins[maxn],maxs[maxn],cnt;
signed main(){
scanf("%lld %lld",&n,&k);
for(int i=1; i<=n; ++i)
scanf("%lld",&seq[i]);
for(int i=1; i<=n-k+1; ++i){
int mx(-2147483647),mi(2147483647);
for(int j=i; j<=i+k-1; ++j){
mx = max(mx,seq[j]);//找到当前窗口内的最大值
mi = min(mi,seq[j]);//找到当前窗口内的最小值
}
mins[cnt] = mi;//将最小值添加到结果数组中
maxs[cnt] = mx;//将最大值添加到结果数组中
++cnt;//位置记录+1
}
//输出结果
for(int i=0;i<cnt;++i)
printf("%lld ",mins[i]);
putchar('\n');
for(int i=0;i<cnt;++i)
printf("%lld ",maxs[i]);
return 0;
}
不难看出上面的暴力代码的时间复杂度为 O(nk) ,而看本题的数据范围,nk 最大为 1 0 12 10^{12} 1012 , 这显然是不可接受的。
意料之中的超时,那么有没有其他方法可以更快的解决方法呢(废话)
单调队列
- 单调队列性质
- 单调队列里面的元素是单调递增/单调递减/或者其他规则的。
- 单调队列既可以从队首出队,也可以从队尾出队。
- 单调队列中不存储元素的值,而是存储元素在数组中的下标。
我们以单调递减为例,以数组模拟队列,定义一个数组 Queue ,一个队首指针 head,一个队尾指针 tail
int Queue[maxn],head(0),tail(0);//队列相关
关于单调队列,有如下操作:
- 队头出队:当队头的元素从窗口中滑出时,队头元素出队,即 head++
- 队尾入队:当新元素滑入窗口时,要把新元素从队尾插入,分为两种情况
- 直接插入:如果新元素小于队尾元素,那就直接从队尾插入,即 tail++ ,因为它可能在前面的元素滑出窗口后成为新的最大值。
- 先删后插:如果新元素大于等于队尾元素,那就先删除队尾元素(因为队尾元素在整个窗口中不可能成为新的最大值了),删除方法为 tail-- , 即从队尾出队。循环删除,直到队空或者遇到第一个大于新元素的值,插入其后,即 ++tail 。
这样操作每次都能从队首取到当前窗口内的最大值。
下面以图示说明一下整个过程
- 首先一个序列如上图。n=8,k=3
- 我们需要将 i 从 1 枚举到 n ,当 i = 1 时,显然 1 需要入队,因为此时队列为空
- 当 i = 2 时,新元素为3,我们发现3大于队尾元素1,所以先将1出队,此时队列已经为空,然后将3入队
- 当 i = 3 时,新元素为-1,比队尾元素小,我们直接将它插入队尾。此时窗口已经包住了3个值,所以开始输出,输出队首元素
3
- 当 i = 4 时,新元素为-3,比队尾元素小,直接插入到队尾。输出队首元素
3
- 当 i = 5 时,此时3已经滑出窗口了!所以我们要将队首3出队。然后看新元素为5,比队尾-3大,让-3出队,新队尾为-1,而5比队尾-1大,-1出队,此时队列为空,新元素5入队。输出队首元素
5
- 当 i = 6 时,新元素3比队尾元素小,直接将3入队。输出队首元素
5
- 第一个箭头画反了。。5,3应该是从队尾出队的!(
马虎了) - 当 i = 7 时,新元素为6,比队尾3大,队尾3出队,新队尾为5,而6比队尾5大,5出队,此时队列为空,新元素6入队。 输出队首元素
6
- 第一个箭头画反了。。6应该是从队尾出队的!(
意外) - 当 i = 8 时,新元素7比队尾元素6大,6出队,当前队列为空,新元素7入队。输出队首元素
7
上述步骤详细的说明了整个单调队列执行过程,最终输出了 3 3 5 5 6 7
但是,上述步骤虽然的确解决了问题,我们怎么实现代码?
我们发现无法通过这种方式来判断当前的队首元素是不是已经滑出窗口了!!!
这也就是性质3的来源,单调队列中存储的是元素在数组中的下标 !
- 当窗口往下滑一个时,我们发现 i = 5,k不变,队首3对应的下标为2,不变。所以我们可以用 i-k+1 来判断当前队首是否已经滑出窗口,如果
Queue[head] < i-k+1
则说明队首已经滑出,应该删除队首。
例题代码
我们已经分析了最大值的单调队列,最小值与其几乎没有什么差别,只是让队首元素保持最小而已。
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int maxn = 1e6+9;
int n,k;
int seq[maxn];
int Queue[maxn],head(0),tail(0);//队列相关
signed main(){
scanf("%lld %lld",&n,&k);
for(int i=1; i<=n; ++i)
scanf("%lld",&seq[i]);
for(int i=1;i<=n;++i){
//如果队首元素下标已经滑出窗口,将队首删除
//head<=tail 保证了队列只能有值或为空
if(head<=tail && Queue[head]<i-k+1) head++;
//循环删除队尾元素比新元素大的值
while(head<=tail && seq[i]<=seq[Queue[tail]]) tail--;
//新元素入队
Queue[++tail] = i;
//如果窗口已经包住了k个值,则输出队首,即当前窗口内最小值
if(i>k-1) printf("%lld ",seq[Queue[head]]);
}
putchar('\n');
head=0; //清空队列
tail=0;
for(int i=1;i<=n;++i){
if(head<=tail && Queue[head]<i-k+1) head++;
//同递增的单调队列,递减只是改变了一个符号
while(head<=tail && seq[i]>=seq[Queue[tail]]) tail--;
Queue[++tail] = i;
if(i>k-1) printf("%lld ",seq[Queue[head]]);
}
return 0;
}
- 绿了!
额外经验
洛谷 P2032 扫描
洛谷 P1440 求m区间内的最小值