【C++算法模板】单调队列维护滑动窗口最大最小值、维护连续子序列最大和

推荐学习视频: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]]<<' ';
}

题目链接:P1886 滑动窗口 /【模板】单调队列 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

#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[j1],我们只要找到左端点 s [ l − 1 ] s[l-1] s[l1] 的最小值,那么就可以求出 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[im,i1] 得到前 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]]);
}
  • 29
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值