推荐学习视频:C15【模板】单调队列 滑动窗口最值_bilibili
0)概述
- 用于求滑动窗口内的最小值和最大值
1)单调队列
- 队尾进队出队,队头出队(维护子序列的单调性)的数据结构
- 队尾出队的条件:队列不空且新元素更优,队中旧元素队尾出队
- 每个元素必然从队尾进队一次
- 队头出队的条件:队头元素滑出了窗口
- 注意:队列中存储元素的下标,方便判断队头出队
1:维护滑动窗口的最大值/最小值
int h=1,t=0; // 当队尾t<队头h时,说明单调队列中没有元素
for(int i=1;i<=n;i++) {
// 当前队头q[h]不在窗口[i-k+1,i]内,队头出队
if(h<=t && q[h]<i-k+1) h++;
// 当前值>=队尾值,队尾出队(如果当前值比队头还大,那么会一直出队到队头出队)
while(h<=t && a[i]>=a[q[t]]) t--;
// 当前值从队尾入队,注意入队的是下标,一直存储的都是下标
q[++t]=i;
// 如果i>=k表明起码占满一个窗口,此时队头中的是最大值
if(i>=k) cout<<a[q[h]]<<' ';
}
#include<bits/stdc++.h>
#define x first
#define y second
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
// 解题思路:
const int N=1e6+5;
int a[N],q[N]; // q是队列,建议数组模拟,不开O2优化的情况下数组都比STL库更快
int main() {
int n,k;
cin>>n>>k;
for(int i=1;i<=n;i++) {
scanf("%d",&a[i]);
}
// 维护窗口最小值O(n)
int h=1,t=0; // 当队尾t<队头h时,说明单调队列中没有元素
for(int i=1;i<=n;i++) { // 枚举队列中所有元素
// 新元素比当前队尾还小,说明新元素更优,那么把后面的全部移出去,最小的变进来
while(h<=t && a[q[t]]>=a[i]) t--;
q[++t]=i; // 注意,q存的都是下标嗷,表示把i从队尾入队
// 当新元素不是最优时,也会直接执行q[++t]=i加到当前窗口的末尾
if(q[h]<i-k+1) h++; // i-k+1是当前窗口的左边界,如果队头下标小于i-k+1,说明窗口右移
// 此时相当于队头出队
if(i>=k) cout<<a[q[h]]<<' '; // 能输出第一个窗口的大小时
}
cout<<endl;
// 维护最大值的单调队列,q[h]代表队头元素的下标
h=1,t=0;
for(int i=1;i<=n;i++) {
// 如果比队尾更优,则出队
while(h<=t && a[q[t]]<=a[i]) t--;
// 入队
q[++t]=i;
// 滑动窗口右移
if(q[h]<i-k+1) h++;
// 输出当前窗口最大值
if(i>=k) cout<<a[q[h]]<<' ';
}
return 0;
}
2:维护连续子序列的最大和
题目链接:
- 给定一个长度为n的整数序列,请找出长度不超过m的连续子序列的最大和。例如,数组 2 , − 3 , 5 , 2 , − 4 , − 1 , 8 {2,\ -3,\ 5,\ 2,\ -4,\ -1,\ 8} 2, −3, 5, 2, −4, −1, 8 , m m m 取 3 3 3 ,那么长度不超过 3 3 3 的连续子序列的最大和为 8 8 8
- 对于常规做法(左边),每次枚举一个窗口长度,再在这个窗口中枚举得到最大的连续子段和,时间复杂度是 O ( n 3 ) O(n^3) O(n3)
- 对于连续子段和(区间和),我们很容易想到预处理一个前缀和数组,这样就可以把计算连续字段和的时间从 O ( n ) O(n) O(n) 优化到 O ( 1 ) O(1) O(1), i i i 到 j j j 的间和计算公式是 s [ i ] − s [ j − 1 ] s[i]-s[j-1] s[i]−s[j−1],我们只要找到左端点 s [ l − 1 ] s[l-1] s[l−1] 的最小值,那么就可以求出 s [ i ] − m i n ( s [ j ] ) , j ∈ [ i − m , i − 1 ] s[i]-min(s[j]),\ j∈[i-m,i-1] s[i]−min(s[j]), j∈[i−m,i−1] 得到前 i i i 项的最大连续子段和,因为要枚举求得最小值,所以时间复杂度是 O ( n 2 ) O(n^2) O(n2)
- 但是,如果我们再用单调队列维护这个最小值,就不需要再枚举求连续区间和的最小值,时间复杂度会继续下降到 O ( n ) O(n) O(n)
#include<bits/stdc++.h>
#define x first
#define y second
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
// 解题思路:
const int N=1e5+5;
int a[N];
int q[N]; // 单调队列
int s[N]; // 前缀和序列
int main() {
int n,k;
cin>>n>>k; // 求的是长度不超过k的最大区间和
for(int i=1;i<=n;i++) {
scanf("%d",&a[i]);
s[i]=s[i-1]+a[i];
}
int h=0,t=0;
q[0]=0;
int ans=s[1]; // 初始化区间和的答案
for(int i=1;i<=n;i++) {
// 队头不在窗口[i-k,i-1]内,队头出队
if(h<=t && q[h]<i-k) h++; // 这里因为只用看前i-k个元素,窗口右边界小了1,因此不用加1
// 使用队头最小值,同样的因为队尾小了1,所以要把最小值在这里处理
ans=max(ans,s[i]-s[q[h]]);
// 当前值<=队尾值,队尾出队,把更小的s[i]放进队列中以维护s[i]的最小值
while(h<=t && s[i]<=s[q[t]]) t--;
// 从队尾正常入队
q[++t]=i; // 放进去的仍然是下标哈
}
cout<<ans<<endl;
return 0;
}
- 另一种写法,本人暂时还没验证是否正确,在维护滑动窗口最大值和最小值的基础上只改动了最后一步,即维护 a n s ans ans 的代码,因为 s [ i ] − s [ q [ h ] ] s[i]-s[q[h]] s[i]−s[q[h]] 中 s [ q [ h ] ] s[q[h]] s[q[h]] 已经前 i i i 项窗口大小为 [ 1 , k ] [1,k] [1,k] 的滑动窗口中前缀和的最小值,所以相减即可得到最大区间和
int ans=s[1];
h=1,t=0;
for(int i=1;i<=n;i++) {
while(h<=t && s[i]<=s[q[t]]) t--;
q[++t]=i;
if(h<=t && q[h]<i-k) h++;
ans=max(ans,s[i]-s[q[h]]);
}
- 如果窗口大小是从 [ s t , e n ] [st,en] [st,en](用户手动输入),那么模板可改为:
int h=1,t=0;
// 枚举每个元素
for(int i=1;i<=n;i++) {
// 对于区间长度在[st,en]之间,当i>=st才计算
if(i>=st) {
while(h<=t && s[i-st]<=s[q[t]]) t--;
q[++t]=i-st; // 偏移量是i-st
}
if(h<=t && q[h]<i-en) h++;
ans=max(ans,s[i]-s[q[h]]);
}