数组模拟队列。
例题:829. 模拟队列(模板题)
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
| 实现一个队列,队列初始为空,支持四种操作: push x – 向队尾插入一个数 x; pop – 从队头弹出一个数; empty – 判断队列是否为空; query – 查询队头元素。 现在要对队列进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。
输入格式 第一行包含整数 M,表示操作次数。 接下来 M 行,每行包含一个操作命令,操作命令为 push x,pop,empty,query 中的一种。
输出格式 对于每个 empty 和 query 操作都要输出一个查询结果,每个结果占一行。 其中,empty 操作的查询结果为 YES 或 NO,query 操作的查询结果为一个整数,表示队头元素的值。
数据范围 1≤M≤100000, 1≤x≤10^9, 所有操作保证合法。
输入样例: 10 push 6 empty query pop empty push 3 push 4 pop query push 6 输出样例: NO 6 YES 4
|
两种写法:(选择哪种看个人习惯,栈也是一样)
1.tt=-1, hh=0,q[++tt]=x, hh<=tt 表示队列非空
2.tt=0, hh=0,q[tt++]=x, hh<tt 表示队列非空
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
| #include <iostream> using namespace std; const int N = 100010; int q[N],head = 0,tail = -1;
int main(){ ios::sync_with_stdio(false);
int n;cin >> n;
string op; int x; while (n--){ cin >> op; if (op == "push"){ cin >> x; q[++tail] = x; } if (op == "pop"){ head ++; } if (op == "empty"){ cout << (head <= tail ? "NO" : "YES") << "\n"; } if (op == "query"){ cout << q[head] << "\n"; } } return 0; }
|
补充一个y总的循环队列的模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| int q[N], hh = 0, tt = 0;
q[tt ++ ] = x; if (tt == N) tt = 0;
hh ++ ; if (hh == N) hh = 0;
q[hh];
if (hh != tt){ }
|
2.2:单调栈与单调队列
什么是单调栈?
单调栈的定义:
- 单调栈里面的元素具有单调性,单调递增或递减;
- 元素加入栈前会把栈顶破坏单调性的元素删除,直到栈内元素满足单调性;
单调栈的用途如下:
例题:830. 单调栈(模板题)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。
输入格式 第一行包含整数 N,表示数列长度。 第二行包含 N 个整数,表示整数数列。
输出格式 共一行,包含 N 个整数,其中第 i 个数表示第 i 个数的左边第一个比它小的数,如果不存在则输出 −1。
数据范围 1≤N≤10^5 1≤数列中元素≤10^9 输入样例: 5 3 4 2 7 5 输出样例: -1 3 -1 2 2
|
参考题解1: https://www.acwing.com/solution/content/13981/。
参考题解2: https://www.acwing.com/solution/content/27437/。(动图解释)
思路:
和双指针做法类似,先考虑一个暴力做法,再来想如何优化。
暴力做法很好写:
1 2 3 4 5 6 7 8 9 10 11
| for (int i = 0;i < n;i++){ int flag = 0; for (int j = i-1;j >= 0;j--){ if (a[i] > a[j]){ cout << a[j] << " "; flag = 1; break; } } if (flag == 0) cout << -1 << " "; }
|
这肯定超时,时间复杂度为O(n^2),需要优化。
现在求 a[t]
左边第一个比它小的数, 考虑两个数,a[x] <= a[y] < a[t]
,且x < y < t
,那么可能会输出a[y]
,一定不会输出a[x]
。
所以我们用到的数字序列必然是满足单调递增的,不满足单调性的数可以直接删掉,可以用单增栈来处理。
通过本题,单调栈的模型一共可用于4种题型,求序列中每个数左、右边第一个比它大、小的数。
图解:(来自上面参考题解1)

对于将要入栈的元素来说,在对栈进行更新后(即弹出了所有比自己大的元素),此时栈顶元素就是数组中左侧第一个比自己小的元素;

