读这一页之前,先知道读完要能做什么
- 写出来BFS 的队列五句:起点
push→while (!empty)→front→pop→ 邻居push - 写出来栈的三句
push/top/pop,并知道它和vector当栈是一回事 - 说清
deque两头都能动,滑动窗口的「右边进、左边出」就是靠它
这是什么
一句话:三个都是「只能按规矩进出」的容器,区别只在哪一头能动。
queue:一头进、另一头出(排队买票)⇒ 用在 BFS
stack:同一头进出(摞盘子)⇒ 用在括号、单调栈
deque:两头都能进、两头都能出 ⇒ 用在滑动窗口
★★ 动作的名字要背准:queue 取排头叫 front,stack 取顶叫 top;pop 一律不给值 —— 和堆一样,先看再扔,两句连着写。
⚠ 三个基本都不能按下标取、也不能遍历(deque 是例外,它能 d[i])。要按下标、要遍历、要留一份,就用 vector —— 第 2 块讲过「vector 当栈」,书里 stack<int> 只有几份在用,vector 有一千五百多份。
完整动作表
复杂度写的是这个动作自己的代价,不是整个程序的。「这一栏最容易错的地方」那一列是重点。
| 写法 | 干什么 | 代价 |
|---|---|---|
queue<int> q; | 开一个队列(先进先出) ★ 装什么都行: queue<pair<int, int>> 是 BFS 的标配 | O(1) |
q.push(x) | 从队尾进一个 | O(1) |
q.front() | 看排头那个(不弹) ⚠ 空队列上调它会出事 —— 先判 q.empty() | O(1) |
q.pop() | 让排头走人(不给返回值) ★★★ 先 front() 存下来,再 pop() —— 顺序反了拿到的是下一个 | O(1) |
q.empty() / q.size() | 空了吗 / 里面几个 ⚠ 没有 q.clear()(vector 才有) | O(1) |
stack<int> st; | 开一个栈(后进先出) ★ 取顶叫 top,不叫 front | O(1) |
st.push(x) / st.top() / st.pop() | 放 / 看顶 / 弹 ★★ 和 vector 的 push_back / back / pop_back 一一对应 | O(1) |
deque<int> d; | 开一个双端队列 ★ 它能 d[i] 下标取、也能范围 for 遍历 —— queue 和 stack 都不行 | O(1) |
d.push_front(x) / d.push_back(x) | 两头进 ★ 队列只有后面那半句 | O(1) |
d.pop_front() / d.pop_back() | 两头出 ★★ 滑动窗口的两句:新来的 push_back、过期的 pop_front | O(1) |
d.front() / d.back() | 两头的元素 | O(1) |
d[i] | 按下标取第 i 个 ★ 这是 deque 和 queue / stack 最实在的区别 | O(1) |
⭐ 这一节最容易错的地方(3 处)
这一节排在例题前面是故意的:读到一个坑,再去看正确的例子,记得住。 下面每一条都是「编译得过去、答案可能是错的」那种 —— 它们才是 OI 里真正扣分的地方。
① 取排头是两句:先 `front()` 再 `pop()`
pop() 只负责让排头走人,它不给任何值。
★ 写反了(先 pop 再 front)拿到的是下一个要处理的点,BFS 的处理顺序整个乱掉;只写 pop 忘了 front,这个点根本没被处理;★ 忘了 pop 最要命 —— 排头永远在,while 变成死循环,跑出来是 TLE,不是报错。int u = q.front(); // 先拿
q.pop(); // 再让排头走人② 一题多组数据,忘了清空 —— 而 `q.clear()` 不存在
queue 没有 clear():照着 vector 的习惯写 q.clear(); 会编译不过 —— 这反而是运气好的时候,运气差的是你根本没想到要清。// 最省事:声明在每一轮的开头,每轮都是新的
while (T--) {
queue<int> q; // ← 就这一句,清空的问题没了
...
}
// 已经声明在外面了:
while (!q.empty()) q.pop();③ 滑动窗口:新来的走队**尾**,过期的走队**头**
deque 两头都能动,所以方向写反了不会编译报错,只会算出别的数。
★ 规矩是「尾巴进、头出」:新元素 push_back,滑出窗口的 pop_front。反过来写,队列里留下的就是最旧的那几个,front() 拿到的也成了最新进来的 —— 程序照跑,最大值是别的数。while (!d.empty() && d.front() <= i - k) d.pop_front(); // 过期:队头走
d.push_back(i); // 新来的:队尾进
int mx = a[d.front()]; // 队头留着的才是最大deque 只是把「头」那半边也借给你用了一下。本书哪一章真的用到它
⚠ 下表里的文件路径和那串 probe 是逐字验过的(check:docs 第 ⑳ 条)—— 改了那份代码这儿会红,正是要的。点章号直接去看那一页。
| 章 | 书里的哪一份代码 | 用它来干什么 |
|---|---|---|
| 第 14 章 | code/14-bfs-maze/fast.cpp | ★★ 第 14 章 BFS —— 队列五句的原产地,第 14 章起每章都是它 |
| 第 35 章 | code/35-monotonic/p5788Stk.cpp | ★ 单调栈:把手写的 stk 数组换成 std::stack,动作一个不少 |
| 第 35 章 | code/35-monotonic/winWrongMax.cpp | ★★ 滑动窗口用 deque —— 例题 ② 同款,名字里那个 Wrong 就是「方向反了」的对照 |
例题(2 道)
每道都给全套:题面 → 样例 → 参考代码 → 一步步的讲解。先自己读题想一遍,再看代码。
例题 ① BFS 的队列五句
给一个 n × m 的网格(. 能走、# 是墙),从左上角走到右下角,每步只能走上下左右,求最少走几步。
★ 这一题的重点不是算法,是队列那五句 —— 把每次从队头取出的点打印出来,你就能亲眼看到「先进先出」长什么样。
n m。之后 n 行,每行一个长度 m 的串。输入
4 4 .... .#.. ...# ....
输出
访问顺序(每次从队头取出的点): (0,0) (1,0) (0,1) (2,0) (0,2) (3,0) (2,1) (1,2) (0,3) (3,1) (2,2) (1,3) (3,2) (3,3) 到右下角最少走 6 步 一共进队出队 14 个点,最后队列空了
(0,0) 的四个邻居,再下一层 —— 这就是「先进先出」。右下角在第 6 层,所以最少 6 步。★ 五句骨架照抄:起点 push → while (!q.empty()) → front 取出 → pop → 邻居 push。⚠ 还有一件事队列不帮你做 —— 「来过没有」要另记一张表。
// STL 专题 · queue —— BFS 的骨架就是这五句//// ① 起点 push 进去 ② while (!q.empty()) ③ front 取出排头// ④ pop 让排头走人 ⑤ 邻居 push 进去//// ⚠ ③④ 是**两句**,而且顺序不能反:pop() 只负责让人走,它不给任何值。// ⚠ 「来过没有」要另记一张表(这里用 dist == -1)—— 队列本身不帮你去重。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; cin >> n >> m; vector<string> g(n); for (string &row : g) cin >> row;
vector<vector<int>> dist(n, vector<int>(m, -1)); // -1 = 还没来过 queue<pair<int, int>> q; dist[0][0] = 0; q.push({0, 0});
const int dr[4] = {-1, 1, 0, 0}; const int dc[4] = {0, 0, -1, 1}; string order, line; int seen = 0;
while (!q.empty()) { auto [r, c] = q.front(); // ★ 先看排头是谁 q.pop(); // ★ 再让他走人 seen++;
line += "(" + to_string(r) + "," + to_string(c) + ") "; if (line.size() > 50) { order += line + "\n"; line.clear(); } // 每行不超过 50 列
for (int k = 0; k < 4; k++) { int nr = r + dr[k], nc = c + dc[k]; if (nr < 0 || nr >= n || nc < 0 || nc >= m) continue; if (g[nr][nc] == '#') continue; if (dist[nr][nc] != -1) continue; // ★ 来过就不再进队 dist[nr][nc] = dist[r][c] + 1; q.push({nr, nc}); } } if (!line.empty()) order += line;
cout << "访问顺序(每次从队头取出的点):\n" << order; cout << "\n到右下角最少走 " << dist[n - 1][m - 1] << " 步\n"; cout << "一共进队出队 " << seen << " 个点,最后队列" << (q.empty() ? "空了" : "没空") << "\n"; return 0;}点「运行 ▶」看结果
- 记距离:
dist[r][c] == -1表示还没来过,顺手当「访问过没有」用 —— 队列本身不去重,不记的话同一个点会进队好几次。 - ★★ 取出是两句:
auto [r, c] = q.front();然后q.pop();——pop不给任何值,写反了就拿到下一个点了。 - 四邻判定:出界、是墙、来过 —— 三条
continue,全过了才dist = 上一个 + 1再push。 - 答案在
push的时候就写好了:dist是入队时定的,所以走到右下角时dist[n-1][m-1]就是最少步数(BFS 先到的层数最浅)。
★★★ BFS 的骨架永远是那五句,第 14 章到第 30 章换的只是「点是什么」—— 点是坐标、是数字、是一个状态,队列的写法一个字都不变。
例题 ② 滑动窗口的最值:两头各管一头
给 n 个数和窗口长度 k,窗口从左往右每次挪一格,输出每个窗口里的最大值。
★ 这是 deque 存在的理由:新来的从队尾进、过期的从队头走 —— queue 只能动一头,做不了这件事。
n k。第二行 n 个整数。输入
8 3 3 1 4 1 5 9 2 6
输出
每个窗口的最大值: 窗口 [0, 2]:3 1 4 -> 最大 4 窗口 [1, 3]:1 4 1 -> 最大 4 窗口 [2, 4]:4 1 5 -> 最大 5 窗口 [3, 5]:1 5 9 -> 最大 9 窗口 [4, 6]:5 9 2 -> 最大 9 窗口 [5, 7]:9 2 6 -> 最大 9 走完之后队列里还剩 2 个候选下标(队头就是全局最大值的下标)
★ 三个动作的顺序别乱:① 队头过期了就 pop_front;② 队尾比新来的小的,永远轮不到它当最大,pop_back 弹掉;③ 新来的 push_back。
// STL 专题 · deque —— 滑动窗口的最小值 / 最大值//// ★ 窗口从左往右挪:**新来的从队尾进,过期的从队头走** —— 两头各管一头,// 这就是 deque 存在的理由(queue 只能动一头)。//// ★ 队列里存的是**下标**,不是值:// 过期判据 d.front() <= i - k (队头下标滑出窗口了)// 保单调 比新来的小的队尾元素,后面再也轮不到它当最大 → 弹掉// 打印 a[d.front()] 就是这个窗口的最大值//// ⚠ 方向写反不会编译报错,只会算出别的数 —— 见陷阱 ③。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, k; cin >> n >> k; vector<int> a(n); for (int &x : a) cin >> x;
deque<int> d; // 存下标 cout << "每个窗口的最大值:\n"; for (int i = 0; i < n; i++) { while (!d.empty() && d.front() <= i - k) d.pop_front(); // ★ 过期的从队头走 while (!d.empty() && a[d.back()] < a[i]) d.pop_back(); // 队尾比它小的,永远轮不到了 d.push_back(i); // ★ 新来的从队尾进
if (i >= k - 1) { cout << "窗口 [" << (i - k + 1) << ", " << i << "]:"; for (int j = i - k + 1; j <= i; j++) cout << a[j] << (j == i ? "" : " "); cout << " -> 最大 " << a[d.front()] << "\n"; } } cout << "\n走完之后队列里还剩 " << d.size() << " 个候选下标(队头就是全局最大值的下标)\n"; return 0;}点「运行 ▶」看结果
- 存下标、不存值:
deque<int> d;里放的是下标 —— 因为要回答「队头那个还在不在窗口里」,只有下标答得上来。 - ★★ 过期:
while (!d.empty() && d.front() <= i - k) d.pop_front();—— 队头下标滑出窗口了就走人(这就是「头出」)。 - 保单调:队尾那些比
a[i]小的,只要a[i]在窗口里它们就永远轮不上 ——while (!d.empty() && a[d.back()] < a[i]) d.pop_back();弹掉。 - 进:
d.push_back(i);;i >= k - 1之后,a[d.front()]就是这个窗口的最大值。
★★ deque = 两头都能动的 queue。「右边进、左边出」的题(滑动窗口最值、单调队列)都是这三句;第 35 章会展开讲。
用到的写法:deque<int> d; d.push_front(x) / d.push_back(x) d.pop_front() / d.pop_back() d.front() / d.back()
练习(2 道)
只给题面、样例和一句提示,答案是折叠着的 —— 先自己写一版。
练习 ① 后缀表达式:栈就是「等着被算」的那一摞
一行里是空格隔开的后缀表达式:数字直接进栈,遇到 + - * / 就弹出两个算完再压回去。求最后的值。
★ 后缀的好处是不用括号 —— 读的顺序就是算的顺序,而「等着被算的」全在栈里。
+ - * /),空格隔开。输入
3 4 + 2 * 7 -
输出
3 -> 栈里(顶在最左)3 4 -> 栈里(顶在最左)4 3 + -> 栈里(顶在最左)7 2 -> 栈里(顶在最左)2 7 * -> 栈里(顶在最左)14 7 -> 栈里(顶在最左)7 14 - -> 栈里(顶在最左)7 最后栈里剩 1 个,结果 = 7
★ 想清楚两件事:① 见到运算符要弹两个,谁是左谁是右 —— 先弹出来的是右操作数;② 算完的结果要压回去。
★★ 见到数就 push,见到运算符就弹两个、算完 push。⚠ 减法和除法会立刻暴露「左右写反」—— 加法乘法看不出来,所以别拿加法试。
练习 ② 约瑟夫环:队列就是「转圈」
n 个人围成一圈报数,报到 m 的人出列,然后从下一个人重新报数,直到只剩一个。输出出列顺序和剩下的人。
★ 队列把「围圈」这件事变成了两句:不出列的塞回队尾(转一圈),出列的不再塞回去。
n m。输入
7 3
输出
出列顺序: 3 6 2 7 5 1 最后剩下的是 4(队里还剩 1 个)
★ 一轮里要做两件事:前 m − 1 个人「拿出来再塞回队尾」,第 m 个人「拿出来就不要了」。⚠ 想清楚塞回队尾这一步忘了会怎样。
★★★ 「转圈」就是 q.push(q.front()); q.pop(); 两句话 —— 这两句的顺序也别反(先 pop 的话排头那个就永远拿不到了)。
用到的写法:queue<int> q; q.push(x) q.front() q.pop() q.empty() / q.size()
← 上一块:lower_bound 和 upper_bound:在排好序的数组上找 · 回 STL 专题目录 · 下一块:priority_queue:每次拿走最大的那个 →
⚠ 本页说的是 STL 怎么用。算法本身(Dijkstra、单调栈、状压……)在53 章正课里,而每一章末尾都会告诉你它用到了哪些 STL。