这是一张查的表,不是一门 C++ 课。它假设你已经会变量、if、for、函数和普通数组 —— 从这儿往上,才是本书代码里会冒出来、 而正课不会停下来讲的那些东西。
每一组底下都有一份能直接点「运行」的最小示例:与其读解释,不如把数字改一改看它变成什么。
一张表再厚,也只是「看过」。所以每一组另开了一页, 放 20 道例题(题面 + 样例 + 完整参考代码 + 一步步的讲解)和30 道练习(只给题面和一句提示,答案折叠着,先自己写)。 十组一共 50 道,每道都能直接点运行。
零基础的话,别从这张表读起 —— 挑一组,点进它的练习页,从第一道例题开始做。
位运算 · 整数类型与溢出 · 数组与 vector · 字符串 string · 排序与比较器 · STL 容器(本书只用到这几个) · 读入与输出 · 函数、引用与递归 · 零碎但天天用的那几样 · 报错了怎么读
课程的顺序是按算法排的,不是按 C++ 语法排的,于是有些写法会出现得很早、 讲得很晚。量过一遍:全书有 15 章的正文代码在第 46 章(位运算)之前就用了真位运算 —— 最早是第 2 章的 1LL << n、 第 3 章的 mask >> i & 1,中间隔着四十多章。vector 更极端:从第 2 章起几乎每份代码都有它,而没有任何一章教过它。 这一页就是补这个缺口的。
- 位运算 10 条 · 带可运行示例 · 例题与练习 5 道 →
- 整数类型与溢出 6 条 · 带可运行示例 · 例题与练习 5 道 →
- 数组与 vector 8 条 · 带可运行示例 · 例题与练习 5 道 →
- 字符串 string 9 条 · 带可运行示例 · 例题与练习 5 道 →
- 排序与比较器 8 条 · 带可运行示例 · 例题与练习 5 道 →
- STL 容器(本书只用到这几个) 8 条 · 带可运行示例 · 例题与练习 5 道 →
- 读入与输出 5 条 · 带可运行示例 · 例题与练习 5 道 →
- 函数、引用与递归 6 条 · 带可运行示例 · 例题与练习 5 道 →
- 零碎但天天用的那几样 8 条 · 带可运行示例 · 例题与练习 5 道 →
- 报错了怎么读 7 条 · 例题与练习 5 道 →
1. 位运算
缺口最大的一组:全书 15 章的正文代码在第 46 章之前就用了它,最早是第 2 章。
地基只有一句话:一个整数就是一排开关。 我们平时把 13 读成「十三」,而计算机里它是 1101 —— 从右往左数,第 0 号开关开、第 1 号关、第 2 号开、第 3 号开。位运算就是拨这排开关的几个动作,它们不是数学运算,别拿加减乘除的直觉去套。
⚠ 两个编号要一次记牢,后面所有条目都按这个约定:最右边那一位是「第 0 位」(不是第 1 位),而且越往左位数越大。第 i 位代表的数值是 2ⁱ —— 所以 1101 = 8 + 4 + 0 + 1 = 13。
★ 而这排开关彼此没有一根线连着:对第 3 位做的事不会影响第 2 位。这就是为什么「一个集合」可以用「一个整数」表示(第 28 章状压 DP 的全部立论),也是为什么第 46 章那道题能用异或一路消下去。
★★ 优先级是这一组唯一一个会咬人的语法问题:位运算的优先级比比较运算还低。所以凡是位运算和 == / < 写在一起,一律加括号 —— 这条没有例外,看到就加。
x >> i & 1取出 x 的第 i 位(从右往左数,最右边是第 0 位),结果只会是 0 或 1。怎么读分两步读。x >> i是「整排开关右移 i 格」,把你要的那一位挪到最右边;再& 1是「只留最右边那一位,其余全抹掉」。拿 x = 13(1101)、i = 2 走一遍:13 >> 2=11= 3,3 & 1= 1 ⇒ 第 2 位是 1。✓什么时候用凡是「一个整数代表一组选/不选」的地方都要用它。第 3 章那份二进制枚举里,mask >> i & 1问的就是「第 i 个数选了没有」;第 28 章问的是「第 i 个城市去过没有」。讲透它的是 第 46 章第 3 章用到 →x & 1判奇偶 —— 等于 1 就是奇数。它其实就是「取第 0 位」。怎么读为什么成立:二进制里除了最低位,每一位代表的都是 2 的正整数次方,全是偶数。所以整个数的奇偶只由最低位决定 —— 而& 1恰好只留最低位。什么时候用和x % 2完全等价,写哪个都对。⚠ 但对负数不一样:-3 % 2是 −1,而-3 & 1是 1 —— 判奇偶时后者反而更省心。讲透它的是 第 46 章第 46 章用到 →1 << n2 的 n 次方。1后面跟 n 个 0,所以左移一位就是乘 2。怎么读1是……0001,左移 3 位变成1000,也就是 8 = 2³。十进制里「后面添个 0 就是乘 10」,二进制里「后面添个 0 就是乘 2」,是同一件事。什么时候用两个场合:算「一共有多少种选法」(n 个东西各选/不选 ⇒1 << n种),以及造一个「只有第 i 位是 1」的数去拨那一位。⚠ ⚠第 3 章用到 →n到 31 就已经溢出了(1 << 31是负数),到 32 更是直接未定义。要大的写1LL << n。1LL << n同上,但用 long long 算 —— n 超过 30 就得用这一种。怎么读1 << n里的1是int,整句就按 int 算,而 int 只装得下 2³¹−1。1LL是「long long 类型的 1」,整句就升级成 long long,能到 2⁶³−1。什么时候用第 2 章汉诺塔的总步数第 2 章用到 →2ⁿ − 1,n 可以到 60 ⇒ 非1LL不可。判据很简单:n 有没有可能超过 30,有就写 LL。x | (1 << i)把第 i 位设成 1(其余位不动)。集合里「加入第 i 个元素」。怎么读|(或)的规则是「两边只要有一个是 1,结果就是 1」。1 << i造出一个「只有第 i 位是 1」的数,和 x 一或:第 i 位被顶成 1,其余位是「x 的那一位 | 0」= 原样。例:13 | (1 << 1)=1101 | 0010=1111= 15。什么时候用第 28 章往状态里「加一个已访问的城市」;也常写成第 14 章用到 →x |= 1 << i。x & ~(1 << i)把第 i 位设成 0。集合里「去掉第 i 个元素」。怎么读~是「每一位都翻过来」。1 << 2是0100,~(1 << 2)就是……1011—— 一个「只有第 2 位是 0,其余全是 1」的掩码。再&(与,两边都是 1 才是 1):第 2 位被按成 0,其余位是「x 的那一位 & 1」= 原样。例:13 & ~(1 << 2)=1101 & 1011=1001= 9。什么时候用第 28 章「从状态里去掉当前城市,回头看上一步是从哪来的」。第 28 章用到 →x ^ (1 << i)把第 i 位翻过来:0 变 1、1 变 0。怎么读^(异或)的规则是「两边不一样才是 1」。和 1 异或必翻,和 0 异或不变 —— 所以只有第 i 位被翻,其余原样。⚠ 本书正文一次都没用到它 —— 列在这儿是为了让那三个动作凑齐一套。本书暂未用到x & -xlowbit:只留下最低位的那个 1(连同它后面的 0)。怎么读-x在计算机里是「把 x 每一位翻过来再加 1」(这叫补码)。拿 x = 12 =1100:翻过来是…0011,加 1 变成…0100。和原来的1100一与,只剩100= 4 —— 正是最低位那个 1。⚠ 这一段是本条唯一需要「相信」的地方,第 46 章第 9 步会把补码讲明白。什么时候用第 38 章树状数组每一行都在用:i += i & -i往上跳父节点,i -= i & -i抹掉最低位那个 1 往前挪。也可以用来「取出集合里最小的那个元素」。讲透它的是 第 46 章第 4 章用到 →__builtin_popcount(x)数一数 x 的二进制里有几个 1。GCC 内置,比赛能用。怎么读__builtin_popcount(13)= 3,因为1101里有三个 1。它是一条 CPU 指令,比自己写循环数快得多。什么时候用状压 DP 里问「这个状态已经访问了几个城市」——集合大小就是 1 的个数,不用另外存。⚠ long long 要用第 28 章用到 →__builtin_popcountll,写错了只会数低 32 位。(x & 1) == 0⚠ 括号不能省:位运算的优先级比比较运算还低。怎么读写x & 1 == 0,编译器先算1 == 0得到 0,整句变成x & 0—— 恒等于 0,也就是这个判断永远是假。它不报错、不警告,只是安静地一直答「不是」。什么时候用记一条无脑规则就行:位运算只要和==/</>出现在同一个表达式里,就加括号。⚠ 同一类还有一个:本书暂未用到a & b == c也会先算b == c。而<<的优先级比+还低 ——1 << n + 1是1 << (n+1),不是(1 << n) + 1。
// 位运算速查 —— 七个动作,跑一遍就能看见//// 为什么这一份排在第一个:本书**第 2 章**就用了 `1LL << n`、// **第 3 章**的 `bitmask.cpp` 用了 `mask >> i & 1`,// 而真正把位运算讲透的是**第 46 章** —— 中间隔着四十多章。// 这一份不讲道理,只把「那几个符号在干什么」摆出来给你看。//// 一句话地基:**一个整数就是一排开关。**// 13 写成二进制是 1101,也就是第 0、2、3 号开关是开的(从右往左数,从 0 开始)。// 下面七个动作,每一个都只是在拨这排开关。
#include <bits/stdc++.h>using namespace std;
// 把 x 的低 8 位打成 0/1 串,方便肉眼对照string bits8(int x) { string s; for (int i = 7; i >= 0; i--) s += char('0' + (x >> i & 1)); return s;}
int main() { int x = 13; // 13 = 1101 cout << "x = " << x << " 二进制 " << bits8(x) << "\n\n";
// ① 取出第 i 位:先右移 i 位把它挪到最右边,再和 1 做「与」 cout << "① 取第 i 位 x >> i & 1\n"; for (int i = 0; i < 4; i++) cout << " i=" << i << " -> " << (x >> i & 1) << "\n";
// ② 判奇偶:就是第 0 位 cout << "\n② 判奇偶 x & 1 -> " << (x & 1) << " (1 是奇数,0 是偶数)\n";
// ③ 2 的幂:1 左移 n 位 cout << "\n③ 2 的 n 次 1 << n\n"; for (int n = 0; n <= 4; n++) cout << " n=" << n << " -> " << (1 << n) << "\n"; // 注意:1 是 int,只有 31 位可用。要算 2^40 必须写 1LL << 40。 cout << " 1 << 40 是错的(int 装不下),要写 1LL << 40 = " << (1LL << 40) << "\n";
// ④ 把第 i 位设成 1:或上一个「只有第 i 位是 1」的数 cout << "\n④ 置 1 x | (1 << i) i=1 -> " << setw(2) << (x | (1 << 1)) << " " << bits8(x | (1 << 1)) << "\n";
// ⑤ 把第 i 位设成 0:与上一个「只有第 i 位是 0」的数 cout << "⑤ 清 0 x & ~(1 << i) i=2 -> " << setw(2) << (x & ~(1 << 2)) << " " << bits8(x & ~(1 << 2)) << "\n";
// ⑥ 把第 i 位翻过来 cout << "⑥ 翻转 x ^ (1 << i) i=0 -> " << setw(2) << (x ^ (1 << 0)) << " " << bits8(x ^ (1 << 0)) << "\n";
// ⑦ lowbit:最低位的那个 1(连同它后面的 0)。第 38 章树状数组每一行都在用它。 cout << "\n⑦ lowbit x & -x\n"; for (int v : {6, 8, 7, 12}) cout << " " << setw(2) << v << " (" << bits8(v) << ") -> " << (v & -v) << "\n";
// 附:数一个数里有几个 1(GCC 内置,比赛能用) cout << "\n附 数 1 的个数 __builtin_popcount(" << x << ") = " << __builtin_popcount(x) << "\n";
// 最后一个坑:位运算的优先级**比比较运算还低**。 // 想问「y 的第 0 位是不是 0」(也就是 y 是不是偶数),写成 y & 1 == 0 会先算 // 1 == 0 得到 0,整句变成 y & 0 —— 恒等于 0,也就是**永远答「不是」**。括号不能省。 int y = 12; // 12 是偶数,正确答案应该是 1 cout << "\n坑 y = " << y << "\n"; cout << " y & 1 == 0 编译器读成 y & (1 == 0) = " << (y & (1 == 0)) << " <- 错\n"; cout << " (y & 1) == 0 = " << ((y & 1) == 0) << " <- 对\n"; return 0;}点「运行 ▶」看结果
读完这一组 ⇒ 做 2 道例题 + 3 道练习 把「一个整数就是一排开关」这句话,变成手上真的会写的四个动作:取一位、加一位、去一位、数几个 1。
2. 整数类型与溢出
本书最常咬人的一类 bug,而且它不报错:编译过、样例过、肉眼看也正常,就是答案错。第 11、19、23、35、43 章各栽过一次。
每个整数类型都有一个固定的抽屉大小。装不下的时候,C++ 不会报错、不会提醒,它只是把超出去的部分扔掉,留下一个看起来很正常的错数字 —— 这叫溢出。
★ 记两个数量级就够:int 大约 2.1 × 10⁹(二十一亿),long long 大约 9.2 × 10¹⁸。竞赛题面里常写 1 ≤ aᵢ ≤ 10⁹,单个数 int 装得下 —— 可两个这样的数一乘就是 10¹⁸,当场爆掉。
⇒ ★★ 所以动笔之前有一个三十秒的动作:拿题面的上限乘一遍,看最大的那个中间值有多大。不是看输入多大,也不是看答案多大,是看算的过程中出现过的最大的那个数。本书里栽的每一次都栽在这儿。
★ 拿不准就一律 long long,代价几乎为零。只有「开一个几千万个元素的大数组」时,才值得回头精打细算 —— 那时内存翻一倍是真的会 MLE。
int上限约 2.1 × 10⁹(二十一亿)。题目里的数一旦可能超过它,就得换。怎么读精确值是 2 147 483 647(= 2³¹ − 1),最小是 −2 147 483 648。记不住就用INT_MAX/INT_MIN,它们在<bits/stdc++.h>里现成。什么时候用循环变量、数组下标、题面上限在 10⁹ 以内的单个数值 —— 这些用 int 就够。第 1 章用到 →long long上限约 9.2 × 10¹⁸。本书的默认选择 —— 拿不准就用它,代价几乎为零。怎么读精确值是 9 223 372 036 854 775 807(= 2⁶³ − 1)。写字面常量要带后缀LL(如1LL),用printf打它要用%lld。什么时候用求和、求乘积、计数类的答案,一律先用它。第 19 章那道题的「总等待时间」顶格是 10¹⁰ 级别 —— 单个数都在 int 里,加起来就不在了。讲透它的是 第 11 章第 1 章用到 →(long long)a * b两个 int 相乘之前,先把一个转成 long long,整句才会按 long long 算。怎么读C++ 算一个表达式时,先按两边的类型算完,再赋值。a * b两边都是 int ⇒ 这次乘法就是 int 乘法,10⁵ × 10⁵ = 10¹⁰ 当场溢出,之后再赋给 long long 也救不回来 —— 错的数已经算出来了。前面加一个(long long),整句就升级成 long long 乘法。什么时候用两个下标相乘、两个坐标差相乘(dx*dx + dy*dy)、计数乘计数 —— 都要先过一遍这个动作。⚠ 写第 5 章用到 →long long c = a * b;没用 —— 等号右边那次乘法仍然是 int 乘法,溢出已经发生了。0x3f3f3f3f当「无穷大」用的那个数(约 1.06 × 10⁹)。怎么读为什么不直接用INT_MAX:最短路里要算dist[u] + w,而INT_MAX + 任何正数当场溢出,绕回成一个负数 —— 于是「走不到的点」反而成了最短的。0x3f3f3f3f的好处是它自己加自己还不溢出(约 21.2 亿 < 21.47 亿),而且它的每个字节都是0x3f⇒ 可以memset(d, 0x3f, sizeof d)一次填满整个数组。什么时候用第 32、33 章最短路,第 34 章最小生成树,任何「先设成无穷大再不断取 min」的地方。⚠ 选它是因为它自己加自己还不溢出 —— 最短路里第 29 章用到 →dist[u] + w要先加再比。-7 / 2整数除法向零取整,得 −3(不是 −4);-7 % 2得 −1,取模的符号跟着被除数走。怎么读「向零取整」就是把小数部分直接砍掉:−3.5 砍成 −3,而不是往下取到 −4。砍完之后等式仍然成立:(-7)/2*2 + (-7)%2= −6 + (−1) = −7。⚠ ⚠ 取模题里最容易出事:答案要求非负时本书暂未用到a % p可能是负的,要写成(a % p + p) % p才保险 —— 第 42、43 章都这么写。v.size()⚠ 它是无符号的。空数组上v.size() - 1不是 −1,是一个天文数字。怎么读「无符号」就是这个类型没有负数,于是 0 − 1 不会变成 −1,而是绕回它能表示的最大值(18 446 744 073 709 551 615)。写for (int i = 0; i < v.size() - 1; i++),空数组时循环条件成了「0 < 天文数字」⇒ 循环真的跑起来了,然后越界。什么时候用写「倒数第二个元素」、写「相邻两两比较」的循环时必踩。⚠ 两个改法:本书暂未用到(int)v.size() - 1(先转成有符号),或者换成i + 1 < v.size()(不做减法)。
// 整数类型速查 —— 它装得下多大,以及什么时候会悄悄装不下//// 这是本书最常咬人的一类 bug,而且它**不报错**:算出来的数就是错的,// 编译过、样例过、肉眼看也很正常。第 11、19、23、35、43 章都各栽过一次。
#include <bits/stdc++.h>using namespace std;
int main() { // ① 两个类型的上限,背下来这两个数量级就够 cout << "int 上限 " << INT_MAX << " (约 2.1 x 10^9,二十一亿)\n"; cout << "long long 上限 " << LLONG_MAX << " (约 9.2 x 10^18)\n\n";
// ② 最常见的溢出现场:两个 int 相乘。 // a 和 b 各自都装得下,可它们的**积**装不下 —— 而乘法是按 int 算完才赋值的。 int a = 100000, b = 100000; long long right = (long long)a * b; // 先把一个转成 long long,整句就按 long long 算 cout << "② a = " << a << ", b = " << b << "\n"; cout << " 真正的 a*b = " << right << ",而 int 上限只有 " << INT_MAX << "\n"; cout << " => 写 int c = a * b; 就装不下了;要写 long long c = (long long)a * b;\n"; cout << " ⚠ 只把结果声明成 long long 没用:long long c = a * b; 里那次乘法「仍然是 int 乘法」\n\n";
// ③ 移位同理:1 是 int,1 << 40 装不下 cout << "③ 1LL << 40 = " << (1LL << 40) << " (写成 1 << 40 就错了)\n\n";
// ④ 除法是向零取整,不是向下取整 —— 负数上两者不一样 cout << "④ 7 / 2 = " << 7 / 2 << " 7 % 2 = " << 7 % 2 << "\n"; cout << " -7 / 2 = " << -7 / 2 << " -7 % 2 = " << -7 % 2 << " (取模的符号跟着被除数走)\n\n";
// ⑤ size() 是**无符号**的,这一条每年都有人栽 vector<int> v; // 空的 cout << "⑤ 空 vector 的 v.size() = " << v.size() << "\n"; cout << " 而 v.size() - 1 = " << v.size() - 1 << " <- 不是 -1,是一个天文数字\n"; cout << " => 写循环边界要么用 (int)v.size() - 1,要么换成 i + 1 < v.size()\n"; return 0;}点「运行 ▶」看结果
读完这一组 ⇒ 做 2 道例题 + 3 道练习 把「动笔之前先拿题面的上限乘一遍」变成一个真的会做的动作 —— 这一组的每道题都在那次乘法上。
3. 数组与 vector
从第 2 章起几乎每份代码都有 vector,而没有任何一章教过它。
你已经会写 int a[100]; 了。vector 就是同一个东西,只多给你三件事:长度可以是运行时才知道的变量、可以随时往后加、它自己知道自己有多长。除此之外用法一模一样 —— 照样 a[0]、a[i],下标照样从 0 开始。
★ 为什么本书几乎全用它而不用裸数组:一是题目的 n 是读进来的,int a[n]; 在标准 C++ 里其实不合法(GCC 容忍而已);二是 vector<int> a(n); 保证全是 0,而函数里的 int a[100]; 里面是垃圾值。
⚠ 但有一个场合反过来用裸数组:几百万个元素的大数组要开成全局的。写在 main 里的数组放在栈上(一般只有 8 MB),开大了直接段错误;全局数组不占栈,而且自动清零。竞赛代码里 const int N = 100005; int a[N]; 就是这个习惯。
★ 还有一条贯穿全书的:数组越界 C++ 不会报错。 它只是安静地读写了别人的内存,于是程序可能在一个毫不相干的地方崩掉,或者答案莫名其妙 —— 查这类问题的第一招是临时把 a[i] 换成 a.at(i)。
vector<int> a(n)开一个长度为 n 的数组,全部初始化成 0。n 可以是运行时才知道的变量。怎么读读成「一个装 int 的 vector,名字叫 a,长度 n」。尖括号里写的是装什么,括号里写的是装多少个。换成vector<long long> a(n)就是装 long long。什么时候用读完第 2 章用到 →n之后开数组,这是本书每道题的第一句。vector<int> a(n, 7)同上,但全部填 7。怎么读第二个参数是「每一格的初值」。不写就是 0。什么时候用最短路里本书暂未用到vector<int> dist(n, 0x3f3f3f3f);—— 一句话把整张表设成无穷大。a.size() / a.push_back(x) / a.back()有多长 / 往后加一个 / 最后一个元素。怎么读push_back把 x 接在末尾,长度自动 +1 —— 不用你先想好开多大。a.back()就是a[a.size()-1],a.pop_back()则是删掉最后一个。什么时候用搜索时往路径里记一步(第 3 章用到 →path.push_back(v)),回溯时再pop_back()撤销 —— 第 3、4 章那套「进入 → 递归 → 撤销」就是这两句。vector<vector<int>> f(R, vector<int>(C, 0))R 行 C 列的二维数组,全 0。怎么读从里往外读:vector<int>(C, 0)是「一行,C 个 0」;外面那层是「R 个这样的行」。取元素照样f[i][j]。什么时候用DP 的表格(第 21 章起每一章都是它)、网格图、邻接矩阵。⚠ ⚠ 行列别写反:第 6 章用到 →f(R, vector<int>(C))的第一个数是行数。写反了不一定崩,只是答案错 —— 而且小数据上常常正好是方阵,看不出来。for (int x : a)范围 for:把 a 里每个元素依次取出来叫 x。怎么读它等价于for (int i = 0; i < a.size(); i++) { int x = a[i]; … },只是不用管下标。⚠ 注意x是一份复制品。什么时候用只读地遍历一遍(求和、打印)时最省事;要用到下标就老老实实写for (int i…)。⚠ 要改元素得写本书暂未用到for (int &x : a)—— 少一个 & 就只是改了一份复制品。全局数组 vs 局部数组全局数组自动全是 0,函数里的int t[5];不初始化就是垃圾值。怎么读写在所有函数外面的叫全局,程序启动时由系统一次性清零;写在函数里的在栈上,那块内存上次谁用过就留着谁的数据。什么时候用⚠ 大数组(几十万以上)一律全局,否则爆栈;⚠ 而多组数据的题里,全局数组每组之间要自己清,它只在启动时清过一次。⚠ 「第一次跑对、第二次跑错」十有八九是这个。本书暂未用到vector<int> a(n);则保证全 0。memset(a, 0, sizeof a)按字节填。只认三个值:0、−1、0x3f。怎么读它填的单位是字节不是元素。一个 int 有 4 个字节,memset(a, 1, …)是把每个字节都填成 1 ⇒ 每个 int 变成0x01010101= 16843009,而不是 1。只有 0(四个 0 还是 0)、−1(四个0xff还是 −1)、0x3f(拼出0x3f3f3f3f)这三个刚好合用。什么时候用一次性把整张 dist 表设成无穷大:memset(d, 0x3f, sizeof d);。要填别的值就用fill(a, a + n, v)或vector的构造参数。⚠第 29 章用到 →memset(a, 1, ...)得到的不是 1,是 16843009 —— 它填的是每个字节。a.at(i)带越界检查的下标。a[i]越界不会报错,只会安静地读写别人的内存。怎么读at每次都先检查下标在不在范围内,越界就抛异常、程序当场停在那一行;[]不检查,所以更快,但错了没人告诉你。什么时候用⚠ 只在排查 bug 时临时换成at,查完换回去 —— 它在内层循环里是真的慢。⚠ 查这类问题时临时换成本书暂未用到at,越界会当场抛异常;查完再换回去(at更慢)。
// 数组与 vector 速查 —— 本书从第 3 章起几乎每份代码都有它//// vector 就是「长度可以在运行时决定的数组」。// 你已经会写 int a[100]; 了,那么 vector 只多给你三件事:// 长度可以是变量、可以随时往后加、以及它自己知道自己有多长。
#include <bits/stdc++.h>using namespace std;
int g[5]; // 全局数组:自动全是 0
int main() { int n = 5;
// ① 开一个长度为 n 的数组,全部初始化成 0 vector<int> a(n); // a[0..4],全是 0 vector<int> b(n, 7); // 全是 7 cout << "① a = "; for (int x : a) cout << x << ' '; cout << "\n"; cout << " b = "; for (int x : b) cout << x << ' '; cout << "\n\n"; // `for (int x : a)` 叫范围 for,意思是「把 a 里每个元素依次取出来叫 x」。 // 要**改**元素得写 for (int &x : a)(加一个 &,见 funcref.cpp)。
// ② 下标、长度、往后加 a[0] = 3; a[4] = 9; a.push_back(100); // 长度变成 6 cout << "② a.size() = " << a.size() << " a[0] = " << a[0] << " 最后一个 a.back() = " << a.back() << "\n\n";
// ③ 二维:一个「元素是 vector 的 vector」 int R = 3, C = 4; vector<vector<int>> f(R, vector<int>(C, 0)); // R 行 C 列,全 0 f[1][2] = 5; cout << "③ 二维 f[1][2] = " << f[1][2] << ",共 " << f.size() << " 行 " << f[0].size() << " 列\n\n";
// ④ 全局数组自动清零,局部的**不会** —— 这是最常见的「第一次跑对、第二次跑错」 cout << "④ 全局 g[3] = " << g[3] << " (全局数组一定是 0)\n"; cout << " 而函数里写 int t[5]; 不初始化的话,里面是垃圾值\n"; cout << " vector<int> a(n); 则保证全 0 —— 这也是本书偏爱它的原因之一\n\n";
// ⑤ memset 只认三个值:0、-1、0x3f。它是按**字节**填的,不是按元素。 int m[4]; memset(m, 0, sizeof(m)); cout << "⑤ memset(m, 0, sizeof m) -> " << m[0] << "\n"; memset(m, 0x3f, sizeof(m)); cout << " memset(m, 0x3f, ...) -> " << m[0] << " (一个「很大但加一倍也不溢出」的数,常当无穷大用)\n"; cout << " ⚠ memset(m, 1, ...) 得到的不是 1,是 " << (memset(m, 1, sizeof(m)), m[0]) << "\n\n";
// ⑥ 越界不会报错 —— 它只是安静地读写了别人的内存 cout << "⑥ a 只有 " << a.size() << " 个元素,写 a[100] = 1 不会有任何提示,\n"; cout << " 但程序可能在很远的地方崩掉,或者答案莫名其妙。\n"; cout << " 查这类问题可以临时把 a[i] 换成 a.at(i):越界会当场抛异常。\n"; return 0;}点「运行 ▶」看结果
读完这一组 ⇒ 做 2 道例题 + 3 道练习 把「读 n → 开数组 → 读进去 → 扫一遍」这套本书每道题都要写一遍的开头,练到不用想。
4. 字符串 string
和 vector 一样的处境:第 1 章的代码里就有它,而第 47 章才正式讲字符串。
string 就是「一排 char」,而且它自己知道自己有多长。你可以像数组一样 s[0]、s[i] 地取,也可以整个拿来比大小、拼接、当函数的返回值 —— 这比 C 语言那种 char s[100] 省心得多。竞赛里除非卡常,一律用它。
★ 最该先记牢的一条是「字符也是整数」:char 在 C++ 里本来就是一个小整数,'a' 是 97、'0' 是 48。所以 c - '0' 把字符 '7' 变成数字 7,c - 'a' 把字母变成 0~25 的编号 —— 后者是开计数数组 cnt[26] 的前提,第 47 章起几乎每道字符串题都要用。
⚠ 而字符串比大小是字典序,不是数值:"100" < "99" 是真的(因为 '1' < '9')。第 44 章高精度里那个「比大小要先比位数」的坑,根子就在这儿。
string s; cin >> s;读一个不带空格的串。怎么读>>读到空格或换行就停 —— 所以它读的是「一个词」,不是「一行」。要读一整行用getline。什么时候用读单词、读一串没有空格的数字字符、读一行地图。第 3 章用到 →s.size() / s[i] / s.back()长度 / 第 i 个字符(从 0 数)/ 最后一个字符。怎么读和数组一模一样。遍历可以写for (int i = 0; i < (int)s.size(); i++),也可以写for (char c : s)。⚠ ⚠第 1 章用到 →s.size()是无符号的 —— 和int i比较时编译器会警告,空串上s.size() - 1更是一个天文数字。习惯写(int)s.size()。s + t s += c拼接。+造一个新串,+=接在自己后面。怎么读⚠+=快得多:s = s + c会把整个 s 复制一遍再加一个字符,在循环里就是 O(n²)。拼长串一律用+=。什么时候用把一个数字逐位拼成字符串、把答案一行一行攒起来最后一次输出。本书暂未用到s.substr(i, len)截一段:从下标 i 开始、取 len 个字符。怎么读只写一个参数s.substr(i)就是「从 i 到结尾」。例:"hello world".substr(6, 5)="world"。什么时候用第 18 章字串变换、第 49 章哈希里取一段子串。⚠ ⚠ 第二个参数是长度,不是「结束下标」—— 这是最常见的 off-by-one 来源。第 18 章用到 →s == t s < t==比内容(不是地址),<是字典序。怎么读字典序就是「逐位比,第一个不同的位说了算;都相同则短的在前」。所以"abc" < "abd",而"ab" < "abc"。⚠ ⚠本书暂未用到"100" < "99"是真的 —— 字符串比大小和数字比大小是两回事。第 44 章高精度比大小必须先比位数,原因就在这儿。c - '0' c - 'a'字符 → 整数。前者把'7'变成 7,后者把字母变成 0~25。怎么读char本来就是一个小整数(ASCII 码):'0'是 48、'7'是 55,两者一减正好是 7。字母同理,'a'是 97。反过来(char)('a' + 3)就是'd'。什么时候用⚠ 开计数数组的前提:第 6 章用到 →int cnt[26]; cnt[c - 'a']++;。第 47 章起到处都是。s.find(t)找子串,返回第一次出现的下标;找不到返回string::npos。怎么读string::npos是一个很大的数(不是 −1)。⚠ 判「找到没有」要写!= string::npos,写成> 0会漏掉「在开头找到」这一种。什么时候用第 47 章统计单词出现次数。⚠ 它是朴素查找,最坏 O(nm) —— 大数据要上 KMP(第 48 章)。第 7 章用到 →to_string(x) stoi(s)数字 ↔ 字符串互转。怎么读to_string(255)得到"255";stoi("255")得到 255。大数用stoll(转 long long)。什么时候用第 15 章把一个棋盘状态压成字符串当 map 的键。第 2 章用到 →reverse(s.begin(), s.end())原地翻转整个串。怎么读和 vector 用的是同一个reverse—— 它对任何「一段区间」都管用。什么时候用第 44 章高精度:数要倒着存(个位在下标 0),读进来之后第一件事就是翻转。第 10 章用到 →
// 字符串 string 速查 —— 本书第 1 章就在用它,而第 47 章才正式讲字符串//// 一句话地基:**string 就是「一排 char」,而且它自己知道自己有多长。**// 你可以像数组一样 s[0]、s[1] 地取,也可以整个拿来比大小、拼接、当返回值。// 这比 C 语言的 char 数组省心得多,竞赛里除非卡常,否则一律用 string。
#include <bits/stdc++.h>using namespace std;
int main() { string s = "hello";
// ① 长度和取字符 —— 和数组一模一样,下标从 0 开始 cout << "① s = \"" << s << "\"\n"; cout << " s.size() = " << s.size() << " s[0] = " << s[0] << " s[4] = " << s[4] << " 最后一个 s.back() = " << s.back() << "\n"; cout << " 逐个字符:"; for (int i = 0; i < (int)s.size(); i++) cout << s[i] << ' '; cout << "\n (也可以写 for (char c : s))\n\n";
// ② 拼接:+ 和 += 都行,char 也能直接加上去 string t = s + " world"; t += '!'; cout << "② s + \" world\" 再 += '!' -> \"" << t << "\"\n\n";
// ③ 截一段:substr(起点, 长度)。只写起点就是「从这儿到结尾」 cout << "③ t.substr(6, 5) = \"" << t.substr(6, 5) << "\" (从下标 6 起,取 5 个)\n"; cout << " t.substr(6) = \"" << t.substr(6) << "\" (从下标 6 到结尾)\n\n";
// ④ 比大小:== 比内容(不是地址),< 是**字典序** cout << "④ (\"abc\" == \"abc\") = " << (string("abc") == string("abc")) << "\n"; cout << " (\"abc\" < \"abd\") = " << (string("abc") < string("abd")) << " 字典序,逐位比\n"; cout << " ⚠ 而 (\"100\" < \"99\") = " << (string("100") < string("99")) << " —— 字符串比大小和数字比大小是两回事\n\n";
// ⑤ 找子串:找到返回下标,找不到返回 string::npos(一个很大的数) size_t pos = t.find("world"); cout << "⑤ t.find(\"world\") = " << pos << "\n"; cout << " t.find(\"xyz\") 找不到,等于 string::npos 吗 -> " << (t.find("xyz") == string::npos) << "\n"; cout << " ⚠ 判「找到没有」要写 != string::npos,别写 > 0(下标 0 也是找到了)\n\n";
// ⑥ 字符也是整数 —— 这是竞赛里最常用的一个转换 char c = '7'; cout << "⑥ '7' - '0' = " << c - '0' << " (字符数字 -> 整数)\n"; char d = 'e'; cout << " 'e' - 'a' = " << d - 'a' << " (字母 -> 0~25 的编号,开计数数组要用)\n"; cout << " (char)('a' + 3) = " << (char)('a' + 3) << " (编号 -> 字母)\n\n";
// ⑦ 和数字互转 cout << "⑦ to_string(255) = \"" << to_string(255) << "\"\n"; cout << " stoi(\"255\") + 1 = " << stoi("255") + 1 << "\n\n";
// ⑧ 整串翻转 —— 和 vector 用的是同一个 reverse string r = s; reverse(r.begin(), r.end()); cout << "⑧ reverse 之后 \"" << s << "\" -> \"" << r << "\"\n"; return 0;}点「运行 ▶」看结果
读完这一组 ⇒ 做 2 道例题 + 3 道练习 把「字符也是整数」这半句话用熟 —— 它是开计数数组的前提,也是整组里唯一真正要转的那个弯。
5. 排序与比较器
第 10 章才正式教排序,可从第 2 章起就在用 sort。
要记的只有一件事:sort 不知道「谁该排前面」,那是你告诉它的。不告诉它,它就按 < 排(数字从小到大、字符串按字典序)。
★ 它要的两个参数是「从哪儿开始」和「到哪儿结束」,而结束那个是「最后一个的下一格」。所以整个 vector 是 a.begin(), a.end(),裸数组是 arr, arr + n;只排前 5 个就是 a.begin(), a.begin() + 5。这个「左闭右开」的习惯贯穿整个 STL。
⚠ 自己写比较规则时,函数返回的是「a 该排在 b 前面吗」,不是「谁更大」。想降序就写 return a > b; —— 读成「a 比 b 大时,a 排前面」。
★★ 而这里有一条会让程序直接崩溃的规矩:比较函数里绝对不能写 >= 或 <=。sort 要求「两个元素一样时必须返回 false」,写成 >= 时 a 和 b 相等也返回 true,sort 内部就会越过数组边界一直找下去。⚠ 而小数据上它常常正好不崩,第 10 章量过这件事。
sort(a.begin(), a.end())升序排整个 vector。怎么读a.begin()指向第一个元素,a.end()指向最后一个的下一格(不是最后一个)。所以这一对正好圈住整个数组。复杂度 O(n log n)。什么时候用贪心题的第一步几乎都是它(第 19、20 章);二分查找之前也必须先排好。第 3 章用到 →sort(arr, arr + n)裸数组的写法:首地址、尾后地址。怎么读裸数组没有.begin(),但数组名本身就当「第一个元素的地址」用,arr + n就是「第 n 格的地址」= 尾后。和上一条完全等价。⚠ ⚠ 如果下标是从 1 开始存的(本书暂未用到a[1..n]),要写sort(a + 1, a + n + 1)。写成sort(a, a + n)会把没用的a[0]排进去、漏掉a[n]。sort(a.begin(), a.end(), greater<int>())降序。怎么读第三个参数就是「比较规则」。greater<int>()是 STL 给好的「大的排前面」。⚠ 后面那对空括号不能少 —— 它要的是一个对象,不是类型名。什么时候用也可以不用它:第 16 章用到 →sort完之后reverse(a.begin(), a.end())一样。struct把几个变量打包成一个。存一条边、一个学生、一个状态都用它。怎么读struct Edge { int u, v, w; };定义了一个新类型,之后Edge e; e.u = 1;或者vector<Edge> es;。⚠ 花括号后面那个分号不能漏。初始化可以直接写Edge e = {1, 2, 5};,顺序就是声明的顺序。什么时候用第 34 章存边(起点、终点、边权三个数要一起排序)、第 10 章存学生(名字 + 分数)。讲透它的是 第 29 章第 10 章用到 →[](const T &a, const T &b) { return ...; }lambda:就地写一个没名字的小函数,交给 sort 当比较规则。怎么读[]是它的开头标记(意思是「不捕获外面的变量」),后面的括号是参数、花括号是函数体。整个东西可以直接当参数塞进sort的第三个位置,不用另外在外面定义一个函数。例:sort(v.begin(), v.end(), [](const Node &a, const Node &b){ return a.w < b.w; });什么时候用按结构体的某个字段排序时。要用到外面的变量就把[]写成[&]。⚠ 返回的是「a 该排在 b 前面吗」,不是「谁更大」。第 6 章用到 →比较器里不许写 >=sort 要的是严格小于:a 和 b 一样时必须返回 false。怎么读sort 内部靠「总能找到一个比我小的」来停下来。如果 a == b 时你也说「a 排前面」,它就会一直往前找、越过数组边界 —— 表现是 Segmentation fault 或者莫名其妙的结果。什么时候用要「分数相同时按学号排」,不是改成>=,而是再比一个字段:if (a.score != b.score) return a.score > b.score; return a.id < b.id;⚠ 写成>=遇到并列可能直接崩(越界读),而小数据上看着完全正常 —— 第 10 章量过。讲透它的是 第 10 章本书暂未用到stable_sortsort 不稳定:并列的两个元素谁在前不确定。要保住输入顺序用它。怎么读「稳定」的意思是:比较规则判不出高下的两个元素,排完之后相对顺序和原来一样。sort不保证这件事,stable_sort保证(代价是稍慢一点、要额外内存)。什么时候用题面写着「分数相同时按输入顺序输出」 —— 要么用stable_sort,要么在比较规则里把输入序号也写进去(后者更稳,推荐)。⚠ 第 45 章量过一次反过来的:同一个「漏掉输入顺序」的疏忽,配 sort 是 WA,配(稳定的)冒泡反而 AC。第 10 章用到 →pair<int, int>两个值打包。.first/.second取出来。怎么读比struct省事,代价是字段没有名字。造一个用{a, b}或make_pair(a, b);vector<pair<int,int>> v; v.push_back({1, 2});什么时候用Dijkstra 的堆里放「(距离, 编号)」、存图时放「(邻居, 边权)」。⚠ 它自带比较规则:先比 first,first 一样再比 second —— 所以「按 x 排、x 同按 y 排」根本不用写比较器。⚠ 而谁放 first 是约定不是类型,写反了编译器一声不吭。第 14 章用到 →
// 排序与比较器速查 —— 本书第 10 章正式教排序,但从第 2 章起就在用 sort//// 你要记的只有一件事:sort 不知道「谁该排前面」,那是你告诉它的。// 不告诉它,它就按 < 来排。
#include <bits/stdc++.h>using namespace std;
struct Student { // struct 就是「把几个变量打包成一个」 string name; int score;};
int main() { // ① 最常见的两种写法:vector 用 begin/end,裸数组用指针 vector<int> v = {5, 2, 9, 1}; sort(v.begin(), v.end()); cout << "① 升序 "; for (int x : v) cout << x << ' '; cout << "\n";
int arr[4] = {5, 2, 9, 1}; sort(arr, arr + 4); // 裸数组:首地址 和 尾后地址 cout << " 裸数组 "; for (int x : arr) cout << x << ' '; cout << "\n\n";
// ② 降序:给它第三个参数 sort(v.begin(), v.end(), greater<int>()); cout << "② 降序 "; for (int x : v) cout << x << ' '; cout << "\n\n";
// ③ 按自己的规则排:写一个「a 该排在 b 前面吗」的函数。 // [](...){...} 这个写法叫 lambda,可以理解成「就地写一个没名字的小函数」。 vector<Student> s = {{"li", 90}, {"wang", 75}, {"zhao", 90}, {"sun", 60}}; sort(s.begin(), s.end(), [](const Student &a, const Student &b) { return a.score > b.score; // 分数高的排前面 }); cout << "③ 按分数从高到低\n"; for (auto &t : s) cout << " " << setw(5) << t.name << " " << t.score << "\n"; cout << "\n";
// ④ 两条规矩,每一条都真的会咬人: // // (a) 比较函数里**不能写 >=**。sort 要求「严格小于」:a 和 b 一样时必须返回 false。 // 写成 a.score >= b.score,遇到并列时程序可能直接崩(越界读), // 而小数据上它看着完全正常 —— 第 10 章量过这件事。 // // (b) sort **不稳定**:分数一样的两个人,谁在前面是不确定的。 // 上面 li 和 zhao 都是 90 分,顺序不保证和输入一致。 // 要保住输入顺序,要么用 stable_sort,要么把「输入序号」也写进比较规则。 vector<Student> s2 = {{"li", 90}, {"wang", 75}, {"zhao", 90}, {"sun", 60}}; stable_sort(s2.begin(), s2.end(), [](const Student &a, const Student &b) { return a.score > b.score; }); cout << "④ stable_sort:并列时保住输入顺序\n"; for (auto &t : s2) cout << " " << setw(5) << t.name << " " << t.score << "\n"; cout << "\n";
// ⑤ pair 自带比较规则:先比 first,first 一样再比 second。 // 所以「按 x 排,x 一样按 y 排」根本不用写比较器。 vector<pair<int, int>> p = {{2, 5}, {1, 9}, {2, 1}}; sort(p.begin(), p.end()); cout << "⑤ pair 默认先比 first 再比 second\n "; for (auto &q : p) cout << '(' << q.first << ',' << q.second << ") "; cout << "\n"; return 0;}点「运行 ▶」看结果
读完这一组 ⇒ 做 2 道例题 + 3 道练习 练那句唯一要记的话:sort 不知道谁该排前面,那是你告诉它的 —— 而并列的时候,它更不知道。
6. STL 容器(本书只用到这几个)
不用通读 STL 文档。这一组就是这本书从头到尾用到的全部,而且每个容器只用两三个动作。
这五个容器解决的是同一类问题:我要把一堆东西存起来,然后按某种规矩取出来。差别只在「按什么规矩取」—— 谁先进先出、谁后进先出、谁永远给最大的、谁能快速查在不在。
★ 一张表就能选完:要「一层一层地扩散」用 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;。
queue<int>先进先出。BFS 全靠它。动作只有 push / front / pop / empty。怎么读想成排队买票:push是来一个人站到队尾,front是看看排头是谁,pop是排头走人(⚠ 它不返回那个值,要先front拿到再pop)。什么时候用BFS 的骨架永远是这五句:起点 push →第 14 章用到 →while (!q.empty())→front取出 →pop→ 把邻居 push 进去。第 14 章起每一章都是它。stack<int>后进先出。动作是 push / top / pop / empty —— 取元素叫 top 不叫 front。怎么读想成一摞盘子:只能往最上面放,也只能从最上面拿。什么时候用第 35 章的单调栈。⚠ 而 DFS 不需要它 —— 递归函数自带的调用栈就是它,只有在「递归太深会爆栈、必须手动改成循环」时才会自己写一个。第 35 章用到 →priority_queue<int>每次取出最大的(大根堆)。怎么读它不是排好序的 —— 它只保证堆顶是最大的那个。push和pop都是 O(log n),取top是 O(1)。⚠ 取元素同样叫top,而且pop不返回值。什么时候用第 37 章合并果子、第 32 章 Dijkstra。凡是「反复取当前最优的那个」就是它。讲透它的是 第 32 章第 31 章用到 →priority_queue<int, vector<int>, greater<int>>小根堆。这一长串背下来就行 —— Dijkstra 每次都要写。怎么读三个参数分别是「装什么」「底下用什么容器装」「怎么比」。中间那个vector<int>是默认值,只是因为要写第三个参数,才不得不把它也补上。⚠ 两个>>中间在 C++11 之后不用空格了,但写成> >也对。什么时候用Dijkstra 里放 pair 时是第 32 章用到 →priority_queue<pair<int,int>, vector<pair<int,int>>, greater<>>。★ 另一个偷懒写法:把值取负塞进大根堆,取出来再取负 —— 效果一样,少写一串。set<int>自动去重、自动有序。查在不在用count,O(log n)。怎么读s.insert(x)插入(已经有了就什么都不做),s.count(x)返回 0 或 1,s.erase(x)删掉。遍历for (int x : s)出来是从小到大的。什么时候用「这个状态之前访问过吗」(第 15 章八数码)、「把重复的去掉」。⚠ 要允许重复就用第 29 章用到 →multiset。map<K, V>一张「键 → 值」的表,键自动有序。cnt[w]++时键不存在会自动新建、值从 0 开始。怎么读当成「下标可以是任意类型的数组」来用:map<string,int> cnt; cnt["ab"]++;。遍历用for (auto &kv : mp),kv.first是键、kv.second是值。什么时候用数每个单词出现几次;给一堆很大的编号做映射(不想开一个 10⁹ 的数组时)。⚠ ⚠ 光是读一下第 15 章用到 →cnt["zz"]也会顺手把这个键建出来 —— 只想查在不在就用count。unordered_map<K, V>不排序的 map,平均更快。顺序不确定,别指望遍历出来是有序的。怎么读它用哈希表实现,平均 O(1);map用平衡树,稳定 O(log n)。用法完全一样,换个名字就行。什么时候用只查不遍历、而且数据量大时用它。⚠ 但它最坏会退化成 O(n),有些出题人会专门卡这个 —— 稳妥起见比赛里第 15 章用到 →map更保险。auto「类型你自己推,我不写了」。最常见于for (auto &kv : mp)。怎么读编译器看等号右边是什么类型,就把auto换成什么。auto x = 1;里 x 是 int;for (auto &kv : mp)里 kv 是那一对键值。什么时候用类型名很长的时候(第 14 章用到 →pair<int,int>、迭代器)。⚠ 加&是为了不复制一份 —— 遍历一个装 string 的容器时,少这个 & 会白白复制每个串。
// STL 容器速查 —— 本书只用到五个,而且每个只用两三个动作//// 不用通读 STL 文档。下面这张单子就是这本书从头到尾用到的全部。
#include <bits/stdc++.h>using namespace std;
int main() { // ① queue:先进先出。BFS(第 14 章起)全靠它。 queue<int> q; q.push(1); q.push(2); q.push(3); cout << "① queue 先进先出:"; while (!q.empty()) { cout << q.front() << ' '; q.pop(); } cout << "\n 用到的动作只有 push / front / pop / empty\n\n";
// ② stack:后进先出。DFS 改成迭代写法时用(第 13 章)。 stack<int> st; st.push(1); st.push(2); st.push(3); cout << "② stack 后进先出:"; while (!st.empty()) { cout << st.top() << ' '; st.pop(); } cout << "\n 动作是 push / top / pop / empty —— 注意取元素叫 top 不叫 front\n\n";
// ③ priority_queue:每次取出**最大**的。第 32 章 Dijkstra、第 37 章堆都要它。 priority_queue<int> big; for (int x : {3, 1, 4, 1, 5}) big.push(x); cout << "③ priority_queue 默认大根堆:"; while (!big.empty()) { cout << big.top() << ' '; big.pop(); } cout << "\n"; // 要每次取**最小**的,就得把那一长串类型写全 —— 背下来就行: priority_queue<int, vector<int>, greater<int>> small; for (int x : {3, 1, 4, 1, 5}) small.push(x); cout << " 小根堆 priority_queue<int, vector<int>, greater<int>>:"; while (!small.empty()) { cout << small.top() << ' '; small.pop(); } cout << "\n\n";
// ④ set:自动去重 + 自动有序。查一个数在不在,O(log n)。 set<int> s = {5, 2, 5, 9}; cout << "④ set 去重且有序:"; for (int x : s) cout << x << ' '; cout << "\n s.count(5) = " << s.count(5) << "(在不在,只会是 0 或 1)" << " s.size() = " << s.size() << "\n\n";
// ⑤ map:一张「键 -> 值」的表,键自动有序。 map<string, int> cnt; for (string w : {"ab", "cd", "ab"}) cnt[w]++; // 键不存在时自动新建,值从 0 开始 cout << "⑤ map 数个数:"; for (auto &kv : cnt) cout << kv.first << '=' << kv.second << " "; cout << "\n ⚠ 光是写 cnt[\"zz\"] 去读它,也会「顺手把这个键建出来」 —— " << "只想查在不在就用 cnt.count(\"zz\")\n"; cout << " 现在 cnt.size() = " << cnt.size() << ",读一下 cnt[\"zz\"]=" << cnt["zz"] << " 之后 size 变成 " << cnt.size() << "\n\n";
// ⑥ auto 和范围 for:上面已经用了好几次 // `auto &kv : cnt` 的意思是「依次取出 cnt 里每一项,类型你自己推,我不写了」。 // 加 & 是为了不复制一份(大对象上这很值钱),顺便也能就地修改。 vector<int> v = {1, 2, 3}; for (int &x : v) x *= 10; // 加 & 才改得动原来的 cout << "⑥ for (int &x : v) x *= 10 -> "; for (int x : v) cout << x << ' '; cout << "\n"; return 0;}点「运行 ▶」看结果
读完这一组 ⇒ 做 2 道例题 + 3 道练习 把那张「按什么规矩取东西」的选型表用一遍 —— 五个容器,每个只用两三个动作。
7. 读入与输出
第 1 章的解析代码就在用关同步那两行,而真正解释它的是第 45 章第 11 步。
这一组里每一条都被本书量过,不是传说。而要先分清两件常被混在一起的事:「读对」和「读快」。 读错了是 WA(比如 getline 吃到一个空行),读慢了是 TLE —— 两者的症状完全不同,救法也完全不同。
★ 先说「读快」这一半:什么时候要管它,是一道算术题。数一数这道题总共要读多少个数:10⁶ 个以内,默认 cin 就够(约 0.16 秒);到 10⁷ 个,就必须关同步(第 6 章那道题读 47 MB,不关同步会超时)。⇒ 不到这个量级就别折腾,写快读反而容易写错。
★ 再说「读对」:>> 会自动跳过空格和换行,所以读一串数字从来不用管格式。但 getline 不跳 —— 它从当前位置一直读到换行为止。两者混用时,前一个 >> 留下的那个换行就会被 getline 当成「一个空行」读走。这是本组最常见的坑。
ios::sync_with_stdio(false); cin.tie(nullptr);关掉 cin 和 scanf 的同步,读入能快好几倍。怎么读C++ 的cin默认要和 C 的scanf共用同一套缓冲,保证两者混着用也不乱序 —— 这份保证很贵。第一句把它断开,cin就用自己的缓冲。第二句是解开cin和cout的绑定(默认每次cin前都会先把cout刷出去)。⚠ 两句都写在main的第一行,写在读入之后就没用了。什么时候用读的数超过 10⁶ 个就写上。反正代价为零,本书的习惯是一律写。⚠ 关了之后就不要再混用scanf/printf。第 41 章量到读这一侧值 6.2 倍,而写那一侧只值 1.1 倍 —— 两条并排的建议,分量差一个数量级。讲透它的是 第 45 章第 1 章用到 →'\n' 而不是 endlendl 除了换行还会强制刷新缓冲区,输出量大时慢很多。怎么读「刷新缓冲区」就是「立刻把攒着的字都真正写出去」。正常情况下攒一批一起写最快,而endl每一行都强制写一次。输出十万行就是十万次系统调用。什么时候用只有在「程序可能中途崩掉、想看到已经打出来的内容」时才需要endl。竞赛里一律'\n'。⚠ 第 41 章和第 48 章各量过一次,都是 8.8 倍。本书暂未用到while (cin >> x)读到文件结尾。题目不告诉你有几个数时用它。怎么读cin >> x这个表达式本身有一个值:读成功了是「真」,读到头了是「假」。所以可以直接拿它当循环条件。什么时候用第 22 章导弹拦截那种「输入不给个数」的题。⚠ 本地测试时要用 Ctrl+D(Linux)告诉它输入结束,不然它一直等。第 24 章用到 →getline(cin, s)读一整行(含空格)。怎么读cin >> s遇到空格就停,所以读「一行带空格的句子」只能用getline。它从当前位置一直读到换行,并把那个换行吃掉但不放进 s。什么时候用读一整行英文句子、读一整行地图。⚠ ⚠ 紧跟在第 47 章用到 →cin >> n后面用会读到一个空串 —— 因为>>停在数字后面,那一行的换行还留在输入里,getline当场就撞上它。改法:中间加一句cin.ignore();(扔掉一个字符)。printf("%lld", x)long long 的格式是%lld,int 是%d,保留两位小数是%.2f。怎么读printf靠你写的格式串去猜参数类型 —— 写错了它不会报错,只会打出垃圾。常用的四个:%d(int)、%lld(long long)、%s(C 字符串)、%.2f(double 两位小数)。什么时候用要控制小数位数时第 26 章用到 →printf最省事(cout要写fixed << setprecision(2))。⚠ 打string要写s.c_str(),直接塞%s会崩。
// 读入输出速查 —— 本书第 6 章的正文代码就在用关同步那两行,第 10 章才解释它//// 试着把右边的输入改一改再运行。
#include <bits/stdc++.h>using namespace std;
int main() { // ① 这两行是什么意思 // cin 默认和 C 的 scanf 保持同步,这份同步在数据量大时很贵。 // 关掉它,cin 能快好几倍(第 6 章那道题读 47 MB,第 41 章量到差 6.2 倍)。 // 代价只有一条:关了之后**不要再混用 scanf / printf**。 ios::sync_with_stdio(false); cin.tie(nullptr);
int n; cin >> n; // >> 会自动跳过前面的空格和换行 cout << "① 读到 n = " << n << "\n";
// ② getline 的坑:>> 停在数字后面,**那一行的换行还留在输入里**。 // 紧接着 getline 会读到一个空串。 string bad; getline(cin, bad); cout << "② 紧跟着 getline 读到的是 [" << bad << "] <- 空的,就是上面那个换行\n";
// 正确做法:先把那个换行扔掉(或者干脆再 getline 一次)。 string line; getline(cin, line); cout << " 再 getline 一次才是真正的一行:[" << line << "]\n"; cout << " 更稳的写法是 >> 之后写一句 cin.ignore();\n";
// ③ 读到文件结尾:题目不告诉你有几个数时用这个 int x, sum = 0, cnt = 0; while (cin >> x) { sum += x; cnt++; } cout << "③ 剩下的 " << cnt << " 个数读完了,和 = " << sum << "\n";
// ④ 换行用 '\n',别用 endl。 // endl 除了换行还会**强制刷新缓冲区**,输出量大时慢很多 // (第 41、48 章各量过一次,都是 8.8 倍)。 cout << "④ 用 '\\n' 换行,不要用 endl\n";
// ⑤ printf 的格式:int 用 %d,long long 用 %lld,double 保留两位用 %.2f。 // ⚠ 上面关了同步,这里就不该再用 printf 了;单独用时才写。 return 0;}点「运行 ▶」看结果
读完这一组 ⇒ 做 2 道例题 + 3 道练习 先把「读对」练熟(getline 那个坑),再顺手把「打对」练一遍(行末那个空格)。「读快」这一半只有量大时才管。
8. 函数、引用与递归
第 1 章就开始写递归函数,而参数表里那个 & 到第 10 章才顺口解释一句。
先说那个 &。C++ 传参数默认是传值:函数拿到的是一份复制品,在里面怎么改都影响不到外面。参数类型后面加一个 & 就变成传引用:函数拿到的就是外面那个东西本身。
★ 而对数组、字符串、结构体来说,这个 & 不只是「能不能改」的问题,更是快慢的问题 —— 不写 &,每次调用都要把整个数组复制一遍。一百万个 int 实测差 6.7 倍,而递归里调用几十万次就是几十万次复制。⇒ 参数是容器时一律写 const T &(const 表示「我只读不改」,顺带防手滑)。
★★ 再说递归。递归函数每进一层,它的局部变量都要在栈上占一块地方,而栈通常只有 8 MB。所以两件事要当心:一是必须有出口(否则无限递归,几万层就崩);二是别在递归函数里开大数组(每层都占一份)。
⚠ 本书量过一个反直觉的数:同一台机器上,递归函数体里多几个变量,能递归的层数就从 17.4 万掉到 2.4 万 —— 能递归多深不是机器的属性,是「你在那个函数里写了什么」的属性。
void f(int x)传值:函数里改的是一份复制品,外面那个不会变。怎么读调用f(a)时,a的值被抄进x。之后x = 99;改的只是那份抄件。什么时候用参数是单个 int / char / double 时就这么写,复制一个数不值几个钱。本书暂未用到void f(int &x)传引用:函数里改的就是外面那个。怎么读x不是副本,而是外面那个变量的另一个名字。所以x = 99;真的改了外面的a。什么时候用想让函数「顺手改掉某个变量」时 —— 比如void dfs(int u, int &cnt)里累加计数。⚠ 不过本书更常用全局变量干这件事,递归里少传一个参数更清爽。讲透它的是 第 10 章第 10 章用到 →f(const vector<int> &a)参数是数组 / 字符串 / 结构体时,一律这么写。怎么读三个记号各管一件事:vector<int>是类型,&是「别复制」,const是「我不改它」。少了&就是整份复制;少了const编译器不会拦你手滑改掉调用方的数据。什么时候用任何把 vector / string / struct 当参数的函数。⚠ 比较器 lambda 里也要写(第 6 章用到 →[](const Node &a, const Node &b))—— sort 会调用它几百万次。全局变量所有函数都看得见,不用一层层传。递归里省下大量参数。怎么读写在所有函数外面就是全局的。它们在程序启动时自动清零,而且不占栈。什么时候用本书的习惯:图、答案、访问标记这些「整道题共用」的东西放全局,只有「这一层特有」的量才当参数传(比如 dfs 的当前节点 u)。⚠ ⚠ 多组数据的题里,全局变量每组之间必须自己清干净 —— 它只在程序启动时清过一次。这是「第一组对、第二组起全错」的头号原因;另一个代价是「谁改了它」不好追。本书暂未用到递归的出口每个递归函数都必须有一句「到这儿就 return,不再往下递」。怎么读写递归的顺序永远是:① 先写出口(什么情况下不用再递了、答案直接是什么);② 再写递推(假设小一号的问题已经算对了,我怎么用它拼出答案)。⚠ 第 ② 步的关键是相信它会算对,不要在脑子里展开执行 —— 第 1 章讲的就是这件事。什么时候用每一个递归函数。没有出口的表现是程序跑一两秒后 Segmentation fault。本书暂未用到别在递归函数里开大数组每进一层,局部变量都要在栈上占一块地方。怎么读能递归多深 = 栈上限 ÷ 每层占多少字节。本书实测过两份:一份精简的 dfs 每层 48 字节、能递归 17.4 万层;一份在函数里拼字符串的每层 341 字节、只能 2.4 万层。什么时候用⚠ 判据是一道算术题:这道题最深会递归多少层? 顶格n = 10⁵的链式数据就是 10 万层,已经踩在门槛上了 ⇒ 本书的结论是「顶格题一律写迭代,别赌」。讲透它的是 第 30 章本书暂未用到
// 函数、引用与递归速查 —— 那个 & 到底是什么意思//// 本书第 1 章就开始写递归函数,而参数表里的 & 在第 10 章才顺口解释。// 这一份把它单独拎出来。
#include <bits/stdc++.h>using namespace std;
void byValue(int x) { x = 99; } // 传值:改的是一份复制品void byRef(int &x) { x = 99; } // 传引用:改的就是原来那个
// 传 vector 时这个 & 不只是「能不能改」的问题,还是**快慢**的问题:// 不写 &,每次调用都会把整个数组复制一遍。long long sumCopy(vector<int> a) { long long s = 0; for (int x : a) s += x; return s; }long long sumRef(const vector<int> &a) { long long s = 0; for (int x : a) s += x; return s; }// ^^^^^ const 的意思是「我只读,不改」—— 能防手滑,也让调用方放心
int depth = 0; // 全局变量:所有函数都看得见,不用传来传去void dive(int k) { // 递归:函数调用自己 depth = max(depth, k); if (k == 5) return; // 出口。没有出口就是无限递归 -> 栈溢出 dive(k + 1);}
int main() { int a = 1; byValue(a); cout << "① byValue(int x) 之后 a = " << a << " <- 没变\n"; int b = 1; byRef(b); cout << " byRef(int &x) 之后 b = " << b << " <- 变了\n\n";
vector<int> v(1000000, 1); auto t0 = chrono::steady_clock::now(); long long s1 = sumCopy(v); auto t1 = chrono::steady_clock::now(); long long s2 = sumRef(v); auto t2 = chrono::steady_clock::now(); double msCopy = chrono::duration<double, milli>(t1 - t0).count(); double msRef = chrono::duration<double, milli>(t2 - t1).count(); cout << "② 一百万个元素求和,答案都是 " << s1 << " / " << s2 << "\n"; cout << fixed << setprecision(3); cout << " 传值 vector<int> a " << msCopy << " ms <- 先复制了一百万个 int\n"; cout << " 传引用 const vector<int> &a " << msRef << " ms\n"; cout << " => 参数是数组 / 字符串 / 结构体时,「一律写 const T &」,除非你真想改它\n\n";
dive(0); cout << "③ 递归 dive(0) 最深到了第 " << depth << " 层\n"; cout << " 每进一层,函数的局部变量都要在「栈」上占一块地方。\n"; cout << " 所以两件事要当心:一是必须有出口,二是「别在递归函数里开大数组」\n"; cout << " (本书量过:函数体里多几个变量,能递归的层数就从 17 万掉到 2.4 万)。\n"; return 0;}点「运行 ▶」看结果
读完这一组 ⇒ 做 2 道例题 + 3 道练习 两件事:参数表里那个 & 到底改了什么,以及写递归时「先出口、后递推」这个固定顺序。
9. 零碎但天天用的那几样
单独讲不值一节,可不知道就会卡住 —— 而它们在本书每一份代码里都出现。
这一组里没有一样是「算法」,全是写代码的手势。每一条都只要看一眼就会,但没人告诉你的话,读别人的代码时就会卡在那儿。
#include <bits/stdc++.h>一句话把所有标准库都包进来。怎么读它不是标准的一部分,是 GCC 提供的一个「万能头」。正式项目里不该用(编译慢),但竞赛评测机基本都是 GCC,本书一律用它 —— 省得去记vector在哪个头、sort在哪个头。⚠ ⚠ 如果你的编译器不认它(比如苹果的 clang),就老老实实写第 1 章用到 →<iostream> <vector> <algorithm> <string> <cstring>这几个。using namespace std;之后写cout就行,不用写std::cout。怎么读标准库的名字都住在一个叫std的命名空间里。这一句是「把它们都拿到台面上来」。⚠ ⚠ 它偶尔会撞车:自己定义一个叫第 1 章用到 →count、next、y1、hash的全局变量,会和标准库里的同名东西冲突,报一句很长的reference to ... is ambiguous。改法就是换个名字(cnt、nxt)。const int N = 100005;把数组大小写成一个常量,别到处散落魔法数字。怎么读那个多出来的 5 是留余量的:下标从 1 开始要多一格,加哨兵还要几格。题面写n ≤ 100000,就写100005。什么时候用配合全局数组:第 1 章用到 →const int N = 100005; int a[N];—— 这是竞赛代码最常见的开头。swap(a, b)交换两个变量,不用自己写中间变量。怎么读对 vector、string 这些也管用,而且是 O(1) 的(只换内部指针,不真的搬数据)。什么时候用排序、双指针里到处都是;第 3 章全排列里「换过去、递归、换回来」就是它。第 6 章用到 →max(a, b) min(a, b)取大 / 取小。三个以上要加一对花括号:max({a, b, c})。怎么读⚠ 两个参数的类型必须一样:max(1, 2LL)编译不过,要写max(1LL, 2LL)或者max<long long>(1, 2)。这是新手最常撞的一条编译错误。什么时候用DP 的转移方程几乎每一行都是第 1 章用到 →f[i] = max(f[i], f[j] + w)。abs(x)绝对值。怎么读整数用abs,浮点数用fabs(在 C++ 里abs也能处理浮点,但写fabs更清楚)。什么时候用算曼哈顿距离第 3 章用到 →abs(x1-x2) + abs(y1-y2)、判两个数差多少。INT_MAX LLONG_MAX类型的上界,比自己背「二十一亿」靠谱。怎么读求最小值时常写int best = INT_MAX;然后不断best = min(best, x);。⚠ ⚠ 但如果后面要拿它做加法,用第 3 章用到 →0x3f3f3f3f更安全(见第二组)——INT_MAX + 1当场溢出成负数。cond ? a : b三目运算符:一行的 if-else,而且它是一个有值的表达式。怎么读读成「如果 cond 成立就取 a,否则取 b」。因为它有值,所以可以直接写在cout <<里、写在return后面。什么时候用cout << (n % 2 ? "奇" : "偶");、return x > 0 ? x : -x;。⚠ ⚠ 它的优先级很低,放进本书暂未用到cout <<时要加括号,否则编译报错或者结果不对。
// 零碎但天天用的那几样 —— 单独讲不值一节,可不知道就会卡住//// 这一份里没有一样是「算法」,全是「写代码的手势」。
#include <bits/stdc++.h>using namespace std;
// ★ 全局数组开多大:写成一个常量,别到处散落魔法数字// 竞赛里的习惯是「题面上限 + 5」,那 5 是给下标从 1 开始、以及哨兵留的余量。const int N = 100005; // 题面说 n <= 100000int a[N]; // 全局数组:自动全是 0,而且不占栈
int main() { // ① swap:交换两个变量,不用自己写中间变量 int x = 1, y = 2; swap(x, y); cout << "① swap 之后 x = " << x << ", y = " << y << "\n\n";
// ② min / max:两个数直接比;三个以上要加一层花括号 cout << "② max(3, 7) = " << max(3, 7) << " min(3, 7) = " << min(3, 7) << "\n"; cout << " 三个数:max({3, 7, 5}) = " << max({3, 7, 5}) << " (注意那对花括号)\n"; cout << " ⚠ max(1, 2LL) 编译不过 —— 两个参数的类型必须一样,要写 max(1LL, 2LL)\n\n";
// ③ abs:绝对值 cout << "③ abs(-5) = " << abs(-5) << " abs(5 - 9) = " << abs(5 - 9) << "\n\n";
// ④ 类型的上下界,比自己背 21 亿靠谱 cout << "④ INT_MAX = " << INT_MAX << " LLONG_MAX = " << LLONG_MAX << "\n"; cout << " 求最小值时常写 int best = INT_MAX; 然后不断 best = min(best, x);\n"; cout << " ⚠ 但如果后面要拿它做加法,用 0x3f3f3f3f 更安全(见「整数类型」那一组)\n\n";
// ⑤ 三目运算符:一行的 if-else,它是一个「表达式」,有值 int n = 7; cout << "⑤ (n % 2 ? \"奇\" : \"偶\") -> " << (n % 2 ? "奇" : "偶") << "\n"; cout << " 它等价于 if (n % 2) … else …,但可以直接写在 cout 里\n\n";
// ⑥ 全局数组 vs 局部数组:大数组一定要放全局(或 static),否则爆栈 a[3] = 42; cout << "⑥ 全局 int a[" << N << "]:a[0] = " << a[0] << "(自动清零),a[3] = " << a[3] << "\n"; cout << " 而在 main 里写 int b[100005]; 是放在「栈」上的,栈一般只有 8 MB —— \n"; cout << " 开到几百万就直接 Segmentation fault,而且编译不报错。\n"; return 0;}点「运行 ▶」看结果
读完这一组 ⇒ 做 2 道例题 + 3 道练习 这一组没有一样是算法,全是写代码的手势 —— 练一遍就不会再在读别人代码时卡住。
10. 报错了怎么读
编译器的话不难懂,只是没人翻译过。这张表覆盖本书写代码时真正撞到过的那几条。
★ 先记一条通用的读法:只看第一条错误,改完再编译一遍。C++ 的编译错误会「雪崩」—— 前面一个分号引发后面几十条,而后面那几十条全是噪声。一次只改最上面那一条。
★★ 第二条:报错的行号常常指到出错行的下一行。 因为编译器是读到下一行才发现「上一句没写完」。看到行号先往上看一行。
⚠ 第三条,也是最要紧的:能编译过不等于是对的。 越界、溢出、没初始化、递归太深 —— 这四样编译器一句话都不会说,它们要么在运行时崩,要么安静地给出一个错答案。下面最后三条讲的就是它们。
expected ';' before ...上一行漏了分号。怎么读例:int a = 1少了分号,编译器读到下一行cout << a;才发现不对,于是报的是下一行的行号。⇒ 看到这条一律往上看一行。⚠ ⚠ 另一个高频来源:本书暂未用到struct Node { int x; }后面那个分号不能漏 —— 花括号结尾一般不用分号,唯独struct/class要。'xxx' was not declared in this scope这个名字编译器不认识。怎么读按可能性从高到低查三件事:① 拼错了(cnt写成cnf);② 作用域不对 —— 它定义在某个{}里面,出了那对花括号就不存在了;③ 忘了#include(本书一律用<bits/stdc++.h>,所以基本不是这个原因)。⚠ ⚠ 还有一种最气人的:在函数 A 里调用了写在它下面的函数 B。C++ 是从上往下读的 ⇒ 要么把 B 挪到 A 前面,要么在最上面先写一行声明本书暂未用到int B(int);。no matching function for call to ...函数名对,但参数对不上。怎么读两种:个数不对,或者类型不对。最常撞的是max(1, 2LL)——max要求两个参数类型完全一样,int 和 long long 混着传就找不到匹配的版本。改成max(1LL, 2LL)。⚠ 报错信息里会列出一串「candidate:」,那是它试过的所有版本 —— 不用逐条看,直接回去核对自己传的参数类型。本书暂未用到invalid operands of types ...这两个类型不能这么运算。怎么读例:本书暂未用到string s = "ab"; int x = s + 1;—— 串加数字没有定义。想把数字接到串后面要写s + to_string(1);想把串变成数字要写stoi(s)。comparison of integer expressions of different signedness有符号和无符号在比大小。怎么读多半是int i和v.size()在比。size()是无符号的,比较时int会被悄悄转成无符号 —— 于是-1 < v.size()竟然是假的。⚠ ⚠ 它只是警告,编译照样过。但空数组上本书暂未用到i <= v.size() - 1会真的错。改法:i < (int)v.size()。Segmentation fault(运行时)越界访问,或者递归太深爆了栈。编译期一句话都不会说。怎么读查的顺序:① 数组开够了没(a[n]要开n+1格,下标从 1 开始还要再多一格);② 下标算出来会不会是负的;③ 递归有没有出口、会不会太深。什么时候用最快的定位法:把可疑的a[i]临时换成a.at(i),越界会当场抛异常并告诉你下标是多少,而不是让程序在别处崩。讲透它的是 第 30 章本书暂未用到编译过了,可答案不对八成是这四样之一:溢出、没初始化、越界、读错。怎么读一条一条排除:① 溢出 —— 把所有 int 换成 long long 再跑一遍,变对了就是它;② 没初始化 —— 多组数据的题里全局数组每组都要清;③ 越界 —— 见上一条;④ 读错 —— 把读进来的东西原样打印出来看一眼,这一步能省掉一半的调试时间。什么时候用⚠ 而如果只是「部分测试点错」,那多半不是这四样,是算法本身在某种数据上不对 —— 这时候该写对拍了(术语表里有)。本书暂未用到
读完这一组 ⇒ 做 2 道例题 + 3 道练习 这一组的题形状不一样:先给你一版错的,连它真实的报错一起,请你说出它错在哪、再改好。⚠ 下面每一段报错都是真的跑出来抄的,不是编的。
没找到想查的写法?先确认它是不是算法概念而不是 C++ 语法 —— 那些在术语表里,那张表指的是「真正教它的那一章」。