VIRTUALS

the virtual labs for the virtuals

0%

【AcWing算法基础】第二讲-数据结构-单调队列 AcWing 154. 滑动窗口

摘要:
单调队列的模板题,滑动窗口经典应用。

题目

给定一个大小为 $n≤10^6$ 的数组。
有一个大小为k的滑动窗口,它从数组的最左边移动到最右边。
您只能在窗口中看到k个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为[1 3 -1 -3 5 3 6 7],k为3。

窗口位置 最小值 最大值
[1 3 -1] -3 5 3 6 7 -1 3
1 [3 -1 -3] 5 3 6 7 -3 3
1 3 [-1 -3 5] 3 6 7 -3 5
1 3 -1 [-3 5 3] 6 7 -3 5
1 3 -1 -3 [5 3 6] 7 3 6
1 3 -1 -3 5 [3 6 7] 3 7

您的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。

输入格式
输入包含两行。
第一行包含两个整数n和k,分别代表数组长度和滑动窗口的长度。
第二行有n个整数,代表数组的具体数值。
同行数据之间用空格隔开。

输出格式
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。

输入样例:

8 3
1 3 -1 -3 5 3 6 7

输出样例:

-1 -3 -3 -3 3 3
3 3 5 5 6 7

思路

对于求滑动窗口中最小值的情况,假设对于窗口在任意位置时,存在一个单调上升的队列,这个队列的所有元素是窗口中所有元素表示集合的子集。那么我们可以用O(1)的时间得出窗口中的最小值,及队首元素。
现在考虑如何构建这个单调上升的队列。假设我们从原数组的中间某段开始移动窗口,并且考虑维护一个单调上升队列。我们应该怎么做?
显然,要想让当前窗口的元素构成单调上升的队列,那么当窗口元素单调下降时,必然要被排除。换句话说,当窗口左侧的元素比右侧大的时候,一定不会是当前窗口的最小值,也一定不会是将来窗口的最小值。因为它既比右侧元素大,又必然在它的右侧元素之前滑出窗口。
那么,对于窗口的每一步移动,当出现下降情况,我们必然需要从队尾往队首找到比当前入队的新元素小或等的数。
另外,根据题意,只有当窗口完全进入数组时,才需要输出最小值。但是,在完全进入之前,就已经开始构建单调队列了。这也是为什么上文需要”假设我们从原数组的中间某段开始移动窗口“,就是为了暂时只考虑中间段的情况。

我们来从宏观上观察一下单调队列的特性。因为我们按照滑动窗口的右侧边界遍历原数组,所以队列中应该放的是下标,以便于判断。
我们维护的单调队列所包含元素构成的集合一定是当前窗口元素构成集合的子集。
单调队列的左侧边界不一定非得是滑动窗口的左边届,因为上一步为了维护队列单调上升,可能从上一步的窗口中间开始维护递增,到了当前步,队列左边界就不会是窗口左边界而是中间位置。
单调队列可以包含任意个相同值,因为相同值不影响最值选取,换句话说就是完全可以占个坑。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>

using namespace std;

const int N = 1000010;

int a[N], q[N];

int main()
{
int n, k;
scanf("%d%d", &n, &k);
for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
// 求每个窗口最小值
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
// 判断队头是否已经滑出窗口,如果已经滑出了窗口,队列从hh处弹出一个数
if (hh <= tt && i - k + 1 > q[hh]) hh ++ ;
// 在i加入到队尾之前,先维护队列的单调递增性
// 如果a[i]比队尾对应值小的话,那么不能加入队列,否则队列会递减,此时队尾向左移动,直到在队列中找到一个值使得a[i]比它大(或者找完队列也没有找到)。那么这个时候再将i入队的话,对应的值就会单调递增了。
while (hh <= tt && a[q[tt]] >= a[i]) tt -- ;
q[ ++ tt] = i;
// 因为i表示窗口末端或者说队尾,而它又是从0开始的,那么在窗口完全进入数组之前,不需要输出最值。
// 即:i对应的窗口左端为i - k + 1,当i - k + 1 < 0时,是不需要输出最小值的。
// 但是由于需要从i = 0起开始维护队列的单调性,所以之前过程不能省略。
// 我们怎么能知道此时队列头对应的数一定是最小的呢?(当队列数量小于k的时候)
// 因为不管队列长度是刚好等于窗口长度,还是小于窗口长度,最左边的数一定是最小的。妙啊!
if (i >= k - 1) printf("%d ", a[q[hh]]); // 因为队列内部元素对应数组汇中的值是单调递增的,所以可以在O // (1)的时间复杂度内找到最小值
}

