读这一页之前,先知道读完要能做什么
- 写出来计数三句:
cnt[x]++、遍历按键从小到大、count查在不在 - 不犯那个最常见的错:只想查一下却写了
m[k],把表改了 - 知道
find找不到时返回m.end(),取值前先判
这是什么
一句话:map 是「下标可以不是整数的数组」。map<string, int> cnt; 之后 cnt["cat"]++; —— 键不存在就自动建一个,值从 0 开始。这就是计数的全部秘密。
★ 键是字符串、是 pair 都行,所以编号很大、下标不是整数时它就是标准解法 —— 「开不出那么大的数组」时别忘了它。
★★ map 和第 9 块的 set 一样有序、查找是 O(log n),lower_bound(第 5 块)那套规矩也原样适用。
完整动作表
复杂度写的是这个动作自己的代价,不是整个程序的。「这一栏最容易错的地方」那一列是重点。
| 写法 | 干什么 | 代价 |
|---|---|---|
map<K, V> m; | 开一张表(map<string, int>、map<int, int> …)★ 「K」是键的类型,「V」是值的类型 | O(1) |
m[k]++ | 计数:键不存在就先建一个(值从 0 开始),再 +1 ★★★ 三句里的第一句;⚠ 想「只查不建」别用它(陷阱 ①) | O(log n) |
m[k] = v; | 把键 k 的值写成 v(键不存在就建) ★ 和数组下标赋值一个写法 | O(log n) |
m.count(k) | 键 k 在不在(返回 0 或 1) ★★★ 只想知道「有没有」就用它 —— 它不会建键 | O(log n) |
m.find(k) | 找键 k;找不到返回 m.end()★★ 拿值之前先判 != m.end()(陷阱 ②) | O(log n) |
m.erase(k) | 删掉键 k(没有就什么都不做) | O(log n) |
for (auto &[k, v] : m) | 遍历整张表 ★★★ 出来按键从小到大;老写法是 for (auto &kv : m) + kv.first / kv.second | O(n) |
m.size() / m.empty() | 里面几张记录 / 一条都没有吗 ⚠ 和 int 比大小写 (int)m.size() | O(1) |
⭐ 这一节最容易错的地方(2 处)
这一节排在例题前面是故意的:读到一个坑,再去看正确的例子,记得住。 下面每一条都是「编译得过去、答案可能是错的」那种 —— 它们才是 OI 里真正扣分的地方。
① 只想「查一下」,却写了 `m[k]` —— 它会顺手把键造出来
m[k] 的意思是「给我这个键的值,没有就先造一个」。所以 if (m["ghost"] > 0) 这种判断跑完,表里就多了一个键,m.size() 变了。
★ 于是遍历时冒出一个你没放过的键、计数莫名其妙多 1、「明明没交过」的人被算成交过 —— 不报错,答案错。① 用 count 查 cnt[999](不存在):0,size 还是 3 ← 只查不建
② 用 cnt[999] 查同一个键:值 0,size 变成 4 ← [] 顺手把它造了出来// 只想查在不在
if (m.count(k)) { ... }
// 想拿值,又不想建键
auto it = m.find(k);
if (it != m.end()) use(it->second);count;要拿值 → 先 find 再判 end()。② `find` 找不到返回 `m.end()` —— 解引用它就崩
find 返回的是一个位置,不是值。找不到时它给的是 end():「最后一个之后」那一处,不指向任何元素。
★ 直接 *it 去取值,编译器一个警告都不给 —— 运行时要么 Segmentation fault,要么拿到一个看起来挺合理的数。auto it = m.find(k);
if (it != m.end()) { // ★ 先判
use(it->second);
}end()。本书哪一章真的用到它
⚠ 下表里的文件路径和那串 probe 是逐字验过的(check:docs 第 ⑳ 条)—— 改了那份代码这儿会红,正是要的。点章号直接去看那一页。
| 章 | 书里的哪一份代码 | 用它来干什么 |
|---|---|---|
| 第 45 章 | code/45-estimate/p1786Check.cpp | ★★ 把名字映射成编号 —— 「开不出那么大的数组」时的标准解法 |
| 第 49 章 | code/49-hash/p1381.cpp | ★ 同一题的哈希版:unordered_map 换个名字、用法一样,速查里单有一条 |
例题(2 道)
每道都给全套:题面 → 样例 → 参考代码 → 一步步的讲解。先自己读题想一遍,再看代码。
例题 ① 计数三句,外加「查一下」的两种写法
读 n 个整数,数出每个数出现了几次(按键从小到大输出),然后用两种写法各查一个不存在的键,看看表有没有被改。
★ 这一题的重点在后半段:同一个查询,count 和 [] 的后果不一样。
n。第二行 n 个整数。size 变化,最后再遍历一遍。输入
6 3 1 3 5 1 3
输出
读进 6 个数,map 里有 3 个键: 1 -> 2 3 -> 3 5 -> 1 ① 用 count 查 cnt[999](不存在):0,size 还是 3 ← 只查不建 ② 用 cnt[999] 查同一个键:值 0,size 变成 4 ← [] 顺手把它造了出来 现在再遍历一遍,多出来的那个键就在里面: 1 -> 2 3 -> 3 5 -> 1 999 -> 0
count 之后 size 还是 3,[] 之后变成 4,而且最后那次遍历里真的多了一个 999 -> 0。★ 计数只要一句 cnt[x]++;。⚠ 关键是想清楚 cnt[999] 这句话除了返回值还干了什么。
// STL 专题 · map —— 计数三句,外加「查一下」的两种写法//// ★ 计数的全部秘密就是这一句:// cnt[x]++; // 键不存在就先建一个,值从 0 开始//// ⚠★★ 这一份专门演示陷阱:**只想查一下,却写了 m[k]** ——// `[]` 的意思是「给我这个键的值,**没有就先造一个**」。// 于是查一个不存在的键,size 就多 1。它不报错,只是把表改了。// 想「只查不建」,用 count(或者先 find 再判 end())。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n; cin >> n; map<int, int> cnt; for (int i = 0; i < n; i++) { int x; cin >> x; cnt[x]++; // ★ 三句里的第一句:见到就 +1 }
cout << "读进 " << n << " 个数,map 里有 " << cnt.size() << " 个键:\n"; for (auto &[k, v] : cnt) cout << k << " -> " << v << "\n"; // ★ 按键从小到大
cout << "\n① 用 count 查 cnt[999](不存在):" << cnt.count(999) << ",size 还是 " << cnt.size() << " ← 只查不建\n";
cout << "② 用 cnt[999] 查同一个键:值 " << cnt[999] << ",size 变成 " << cnt.size() << " ← [] 顺手把它造了出来\n";
cout << "\n现在再遍历一遍,多出来的那个键就在里面:\n"; for (auto &[k, v] : cnt) cout << k << " -> " << v << "\n"; return 0;}点「运行 ▶」看结果
- 计数一句:
cnt[x]++;—— 键没见过就先建出来(值从 0 开始),再 +1。 - 遍历:
for (auto &[k, v] : cnt)—— 结构化绑定,出来按键从小到大,所以 1、3、5 这个顺序不是巧合。 - ★★ ①
cnt.count(999):返回 0,size没动 —— 只查不建。 - ★★ ②
cnt[999]:也返回 0,但size从 3 变成 4 ——[]顺手把键造出来了,最后那次遍历里999 -> 0就是证据。这就是陷阱 ①。
★★★ m[k] 是「取值,没有就造一个」,m.count(k) 是「只问有没有」。计数用前者,查询用后者 —— 用错了不报错,只是表被改了。
例题 ② 名字 → 编号:查不到的别造出来
一场比赛 n 个人报名(可能重名),编号按第一次出现的顺序从 0 开始。然后回答 m 个查询:这个名字编号是多少?没报名过的输出 -1。
★ 这是 map 计数之外的另一种标准用法:一张「名字 → 编号」的映射表 —— 第 45 章处理名字的那道题就是这么开的表。
n。接下来 n 行报名名字。然后 m。接下来 m 行查询名字。输入
5 cat dog cat emu dog 3 cat bat emu
输出
0 -1 2
cat 第一次出现排 0,dog 1,emu 2;重名的 cat 不重发编号。查 bat 没见过,输出 -1。★ 报名阶段只给没见过的名字编号,查询用 find —— ⚠ 别用 m[name] 读:没见过的名字会被顺手造出来,编号就乱了。
// STL 专题 · map 例题 —— 名字 → 编号//// ★ map 计数之外的另一种标准用法:名字到编号的映射。// 报名阶段只给没见过的名字编号(m[k] 的「建」正好用上),// 查询阶段一律 find —— m[q] 会把没报过名的造出来。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n; cin >> n; map<string, int> id; for (int i = 0; i < n; i++) { string name; cin >> name; if (!id.count(name)) id[name] = (int)id.size(); // ★ 没见过才登记,编号取当前表大小 }
int m; cin >> m; for (int i = 0; i < m; i++) { string q; cin >> q; auto it = id.find(q); // ★ 读一律 find,不用 m[q] if (it == id.end()) cout << -1 << '\n'; else cout << it->second << '\n'; } return 0;}点「运行 ▶」看结果
- 报名:
if (!id.count(name)) id[name] = (int)id.size();—— 没见过才登记,编号取当前表大小。 - 查询:
auto it = id.find(q); if (it == id.end()) cout << -1; else cout << it->second; - ★ 两个阶段都不要在读的时候用
id[q]—— 见陷阱 ①,建出来的那个键会污染表。 - 遍历出来按名字字典序,不是报名顺序 —— 想按编号序输出就得另存一份。
★★★ 「给东西编号 / 查编号」就是 map 的映射用法,读一律 find 先判 end()。
用到的写法:map<string, int> id; id.count(k) id.find(k) it->second
练习(2 道)
只给题面、样例和一句提示,答案是折叠着的 —— 先自己写一版。
练习 ① 谁出现得最多:计数 → 遍历 → 跟着记最大
给 n 个学号(可能重复),输出每个学号出现的次数,以及交得最多的那个学号(并列时取学号小的)。
★ 这是 map 的标准三段式:计数 → 遍历 → 跟着记一个「当前最多」。
n。第二行 n 个学号。输入
9 5 3 5 8 3 5 9 3 5
输出
每个学号出现的次数(按键从小到大): 3 -> 3 5 -> 4 8 -> 1 9 -> 1 交得最多的是 5,交了 4 次 一共 4 个学号交过,记录一共 9 条
★ 计数一句就够。⚠ 关键在并列时怎么取「学号小的」—— 想想 map 遍历出来的顺序,再决定比较写 > 还是 >=。
★★★ map 遍历出来按键从小到大,所以「并列取小的」只要写严格大于 > —— 先见到的那个就守住了;写 >= 会变成后面那个赢。
练习 ② 模拟一本通讯录:add / ask / del
从空开始处理 m 个操作:add name tel(存电话)、ask name(输出电话,没有就 not found)、del name(删掉)。
★ 重点是三个动作各归各位:存用 m[k] = v,问走 find,删用 erase。
m。之后每行一条操作。输入
7 add cat 123 add dog 456 ask cat ask fish del dog ask dog ask cat
输出
123 not found not found 123
★ ask 的正确写法是 find + 判 != m.end()。⚠ 拿 m[name] 问会顺手把不存在的名字建出来,后面再 ask 它就「找到」了 0,size 也对不上。
★★ m[k] 是「建 + 读」的合体,查询和删除一律走 find / erase。
← 上一块:priority_queue:每次拿走最大的那个 · 回 STL 专题目录 · 下一块:set:去重、查在不在、从小到大 →
⚠ 本页说的是 STL 怎么用。算法本身(Dijkstra、单调栈、状压……)在53 章正课里,而每一章末尾都会告诉你它用到了哪些 STL。