课程 · C++ 速查 · STL 容器(本书只用到这几个)

STL 容器(本书只用到这几个) 例题与练习

把那张「按什么规矩取东西」的选型表用一遍 —— 五个容器,每个只用两三个动作。

2 道例题3 道练习5 份可运行代码回速查看这一组的 8 条 →
怎么用这一页

例题给全套:题面、样例、参考代码、以及一步步的讲解。先自己读题想一遍, 再看代码,最后对着讲解核一遍自己想的对不对。

练习只给题面、样例和一句提示,答案是折叠起来的 —— 请先自己写一版跑通了再展开。写不出来也别直接看答案,先回速查那一组找找。

每份代码都能直接点运行:输入框里已经填好了样例输入, 把它改一改再跑,是这一页最值钱的用法。

先把地基摆一遍

4 段

这五个容器解决的是同一类问题:我要把一堆东西存起来,然后按某种规矩取出来。差别只在「按什么规矩取」—— 谁先进先出、谁后进先出、谁永远给最大的、谁能快速查在不在。

★ 一张表就能选完:要「一层一层地扩散」用 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 道)

每道都给全套:题面 → 样例 → 参考代码 → 一步步的讲解。

例题 ① 合并果子(小规模版)

stlEx1.cpp

n 堆果子,第 i 堆重 aᵢ。每次可以把两堆合并成一堆,代价是这两堆的重量之和;合成的新堆还可以继续参与合并。

一直合到只剩一堆为止。请问总代价最小是多少?

★ 结论先给你(第 37 章会证):每次都挑当前最轻的两堆合,总代价就是最小的。

输入格式第一行一个整数 n1 ≤ n ≤ 10⁵);第二行 n 个正整数 aᵢaᵢ ≤ 10⁴)。
输出格式一行一个整数,表示最小总代价。

输入

3
1 2 9

输出

15
先合 1 和 2(代价 3),再合 3 和 9(代价 12),总代价 15。
先想一想

每一步都要「取出当前最小的那个」,而且合出来的新堆还要放回去继续参与。⇒ 选型表上哪一行是干这个的?

stlEx1.cpp参考代码
// 例题 ①「合并果子(小规模版)」—— 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. 「反复取出当前最小的」⇒ 小根堆 priority_queue<int, vector<int>, greater<int>>。这一长串背下来就行:三个参数分别是「装什么」「底下用什么容器装」「怎么比」,中间那个 vector<int> 是默认值,只是因为要写第三个参数,才不得不把它也补上。
  2. ⚠ 它不是排好序的 —— 它只保证堆顶是最小的那一个。想按顺序全拿出来,只能一个一个 pop
  3. pop() 不返回值:要先 q.top() 拿到那个数,再 q.pop() 把它删掉。(queuestack 也一样,只是取元素的名字分别叫 fronttop。)
  4. 循环条件写 while (q.size() >= 2):剩一堆就不用再合了。
  5. ⚠ 总代价要用 long long:10⁵ 堆、每堆 10⁴,光是总重量就 10⁹,而合并的总代价还要比它大好几倍(每个数会被重复计入若干层)。
记住这一句

这就是第 37 章那道原题。⇒ 而这一页真正的收获是那句选型:看到「反复取当前最优的那个」,就该想到 priority_queue。

用到的写法:priority_queue<int, vector<int>, greater<int>> priority_queue<int>

例题 ② 每个单词出现了几次

stlEx2.cpp

给一段由空格和换行分隔的单词,统计每个单词出现了几次,按字典序输出。

输入格式若干个单词(只含小写字母),用空格或换行分隔,读到文件结尾为止。总长度 ≤ 10⁶。
输出格式若干行,每行一个单词和它出现的次数。

输入

the cat the hat

输出

cat 1
hat 1
the 2
the 出现两次,cathat 各一次 —— 输出按字典序。
先想一想

单词是字符串,开不出「用它当下标」的数组。⇒ 选型表上「按一个键查一个值」那一行。⚠ 还有一件顺手的好事:输出要求按字典序,而那个容器恰好……

stlEx2.cpp参考代码
// 例题 ②「每个单词出现了几次」—— 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. map<string,int> cnt; 当成「下标可以是任意类型的数组」来用就行。
  2. cnt[w]++ 时如果 w 这个键还不存在,map 会自动新建它、值从 0 开始⇒ 不用先判断「在不在」。
  3. ⚠ 反过来这也是个坑:光是一下 cnt["zz"] 也会顺手把这个键建出来。只想查在不在就用 cnt.count("zz")
  4. map 的键是自动有序的 ⇒ for (const auto &kv : cnt) 遍历出来天然就是字典序,一句排序都不用写。kv.first 是键、kv.second 是值。
  5. ⚠ 那个 auto 前面的 const&:不加 & 会把每一对键值(含那个 string)复制一遍。10⁶ 个字符的数据上这不是小钱。
记住这一句

⚠ 代价要知道:map 的每次操作是 O(log n),不是 O(1)。10⁵ 次操作无所谓,10⁷ 次的内层循环里它就是瓶颈 —— 那时候才考虑 unordered_map

用到的写法:map<K, V> auto unordered_map<K, V>

练习(3 道)

答案折叠着,先自己写一版。

练习 ① 去重并排序

stlTry1.cpp

n 个整数,去掉重复的,然后从小到大输出。

输入格式第一行一个整数 n1 ≤ 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 都不写。

stlTry1.cpp参考答案

用到的写法:set<int>

练习 ② 括号匹配

stlTry2.cpp

给一个只由 ()[]{} 组成的字符串,判断括号是不是正确匹配。

「正确匹配」的意思是:每个左括号都能找到同类型的右括号与它配对,而且配对的括号不能交叉。例如 ([]{}) 是对的,([)](() 都不对。

输入格式一行一个字符串(长度 ≤ 10⁵)。
输出格式匹配输出 Yes,否则输出 No

输入

([]{})

输出

Yes
([]{}) 里每一对都配上了,而且没有交叉。
提示

关键是这句话:最近打开的那个,必须最先关上。⇒ 选型表上「后进先出」那一行。⚠ 别忘了最后检查一下:有没有打开了却一直没关的?

stlTry2.cpp参考答案
记住这一句

⚠ 取元素的名字:stacktop()queuefront() —— 别记混了。

用到的写法:stack<int>

练习 ③ 约瑟夫:报到 m 的出列

stlTry3.cpp

n 个人围成一圈,编号 1 ~ n。从 1 号开始报数,报到 m 的人出列;下一个人重新从 1 开始报,如此反复,直到所有人都出列。

请按出列的先后顺序输出所有人的编号。

输入格式一行两个整数 nm1 ≤ n ≤ 10⁴1 ≤ m ≤ 100)。
输出格式一行 n 个整数,用空格隔开,行末不留空格。

输入

5 2

输出

2 4 1 5 3
5 个人报到 2 出列:2 先走,然后是 4、1、5,最后剩 3。
提示

「没轮到的人排到队尾去」—— 这句话本身就是选型表上的一行。⚠ 每一轮要让多少个人「过一遍」再让一个人出列?

stlTry3.cpp参考答案
记住这一句

pop() 同样不返回值:先 front() 看是谁,再 pop() 让他走。

用到的写法:queue<int>