单调递增队列(全过程图文实现 另附习题)

什么是单调队列,有什么用?

不妨用一个问题来说明单调队列的作用和操作:
不断地向缓存数组里读入元素,也不时地去掉最老的元素,不定期的询问当前缓存数组里的最小的元素。
最直接的方法:普通队列实现缓存数组。
进队出队都是O(1),一次查询需要遍历当前队列的所有元素,故O(n)。

单调队列的运用

假设数列:7 8 1 1 4 3 2
现在我们要求出以3个元素为区间大小的条件下,每个区间的最小值。
初步判断:
第一个区间:7 8 1最小值为 1
第二个区间:8 1 1最小值为 1
第三个区间:1 1 4最小值为 1
第四个区间:1 4 3最小值为 1
第五个区间:4 3 2最小值为 2

单调队列如何实现?

从单调队列的定义我们知道,数列中的每个元素都要进队列,那么队列有进自然就有出,那出队又是怎么实现的呢?也许不应该称为出队,称为 覆盖 会更为恰当。

我们需要不断更新以下几点(变量定义):
①、已入队元素数量 i
②、队头元素在数列中的序号 start
③、队尾元素在数列中的序号 end
④、区间大小m

输出队头元素要满足三个条件:
①、该队列符合递增。
②、i - start < m。
③、i >= m 。


队列实现步骤

为了更清晰展现实现过程,下文将用变量来代替部分文字。
若对变量定义不理解的朋友,请看上方 变量定义 。
数列
①、首元素入队:7
队列
变量: i = 1, start = 1, m = 3
分析:
1、该队列符合递增,符合。
2、i - start < m,符合。
3、i < m ,不符合
结果:不输出队头元素 7 。


②、入队:8
第二个元素
变量: i = 2, start = 1, m = 3
分析:
1、该队列符合递增,符合。
2、i - start < m,符合。
3、i < m ,不符合
结果:不输出队头元素 7 。


③、入队:1
第三个元素
变量: i = 3, start = 1, m = 3
分析:
1、该队列不符合递增
操作:舍弃队列中比第三个元素1大的元素7 8。
队列
变量: i = 3, start = 3, m = 3
分析:
1、该队列符合递增,符合。
2、i - start < m,符合。
3、i >= m ,符合。
结果:输出队头元素 1 。


④、入队:1
队列
变量: i = 4, start = 3, m = 3
分析:
1、该队列符合递增,符合。
2、i - start < m,符合。
3、i >= m ,符合。
结果:输出队头元素 1 。


⑤、入队:4
队列
变量: i = 5, start = 3, m = 3
分析:
1、该队列符合递增,符合。
2、i - start < m,符合。
3、i >= m ,符合。
结果:输出队头元素 1 。


⑥、入队:3
队列
变量: i = 6, start = 3, m = 3
分析:
1、该队列不符合递增
操作:舍弃队列中比第六个元素3大的元素 4 。
队列
变量: i = 6, start = 3, m = 3
分析:
1、该队列符合递增,符合。
2、i - start == m ,不符合
队列
变量: i = 6, start = 4, m = 3
分析:
1、该队列符合递增,符合。
2、i - start < m,符合。
3、i >= m ,符合。
结果:输出队头元素 1 。


⑦、入队:2
队列
变量: i = 7, start = 4, m = 3
分析:
1、该队列不符合递增
操作:舍弃队列中比第七个元素2大的元素 3 。
队列
变量: i = 7, start = 4, m = 3
分析:
1、该队列符合递增,符合。
2、i - start < m,不符合
队列
变量: i = 7, start = 5, m = 3
分析:
1、该队列符合递增,符合。
2、i - start < m,符合。
3、i >= m ,符合。
结果:输出队头元素 2 。

最终输出:1 1 1 1 2
与初步判断输出相同!
完成!


习题

Problem One:m区间最小值
题目描述
一个含有n项的数列(n<=2000000),求出每一项前的m个数到它这个区间内的最小值。若前面的数不足m项则从第1个数开始,若前面没有数则输出0。

输入格式
第一行两个数n,m。
第二行,n个正整数,为所给定的数列。
输出格式
n行,第i行的一个数ai,为所求序列中第i个数前m个数的最小值。

输入输出样例
输入
6 2
7 8 1 4 3 2
输出
0
7
7
1
1
3
说明/提示
【数据规模】
m ≤ n ≤ 2000000
ai ≤ 3×107