puts("");

// 求每个窗口最大值
hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
if (hh <= tt && i - k + 1 > q[hh]) hh ++ ;

while (hh <= tt && a[q[tt]] <= a[i]) tt -- ;
q[ ++ tt] = i;

if (i >= k - 1) printf("%d ", a[q[hh]]);
}

puts("");

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
* author: etoa
* 2021-08-12 23:52:01
*/
#include <bits/stdc++.h>

using namespace std;

constexpr int N = 1000006;
int a[N], q[N];
int hh = 0, tt = -1;

int main()
{
cin.tie(nullptr)->sync_with_stdio(false);
int n, k;
cin >> n >> k;
for (int i = 0; i < n; i++) cin >> a[i];
// 求最小值
// i表示滑动窗口的右侧边界
for (int i = 0; i < n; i++) {
// 注意队首所表示的原数组下标可能是窗口中间某下标
if (hh <= tt && i - k + 1 > q[hh]) ++hh;
// 维护队列单调上升
for (; hh <= tt && a[q[tt]] > a[i]; --tt);
// 队列直至为空或者找到一个合适的队尾
q[++tt] = i;
// 当滑动窗口完全进入数组时,开始计算最小值,但是在进入之前,上述单调上升队列也必须维护
if (i - k + 1 >= 0) cout << a[q[hh]] << ' ';
}
cout << endl;
hh = 0, tt = -1;
// 求最大值
for (int i = 0; i < n; i++) {
if (hh <= tt && i - k + 1 > q[hh]) ++hh;
for (; hh <= tt && a[q[tt]] < a[i]; --tt);
q[++tt] = i;
if (i - k + 1 >= 0) cout << a[q[hh]] << ' ';
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
* author: etoa
* 2021-08-13 09:40:27
*/
import java.util.*;
import java.io.*;

public class Main {
static final int N = (int)1e6 + 10;
static int[] a = new int[N], q = new int[N];
static int hh = 0, tt = -1;
static Scanner in = new Scanner(System.in);
static PrintWriter out = new PrintWriter(System.out);

public static void main(String[] args) {
int n = in.nextInt(), k = in.nextInt();
for (int i = 0; i < n; i++) a[i] = in.nextInt();
// 求最小值
for (int i = 0; i < n; i++) {
// 队列头溢出
if (hh <= tt && i - k + 1 > q[hh]) ++hh;
for (; hh <= tt && a[i] < a[q[tt]]; --tt);
q[++tt] = i;
if (i - k + 1 >= 0) out.print(a[q[hh]] + " ");
}
hh = 0;
tt = -1;
out.print("\n");
// 求最大值
for (int i = 0; i < n; i++) {
if (hh <= tt && i - k + 1 > q[hh]) ++hh;
for (; hh <= tt && a[i] > a[q[tt]]; --tt);
q[++tt] = i;
if (i - k + 1 >= 0) out.print(a[q[hh]] + " ");
}
out.flush();
}
}

思考

对于这种需要维护一个性质的问题,通常可以这样思考:先假设有这样一个性质,再考虑如何构造和维护这个性质。

原题链接: AcWing 154. 滑动窗口