摘要:
单调队列的模板题,滑动窗口经典应用。
题目
给定一个大小为 $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 ++ ) { 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("");
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
|
#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]; 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
|
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(); } }
|
思考
对于这种需要维护一个性质的问题,通常可以这样思考:先假设有这样一个性质,再考虑如何构造和维护这个性质。