例题给全套:题面、样例、参考代码、以及一步步的讲解。先自己读题想一遍, 再看代码,最后对着讲解核一遍自己想的对不对。
练习只给题面、样例和一句提示,答案是折叠起来的 —— 请先自己写一版跑通了再展开。写不出来也别直接看答案,先回速查那一组找找。
每份代码都能直接点运行:输入框里已经填好了样例输入, 把它改一改再跑,是这一页最值钱的用法。
先把地基摆一遍
这五个容器解决的是同一类问题:我要把一堆东西存起来,然后按某种规矩取出来。差别只在「按什么规矩取」—— 谁先进先出、谁后进先出、谁永远给最大的、谁能快速查在不在。
★ 一张表就能选完:要「一层一层地扩散」用 queue(BFS);要「走到底再退回来」用 stack(不过 DFS 直接写递归就行,递归自带栈);要「每次拿当前最小/最大的」用 priority_queue(Dijkstra、合并果子);要「查一个东西在不在、而且要去重」用 set;要「按一个键查一个值」用 map。
⚠ 代价要知道:queue / stack / vector 的动作都是 O(1),而 priority_queue / set / map 的每一次操作是 O(log n) —— 在 10⁵ 次操作的题里无所谓,在 10⁷ 次的内层循环里就会成为瓶颈。
★ 还有一条新手常卡的语法:queue<int> q; 里的尖括号写「装什么」,所以装 pair 就是 queue<pair<int,int>> q;,装结构体就是 queue<Node> q;。
例题(2 道)
每道都给全套:题面 → 样例 → 参考代码 → 一步步的讲解。
例题 ① 合并果子(小规模版)
有 n 堆果子,第 i 堆重 aᵢ。每次可以把两堆合并成一堆,代价是这两堆的重量之和;合成的新堆还可以继续参与合并。
一直合到只剩一堆为止。请问总代价最小是多少?
★ 结论先给你(第 37 章会证):每次都挑当前最轻的两堆合,总代价就是最小的。
n(1 ≤ n ≤ 10⁵);第二行 n 个正整数 aᵢ(aᵢ ≤ 10⁴)。输入
3 1 2 9
输出
15
每一步都要「取出当前最小的那个」,而且合出来的新堆还要放回去继续参与。⇒ 选型表上哪一行是干这个的?
// 例题 ①「合并果子(小规模版)」—— STL 容器组//// 每次都要「取出当前最小的那个」⇒ 小根堆。这正是第 37 章那道原题。// ⚠ 这一长串 priority_queue<int, vector<int>, greater<int>> 背下来就行:// 三个参数分别是「装什么」「底下用什么容器装」「怎么比」,// 中间那个 vector<int> 是默认值,只是因为要写第三个,才不得不把它补上。
#include <bits/stdc++.h>using namespace std;
int main() { int n; cin >> n;
priority_queue<int, vector<int>, greater<int>> q; // 小根堆:top() 是最小的 for (int i = 0; i < n; i++) { int x; cin >> x; q.push(x); }
long long cost = 0; while (q.size() >= 2) { int a = q.top(); q.pop(); // ⚠ pop() 不返回值 —— 要先 top() 拿到再 pop() int b = q.top(); q.pop(); cost += a + b; q.push(a + b); }
cout << cost << "\n"; return 0;}点「运行 ▶」看结果
- 「反复取出当前最小的」⇒ 小根堆
priority_queue<int, vector<int>, greater<int>>。这一长串背下来就行:三个参数分别是「装什么」「底下用什么容器装」「怎么比」,中间那个vector<int>是默认值,只是因为要写第三个参数,才不得不把它也补上。 - ⚠ 它不是排好序的 —— 它只保证堆顶是最小的那一个。想按顺序全拿出来,只能一个一个
pop。 - ⚠
pop()不返回值:要先q.top()拿到那个数,再q.pop()把它删掉。(queue和stack也一样,只是取元素的名字分别叫front和top。) - 循环条件写
while (q.size() >= 2):剩一堆就不用再合了。 - ⚠ 总代价要用
long long:10⁵ 堆、每堆 10⁴,光是总重量就 10⁹,而合并的总代价还要比它大好几倍(每个数会被重复计入若干层)。
这就是第 37 章那道原题。⇒ 而这一页真正的收获是那句选型:看到「反复取当前最优的那个」,就该想到 priority_queue。
用到的写法:priority_queue<int, vector<int>, greater<int>> priority_queue<int>
例题 ② 每个单词出现了几次
给一段由空格和换行分隔的单词,统计每个单词出现了几次,按字典序输出。
输入
the cat the hat
输出
cat 1 hat 1 the 2
the 出现两次,cat、hat 各一次 —— 输出按字典序。单词是字符串,开不出「用它当下标」的数组。⇒ 选型表上「按一个键查一个值」那一行。⚠ 还有一件顺手的好事:输出要求按字典序,而那个容器恰好……
// 例题 ②「每个单词出现了几次」—— STL 容器组//// map 就是「下标可以是任意类型的数组」。// cnt[w]++ 时如果这个键还不存在,它会**自动新建、值从 0 开始** —— 不用先判断。// 而 map 的键是**自动有序**的,所以遍历出来天然就是字典序,一句排序都不用写。
#include <bits/stdc++.h>using namespace std;
int main() { map<string, int> cnt; string w; while (cin >> w) cnt[w]++; // 读到文件结尾为止
for (const auto &kv : cnt) // kv.first 是键(单词),kv.second 是值(次数) cout << kv.first << ' ' << kv.second << "\n"; return 0;}点「运行 ▶」看结果
map<string,int> cnt;当成「下标可以是任意类型的数组」来用就行。cnt[w]++时如果w这个键还不存在,map 会自动新建它、值从 0 开始⇒ 不用先判断「在不在」。- ⚠ 反过来这也是个坑:光是读一下
cnt["zz"]也会顺手把这个键建出来。只想查在不在就用cnt.count("zz")。 - map 的键是自动有序的 ⇒
for (const auto &kv : cnt)遍历出来天然就是字典序,一句排序都不用写。kv.first是键、kv.second是值。 - ⚠ 那个
auto前面的const和&:不加&会把每一对键值(含那个 string)复制一遍。10⁶ 个字符的数据上这不是小钱。
⚠ 代价要知道:map 的每次操作是 O(log n),不是 O(1)。10⁵ 次操作无所谓,10⁷ 次的内层循环里它就是瓶颈 —— 那时候才考虑 unordered_map。
用到的写法:map<K, V> auto unordered_map<K, V>
练习(3 道)
答案折叠着,先自己写一版。
练习 ① 去重并排序
给 n 个整数,去掉重复的,然后从小到大输出。
n(1 ≤ n ≤ 10⁵);第二行 n 个整数。输入
7 3 1 4 1 5 9 3
输出
5 1 3 4 5 9
3 1 4 1 5 9 3 去重后剩 5 个:1、3、4、5、9。有一个容器一次就把两件事都干了:重复的自动只留一份,遍历出来自动从小到大。⇒ 这道题可以一句 sort 都不写。
用到的写法:set<int>
练习 ② 括号匹配
给一个只由 (、)、[、]、{、} 组成的字符串,判断括号是不是正确匹配。
「正确匹配」的意思是:每个左括号都能找到同类型的右括号与它配对,而且配对的括号不能交叉。例如 ([]{}) 是对的,([)] 和 (() 都不对。
Yes,否则输出 No。输入
([]{})输出
Yes
([]{}) 里每一对都配上了,而且没有交叉。关键是这句话:最近打开的那个,必须最先关上。⇒ 选型表上「后进先出」那一行。⚠ 别忘了最后检查一下:有没有打开了却一直没关的?
⚠ 取元素的名字:stack 用 top(),queue 用 front() —— 别记混了。
用到的写法:stack<int>
练习 ③ 约瑟夫:报到 m 的出列
n 个人围成一圈,编号 1 ~ n。从 1 号开始报数,报到 m 的人出列;下一个人重新从 1 开始报,如此反复,直到所有人都出列。
请按出列的先后顺序输出所有人的编号。
n、m(1 ≤ n ≤ 10⁴,1 ≤ m ≤ 100)。n 个整数,用空格隔开,行末不留空格。输入
5 2
输出
2 4 1 5 3
「没轮到的人排到队尾去」—— 这句话本身就是选型表上的一行。⚠ 每一轮要让多少个人「过一遍」再让一个人出列?
⚠ pop() 同样不返回值:先 front() 看是谁,再 pop() 让他走。
用到的写法:queue<int>