单调队列

最近学习了单调队列,在此做一个小记


单调队列(百度百科)维持了队列的单调性,是队列的一种,与队列的区别有两点:一、单调队列是单调上升或单调下降的,队列无要求。二、队列是tail进,head出,而单调队列是tail进,但head和tail都可以出(如图)。
HaUr1U.png
举个栗子,如果一个队列中有1,4,7,13这几个数字,这时候又来了一个5。为了维持队列的单调性,我们要先把大于5的7和13弹出,在将5压入,队列就变成了1,4,5。单调队列是以比较为基础操作的,时间复杂度是线性的。具体看模板题。

洛谷 P1886 滑动窗口 /【模板】单调队列

题目描述

有一个长为 n 的序列 a,以及一个大小为 k 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。

例如:

The array is [1,3,−1,−3,5,3,6,7], and k = 3。

输入格式

输入一共有两行,第一行有两个正整数 n,k。 第二行 n 个整数,表示序列 a

输出格式

输出共两行,第一行为每次窗口滑动的最小值

第二行为每次窗口滑动的最大值

输入输出样例

输入 #1

点击查看输入
8 3
1 3 -1 -3 5 3 6 7
#### 输出 #1
点击查看输出
-1 -3 -3 -3 3 3
3 3 5 5 6 7
### 说明/提示 【数据范围】

1≤k≤n≤10^6, -231≤a[i]≤231


看到这道题最先想到的必然是暴力,时间复杂度为O(nk),明显会超时。

这道题是单调队列的模板题。以找最大值为例,如果有两个点,下标分别为i和j。如果i>j且a[i]>a[j],那么这个j就毫无用处了。因为区间是向右滑动的,所以无论如何j也不可能是最大值了。相反的,如果i<j但是a[i]<a[j]呢?这时,j是需要被保留的,因为i可能不在下个区间了。因此,在每个区间判断前,都要先把队头的不在这个区间内的弹出。以上就是思路,下面附上代码:

点击查看代码
#include<cstdio>
#include<iostream>
#include<cstring>
#define MAXN 1000000
using namespace std;
int read() {
	int sum = 0, f = 1;
	char ch = getchar();
	while(!isdigit(ch)) {
		if(ch == '-') {
			f = -1;
		}
		ch = getchar();
	}
	while(isdigit(ch)) {
		sum = sum * 10 + ch - '0';
		ch = getchar();
	}
	return sum * f;
}
int a[MAXN + 9];
int q[MAXN + 9];
int n, k;
void min() {
	int head = 1, tail = 0;
	for(int i = 1; i <= n; ++i) {
		while(head <= tail && q[head] + k < i + 1) head++;
		while(head <= tail && a[i] < a[q[tail]]) tail--;
		q[++tail] = i;
		if(i >= k) printf("%d ", a[q[head]]);
	}
}
void max() {
	int head = 1, tail = 0;
	for(int i = 1; i <= n; i++) {
		while(head <= tail && q[head] + k < i + 1) head++;
		while(head <= tail && a[i] > a[q[tail]]) tail--;
		q[++tail] = i;
		if(i >= k) printf("%d ", a[q[head]]);
	}
}
int main() {
	n = read(), k = read();
	for(int i = 1; i <= n; ++i) {
		a[i] = read();
	}
	min();
	printf("\n");
	memset(q, 0, sizeof(q));
	max();
	return 0;
}
--- --- 单调队列主要用以处理一些区间求最值得问题,下面就来看一个简单地应用。 ## 洛谷 P1714 切蛋糕 ### 题目描述 今天是小 Z 的生日,同学们为他带来了一块蛋糕。这块蛋糕是一个长方体,被用不同色彩分成了 n 个相同的小块,每小块都有对应的幸运值。

小 Z 作为寿星,自然希望吃到的蛋糕的幸运值总和最大,但小 Z 最多又只能吃 m(m≤n) 小块的蛋糕。

请你帮他从这 n 小块中找出连续的 k(1≤k≤m) 块蛋糕,使得其上的总幸运值最大。

输入格式

第一行两个整数 n,m。分别代表共有 n 小块蛋糕,小 Z 最多只能吃 m 小块。

第二行 n 个整数,第 i 个整数 p[i] 代表第 i 小块蛋糕的幸运值。

输出格式

仅一行一个整数,即小 Z 能够得到的最大幸运值。

输入输出样例

输入 #1

点击查看输入
5 2
1 2 3 4 5
#### 输出 #1
点击查看输出
9
#### 输入 #2
点击查看输入
6 3
1 -2 3 -4 5 -6
#### 输出 #2
点击查看输出
5
### 说明/提示 【数据范围】 1≤n≤5×10^5,∣p[i]∣≤500。 保证答案在int范围以内,且为非负整数。

简化一下问题,就是求最大不定长字段和的问题。设f[i]为以i为右端点的最大字段和,则f[i]=max{sum[i]-sum[j] (i-m≤j≤i-1)}。其中sum为前缀和预处理,这点不难想到。观察发现,sum[i]始终为定值,所以max{sum[i]-sum[j] (i-m≤j≤i-1)}可以转化为sum[i]-min{sum[j] (i-m≤j≤i-1)}。因为每一次j的循环都花了非常多的时间,时间复杂度为O(nm),所以数据一大就会T掉,因此考虑用单调队列维护min{sum[j] (i-m≤j≤i-1)}。以上即为基本思路,下面附上代码:

点击查看代码
#include<cstdio>
#include<iostream>
#define MAXN 500000
using namespace std;
int read() {
	int f = 1, sum = 0;
	char ch = getchar();
	while(!isdigit(ch)) {
		if(ch == '-') f = -1;
		ch = getchar();
	}
	while(isdigit(ch)) {
		sum = sum * 10 + ch - '0';
		ch = getchar();
	}
	return f * sum;
}
struct Q {
	int id, sum;
};
Q q[MAXN * 2 + 9];
int sum[MAXN + 9];
int main() {
	int n = read(), m = read();
	for(int i = 1; i <= n; ++i) {
		sum[i] = read();
		sum[i] += sum[i - 1];
	}
	int head = 1, tail = 0;
	q[++tail].id = 0;
	q[tail].sum = 0;
	int ans = - 1e9;
	for(int i = 1; i <= n; ++i) {
		while(head <= tail && q[head].id < i - m) {
			++head;
		}
		while(head <= tail && q[tail].sum >= sum[i]) {
			--tail;
		}
		ans = max(ans, sum[i] - q[head].sum);
		q[++tail].id = i;
		q[tail].sum = sum[i];
	}
	printf("%d", ans);
	return 0;
}
--- 以上即为单调队列的应用例子。单调队列使用频率不高,但每次使用都会有意想不到的作用,在OI/ACM中也是各种神奇算法的基础。在题目中,若出现形同max{sum[j] (j的范围)}、min{sum[j] (j的范围)}这样的区间最值问题,就可以考虑用单调队列去维护。当然,更多的还是要靠刷题巩固。 # 到此结束,欢迎指错!
posted @ 2022-02-11 19:34  TuSalcc  阅读(126)  评论(0)    收藏  举报