洛谷测试平台
运用单调递增队列的思想来完成本题。
①、创建结构数组,保存每项数列的数据及排列顺序。

//维护一个单调递增的动态队列:start~end
struct node{
	int x;	//数据大小
	int y;	//排列顺序
}q[2000010];

②、不断维护一个单调递增的动态队列。
注意:队列长度不定,有效的部分q[start] ~ q[end]。

for(int i=1; i<n; i++){ 
	//start 元素不符合条件,该点已经不在查找的区间内 
	//end 始终不会大于 start+m
	if(i-q[start].y>m){
		start++;
	} 
	printf("%d\n", q[start].x);
	int tmp;
	scanf("%d", &tmp);
	//将 tmp 插入队列 start~end 中 
	while(end>start && q[end-1].x>tmp){
		end--;
	}
	q[end].x=tmp;//覆盖原本q[end]
	q[end].y=i;
	end++;
}

整合代码

#include<stdio.h>
struct node{
	int x;
	int y;
}q[2000010];

int main(){
	int n, m, start=0, end=1;
	scanf("%d%d", &n, &m);
	scanf("%d", &q[0].x);		//先插入队列一个元素 
								//防止队列为空 
	q[0].y=0;
	printf("0\n");	
	for(int i=1; i<n; i++){ 
		if(i-q[start].y>m){
			start++;
		} 
		printf("%d\n", q[start].x);
		int tmp;
		scanf("%d", &tmp);
		while(end>start && q[end-1].x>tmp){
			end--;
		}
		q[end].x=tmp;
		q[end].y=i;
		end++;
	}
	
	return 0;
} 

Problem two:
题目链接
题意:给定一个长度为 n 的数列,进行绕环移动。问所有前缀和都大于等于零的数列有多少。
输入:
每组第一行为 n 代表数列长度,第二行为数列。
当 n 为 0 程序结束。

输出:
每组对应一行 m,代表符合条件的数列个数。

输入样例:
3
2 2 1
3
-1 1 1
1
-1
0
输出样例:
3
2
0

样例说明
第一组:n=3,初始化数列为 2 2 1
可得到三组不同数列 2 2 1 、2 1 2 、1 2 2
每组的前缀和一直都大于0,三组都满足条件,m=3.


思考:如何判断判断该以 a 中第 x 个数为开头的数列前缀和是否都大于等于零?

一般
假如我们知道一个数列的最小前缀和,那么我们只要判断这一个最小前缀和是否大于或等于0。

拓展
假如我们知道以 a 中第 x 个数为开头的数列的最小前缀和,那么我们只要判断这一个最小前缀和是否大于或等于0。

实现
每一个数列开头必须在a1 ~ an-1的范围内,而且长度一定位 n 。
①、我们将数列复制再拼接得到新的数列 a
a1 a2 … an a1 a2…an

②、求出每一位的前缀和得到前缀和数列 sum
sum1 sum2 … sumn sumn+1 sumn+2…sumn+n

③、扫描sum数组,用单调队列维护一个长度为 n 的最小值。在 i >= n的情况下,判断 q[start] - sum[i] 是否大于或等于0


如何理解 q[start] - sum[i] 这操作?
由于每一个 sum[i] 值都是 第 i 项 的累加和。
以 i 为开头的最小前缀和 = 以 1 为开头的最小前缀和 - 以 i 为开头的累加和


实现代码

#include<stdio.h>
#include<memory.h>
int sum[2000020], a[2000020];

struct noed{
	int x;
	int y;
}q[2000020];

int main(){
	int n;
	sum[0]=0;
	
	while(scanf("%d", &n) && n){
		int ans=0;
		for(int i=1; i<=n; i++){
			scanf("%d", &a[i]);
			a[i+n]=a[i];
		}
		
		for(int i=1; i<=2*n; i++){
			sum[i] = sum[i-1] + a[i];
		}
		
		int start=1, end=1;
		q[1].x=sum[1];
		q[1].y=1;
		
		for(int i=1; i<2*n; i++){
			if(i-q[start].y >= n)
				start++;
			//					取不取等号都可
			while(end>start && q[end-1].x>=sum[i]) 
				end--;
			q[end].x=sum[i];
			q[end++].y=i;
			if(i>=n && q[start].x-sum[i-n]>=0)
				ans++;
		}
		
		printf("%d\n", ans);
	}
	
	return 0;
} 

若文章对你有帮助,留下了赞好不啦:)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值