对于将要入栈的元素来说,在对栈进行更新后(即弹出了所有比自己小的元素),此时栈顶元素就是数组中左侧第一个比自己大的元素。
y总代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <iostream> using namespace std; const int N = 1e5+5; int stk[N],top;
int main(){ ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int n;cin >> n; int x; for (int i = 0;i < n;i++){ cin >> x; while (top && stk[top] >= x) top--; if (top) cout << stk[top] << ' '; else cout << -1 << ' '; stk[++top] = x; } return 0; }
|
时间复杂度分析:对于序列中每个数,最多进栈一次加出栈一次,所以是O(n)。
关于cin加速的补充:cin.tie(NULL);// NULL可以换成0
,cout也是类似的。
在ACM里,经常出现数据集超大造成 cin TLE的情况。在默认的情况下cin绑定的是cout,每次执行 << 操作符的时候都要调用flush,这样会增加IO负担。可以通过tie(0)(0表示NULL)来解除cin与cou t的绑定,进一步加快执行效率。
ios::sync_with_stdio(false);
的优化效果比起上面的要差不少。
scanf\printf速度比较快,而且很少出问题。cin\cout的优化可能会导致一些bug,不要混用scanf。
什么是单调队列?
单调队列:可以用来维护(给定大小的)区间的最值,其时间复杂度为O(n),其中n为序列的元素个数。
- 单调递增队列:保证队列头元素一定是当前队列的最小值,用于维护区间的最小值。
- 单调递减队列:保证队列头元素一定是当前队列的最大值,用于维护区间的最大值。
参考秦大佬讲义: https://www.acwing.com/blog/content/150/。
参考资料: https://www.jianshu.com/p/e59d51e1eef5。
滑动窗口是单调队列的最典型应用。单调队列还能用于多重背包的优化。
例题:154. 滑动窗口(模板题)
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
| 给定一个大小为 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
|
本题可以用单调队列、线段树、ST表等算法解决。
单调队列算法思路与单调栈类似,先考虑一个暴力做法,再来想如何优化,删除不满足单调性元素。
暴力做法很好写:
用队列来模拟滑动窗口,直接遍历窗口中所有元素取极值。
时间复杂度是O(n*k),k是窗口大小,窗口滑动次数是O(n)级别。
先来看如何求最小值。
我们可以发现一个性质:
如果队列中存在两个元素,满足 a[i] >= a[j]
且 i < j
,那么无论在什么时候我们都不会取 a[i]
作为最小值了,所以可以直接将 a[i]
删掉;
此时队列中剩下的元素严格单调递增,所以队头就是整个队列中的最小值,可以用 O(1)的时间找到;
为了维护队列的这个性质,我们在往队尾插入元素之前,先将队尾>=当前数的元素全部弹出即可;
这样所有数均只进队一次,出队一次,所以时间复杂度是 O(n) 的。
图解:

有了单调的性质,单调队列不仅能很快求出最值,也能结合二分找到特定值。
1.y总手写单调队列。
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 <iostream> using namespace std; const int N = 1e6+5; int a[N],q[N];
int main(){ cin.tie(0); cout.tie(0); ios::sync_with_stdio(false);
int n,k; cin >> n >> k; for (int i = 0;i < n;i++) cin >> a[i];
int head = 0,tail = -1; for (int i = 0;i < n;i++){ if (head <= tail && i-k+1 > q[head]) head++; while (head <= tail && a[q[tail]] >= a[i]) tail--; q[++tail] = i; if (i >= k-1) cout << a[q[head]] << ' '; } cout << "\n"; head = 0,tail = -1; for (int i = 0;i < n;i++){ if (head <= tail && i-k+1 > q[head]) head++; while (head <= tail && a[q[tail]] <= a[i]) tail--; q[++tail] = i; if (i >= k-1) cout << a[q[head]] << ' '; } return 0; }
|
时间复杂度:每个元素只会入队一次, 出队一次, 因此时间复杂度为O(n)。
单调队列是一种特殊的队列,可以从队头、队尾弹出元素,队尾插入元素,就是双端队列的一种。
关于队头是否滑出窗口的解释:
i - k + 1
是以i
为右端点、长度为k的区间的左端点,如果q[head]
的值比左端点小,那就说明队头节点已经不在区间中了,需要弹出。
为什么队列中存的是数组下标而不是元素值?
存放元素值也能做但比较复杂。判断队头元素是否还在窗口中,这一步需要用到下标,存下标比较方便。数组下标唯一,而元素值不一定唯一。
2.STL deque双端队列。(比手写要慢约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
| #include <iostream> #include <deque> using namespace std;
const int N = 1e6+5; int a[N]; deque<int> q; int main(){ ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
int n,k; cin >> n >> k;
for (int i = 0;i < n;i++) cin >> a[i]; for (int i = 0;i < n;i++){ while (!q.empty() && i-k+1 > q.front()) q.pop_front(); while (!q.empty() && a[q.back()] >= a[i]) q.pop_back(); q.push_back(i); if (i-k+1 >= 0) cout << a[q.front()] << ' '; } cout << "\n"; q.clear(); for (int i = 0;i < n;i++){ while (!q.empty() && i-k+1 > q.front()) q.pop_front(); while (!q.empty() && a[q.back()] <= a[i]) q.pop_back(); q.push_back(i); if (i-k+1 >= 0) cout << a[q.front()] << ' '; } return 0; }
|
注意:单调栈与单调队列在插入、弹出元素时要保证栈、队内元素的严格单调性,判断时必须取等号。
如果不是严格单调则不必加上等号。