本文已参与「新人创作礼」活动,一起开启掘金创作之路。单调队列

89 阅读6分钟

1.前言

兩個字 單調

单调队列和单调栈一样:单调队列和单调栈一样:

单调栈是维护一个单调的栈,单调队列就是维护一个单调的队列,通过单调的性质解决问题。

2.单调队列简述

单调队列一般分为单调递增队列单调递减队列。 单调递增队列是解决最小值的问题 单调递减队列是解决最大值的问题

滑动窗口

题目描述

给定一个长度为N(N≤10^6)的数列A,求A中所有长度为k(k<=n)的子序列A[l, r]的最大值。
输入格式
第1行:两个用空格分隔的整数n和k。

第2行:n个用空格分隔的整数,表示数列中的每个数(每个数的范围都在[-10^9, 10^9]
输出格式
1行:从左到右依次输出A数列中所有长度为k的区间的最大值,两个数之间用一个空格分隔。
输入输出样列
输入样例18 3
1 3 -1 -3 5 3 6 7

输出样例13 3 5 5 6 7

在这里插入图片描述

思路1:枚举

枚举每一个长度为k的区间[i,i+k-1],然后取最大值。 对于每一个i的取值范围是[1,n-k+1] 时间复杂度O(n^2^)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10, INF = 0x3f3f3f3f;
int a[N], n, k;
int main () {
	cin >> n >> k;
	for (int i = 1; i <= n; i ++ ) cin >> a[i];
	for (int i = 1; i <= n - k + 1; i ++ ) {
		int maxs = -INF;
		for (int j = i; j < i + k; j ++ )
			maxs = max (maxs, a[j]);
		cout << maxs << '\n';
	}
	return 0;
}

思路2:单调队列

优化思路:

考虑尽可能的排除不可能是答案的值,降低第二层for循环的次数。考虑尽可能的排除不可能是答案的值,降低第二层for循环的次数。

单调队列思想:

继续,首个窗口覆盖的区间范围为[1,k],同时有i, j满足: 1.i<=k&&j<=k 2.i<j 简单点说也就是i, j是[1,k]两数,并且i<j 继续像单调栈一样分析A[i]和A[j]的情况:

  1. A[i]<A[j]:则A[i]是一个无用的值,因为显然它不可能作为所在窗口的值。
  2. A[i]=A[j]:和A[i]<A[j]一样,虽然A[i]=A[j],但是A[j]的位置更优,因为在这个问题里存在过期,就是当前窗口的第一个值在下一个窗口就会过期、不能使用,所以说明A[i]会比A[j]更先过期。

结论可以出来了:结论可以出来了:

当处理到一个A[j]时,就可以排除不可能的答案也就是前面所有<=A[i]的元素

剔除过期元素:

对于队列里的元素可以只记一个下标,不仅可以查到下标,也可以通过下标来查值。 所以如果下标index满足index>=i-k+1&&index<=i说明index在当前窗口内。 反过来:如果下标index满足index<=i-k说明index不在当前窗口内。

思考使用什么数据结构实现

需要满足的要求:

  1. 可以实现队头出队的操作(剔除过期元素)
  2. 踢掉队尾元素(剔除所有<=A[j]元素)
  3. 入队(将当前元素加入队尾)

并且这些操作必须是O(1)的时间复杂度完成。 发现双端队列deque可以实现: 第一个是dequepop_front操作 第二个是dequepop_back操作 最后一个是dequepush_back操作

最后上Dear Code:

#include <bits/stdc++.h>
#define FOR(i, a, b) for(int i = a; i <= b; i ++ )
#define DOR(i, a, b) for(int i = b; i >= a; i -- )
using namespace std;
typedef long long LL;
typedef unsigned long long ULL;
const int N = 1e6 + 10;
int n, k, a[N];
deque<int> dq;
int main () {
	scanf ("%d%d", &n, &k);
	for (int i = 1; i <= n; i ++ ) {
		scanf ("%d", &a[i]);
		if (i - k + 1 > dq.front ()) dq.pop_front ();
		while (dq.size () && a[i] >= a[dq.back ()]) dq.pop_back ();
		dq.push_back (i);
		if (i >= k) printf ("%d ", a[dq.front ()]);
	}
	return 0;
}

滑动窗口类问题

题目描述

小科的期中考试成绩单拿到了,小科共有N门课,其中第i门课考了S[i]分(可能为负
数),N门课的成绩按照S[1]到S[N]的顺序记录在成绩单上。成绩单需要家长签字,所
以小科需要将成绩单拿给自己的爸爸大科签字。爸爸大科的心情会受到成绩单上分数
的影响,确切的说大科任意时刻的心情等于他已经看过的分数之和。如果分数之和为
负数大科的爸爸就会发怒,一旦发怒他就会揍小科。小科不想挨揍,所以他想调整一
下给爸爸看分数的顺序。由于分数都是连续的写在成绩单上的,所以小科没法跳跃着
给爸爸看分数,但是他可以选择让爸爸从哪门课开始看起。比如,他可以让爸爸从第5
门课开始看起,这样的话大科会先依次的看完第5门课到第N门课,然后再回头看第1门
课到第4门课。也就是说如果小科选择让爸爸从第i们课开始看,那么大科将会先依次看
完第i到第N门课的成绩,再依次看完第1到第i-1门课的成绩。初试时大科的心情是0,
大科每看完一门课的成绩后如果心情变为负数就会揍小科。小科想知道为了使自己不
挨揍,有多少个i可以作为给爸爸看的第一门课。请你帮帮小科。
输入格式

第1行:一个正整数N,表示小科考试的科目数。

第2行:N个空格分隔的整数,其中第i个整数S[i],表示第i门课的成绩。
输出格式

一行:一个整数,表示有多少个i可以作为起点,使得小科不会挨揍。
输入输出样列
输入样例15
-5 4 -1 3 2

输出样例12

说明

【样例说明】

小科可以选择以第2门课为起始,或者第4门课为起始给大科看。


【数据范围】

1 <= N <= 10^6, -1000 <= S[i] <= 1000

题目类型:环形的单调队列问题 算法思路:枚举1-N作为起点,判断是否能够让小科不挨揍(任意时刻分数的累加和都>=0) 小科爸爸的5次心情,分别是子段s[2, 2], s[2, 3], s[2, 4], s[2, 5], s[2, 6(1)]的子段和。 为了方便处理右端点>N的子段,可以将原数据复制一份,s[N+1] = s[1], s[N+2] = s[2],以此类推。 子段和可以通过前缀和计算获得:

s[2, 2] = sum[2] - sum[1]
s[2, 3] = sum[3] - sum[1]
s[2, 4] = sum[4] - sum[1]
s[2, 5] = sum[5] - sum[1]
s[2, 6] = sum[6] - sum[1]

当以i为起点时,计算子段和计算时都要减去sum[i-1],所以可以先算出sum[i]sum[i+n-1] 的最小值,然后再减去sum[i-1]判断是否<0。 随着i的增大,区间[i, i+n-1]也在右移,问题转换为求宽度为N的滑动窗口的最小值。 算法过程: ① 将S数组复制一份,从N扩大为2N。 ② 求前缀和sum[1]sum[2N]。 ③ 利用单调队列在数组sum中求长度为N的滑动窗口的最小值。 ④ 当以i为左端点时,判断sum[i]sum[i+n-1]的最小值 - sum[i-1]是否<0

最后上Dear Code:

#include <bits/stdc++.h>
#define FOR(i, a, b) for(int i = a; i <= b; i ++ )
#define DOR(i, a, b) for(int i = b; i >= a; i -- )
using namespace std;
typedef long long LL;
typedef unsigned long long ULL;
const int N = 2e6 + 10;
int n, k, a[N], mins[N];
deque<int> dq;
int main () {
	scanf ("%d", &n); k = n;
	for (int i = 1; i <= n; i ++ ) {
		cin >> a[i];
		a[i + n] = a[i];
	} n <<= 1;
	for (int i = 1; i <= n; i ++ ) a[i] += a[i - 1];
	for (int i = 1; i < n; i ++ ) {
		if (i - k + 1 > dq.front ()) dq.pop_front ();
		while (dq.size () && a[i] <= a[dq.back ()]) dq.pop_back ();
		dq.push_back (i);
		if (i >= k) mins[i - k + 1] = a[dq.front ()];
	} int res = 0;
	for (int i = 1; i <= k; i ++ ) {
		int val = mins[i] - a[i - 1];
		if (val >= 0) res ++;
	} printf ("%d", res);
	return 0;
}

合并果子

在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同
的堆。多多决定把所有的果子合成一堆。

每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之
和。可以看出,所有的果子经过n-1次合并之后,就只剩下一堆了。多多在合并果子时
总共消耗的体力等于每次合并所耗体力之和。

因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。
假定每个果子重量都为1,并且已知果子的种类数和每种果子的数目,你的任务是设计
出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。

例如有3种果子,数目依次为1,2,9。可以先将1、2堆合并,新堆数目为3,耗费体力
为3。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为12,耗费体力为12。
所以多多总共耗费体力=3+12=15。可以证明15为最小的体力耗费值。

将水果堆分成两类:还未合并的原始堆和合并后形成的新堆。 考虑排序好的原始堆:如果不将合并后的新堆加入原始堆,则原始堆始终 是有序的。 同时,对于合并后的新堆,后合并出来的堆中的果子数量一定大于先合并 出来的。 可以使用两个单调递增队列q1和q2分别存储原始堆和合并形成的新堆。

每次比较q1和q2的队首元素,取两者较小的,生成的新堆加入新堆队列 q2。

最后上Dear Code:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 10;
int n, res, a[N];
queue<int> q1, q2;
int main () {
	cin >> n;
	for (int i = 1; i <= n; i ++ ) cin >> a[i];
	sort (a + 1, a + 1 + n);
	for (int i = 1; i <= n; i ++ ) q1.push (a[i]);
	for (int i = 1; i < n; i ++ ) {
		int s = 0;
		for (int j = 1; j < 3; j ++ )
			if (q1.empty ()) s += q2.front (), q2.pop ();
			else if (q2.empty ()) s += q1.front (), q1.pop ();
			else {
				if (q1.front () < q2.front ()) s += q1.front (), q1.pop ();
				else s += q2.front (), q2.pop ();
			}
		res += s;
		q2.push (s);
	}
	cout << res;
	return 0;
}