读这一页之前,先知道读完要能做什么
- 写出来用
pair<int,int>排序,并说清「要按哪个关键字排,就把它放进first」 - 说清
array<int, N>和vector的差别:长度编译期写死,而且不替你清零 - 用
bitset<N>的下标、count()、flip()和移位,并说清「下标 0 在最右边」
这是什么
一句话:pair 装两样东西、array 装长度写死的一串、bitset 装一排 0/1 开关 —— 三个都是标准库自带的现成小件,个数一律在编译期就写死了。
★ pair:{a, b} 就是一个值,.first / .second 取出来。关键是它自带比较规则 —— 先比 first,first 相同再比 second,所以 sort 对 vector<pair> 一个比较器都不用写。
★ array<int, 10>:和 vector 的差别只有一条 —— 长度在编译期写死了:没有 push_back、没有 resize,而 a.size() 就是 10,编译期算得出来。⚠ 它就是一串裸数据,不像 vector 那样替你置 0,所以声明时要写 {}。
★ bitset<N>:N 个 0/1 开关,N 必须是编译期常量。数 1 的个数 count()、整排翻转 flip()、移位 <<=、按位 & | ^ 全都能用 —— 于是「两个集合求并、求交」就是一句 a | b、a & b。
⚠ 三个都不能改长度:长度要读进来才知道就用 vector / string;要装三样以上就嵌套 pair,或者用 第 1 块那个 struct。
完整动作表
复杂度写的是这个动作自己的代价,不是整个程序的。「这一栏最容易错的地方」那一列是重点。
| 写法 | 干什么 | 代价 |
|---|---|---|
pair<int,int> p = {a, b}; | 装两样东西,.first / .second 取出来★ 比 struct 省事,代价是字段没有名字 —— 谁是 first 全靠约定(陷阱 ②) | O(1) |
sort(v.begin(), v.end()) | 对 vector<pair> 直接排:先比 first,相同再比 second★★★ 「按 x 排、x 相同按 y 排」不用写比较器 —— 但顺序是你存的时候定的 | O(n log n) |
p < q / p == q | pair 自带比较:先 first,相同比 second★ 上一句能直接 sort 就是因为它 —— 和 第 1 块的「比较规则」是同一件事 | O(1) |
array<int, N> a{}; | N 个元素,{} 全部清零★★★ 漏掉 {} 里面是没初始化的垃圾值(陷阱 ①)—— vector 总给 0,它不给 | O(N) |
a.size() | 长度 N —— 而且是编译期就算出来的常量 ★ 循环上界直接用它: for (size_t i = 0; i < a.size(); i++) | O(1) |
a[i] / a.begin() a.end() | 下标访问、迭代器 —— 和 vector 一样★★ sort、min_element、范围 for 全都原样适用;⚠ 想加元素就换 vector,它没有 push_back | O(1) |
bitset<N> b; | N 个 0/1 开关,默认全 0 ★ N 必须是编译期常量:写成 bitset<n>(n 是读进来的变量)直接编译不过 | O(N / 字长) |
bitset<8> b("10110100") | 从 0/1 字符串构造,b.to_string() 转回来⚠ 字符串左边是高位: b[0] 是最右边那个字符(陷阱 ③) | O(N) |
b[i] | 第 i 位,读出来是 0 或 1;写 b[i] = 1 就是把那处置 1★★ 下标 0 在最右边(和 string 正相反);⚠ &b[i] 拿到的不是 bool*,别当普通数组传 | O(1) |
b.count() | 1 的个数 ★★ ⚠ 数的是整排 N 位,不是你只动过的那前 n 位;单个整数数 1 用 __builtin_popcount(x) | O(N / 字长) |
b.any() b.none() | 有没有 1 / 是不是全 0 ★ 「一个都没命中」就写 !b.any(),别自己写循环数一遍 | O(N / 字长) |
b.set(i) / b.reset(i) / b.flip() | 那位置 1 / 清 0 / 取反;不带参数是整排 ★ b.reset() 整排清零就是「重来一遍」,b.set() 整排置 1 常用来开全集 | O(N / 字长) |
b <<= 1 / b & c b | c | 移位、按位与或异或 —— 整排一起算 ★★★ 集合的交 / 并 / 对称差就是 a & b / a | b / a ^ b,一句顶一个循环 | O(N / 字长) |
⭐ 这一节最容易错的地方(3 处)
这一节排在例题前面是故意的:读到一个坑,再去看正确的例子,记得住。 下面每一条都是「编译得过去、答案可能是错的」那种 —— 它们才是 OI 里真正扣分的地方。
① `array<int, N> a;` 不写 `{}`,里面是没初始化的垃圾值
vector<int> a(10); 一比就明白:vector 永远给你 0,而 array 是一串裸数据,你不开口它就不初始化。
★ 于是和、最小值、最大值全是随机的:本机跑一遍对,提交上去 WA;而且每次跑的值都不一样 —— 这种错最难查。int a[10]; 和「函数里的 struct S s;」是同一个病。array-noinit.cpp: In function ‘int main()’:
array-noinit.cpp:7:12: warning: ‘a’ is used uninitialized [-Wuninitialized]
7 | for (int x : a) sum += x;
| ^
array-noinit.cpp:5:17: note: ‘a’ declared here
5 | array<int, 4> a; // ← 忘了写 {}
| ^array<int, 10> a{}; // ★ {} → 全 0
array<int, 10> b{}; b.fill(7); // 要填别的值就 fill
// 拿不准就问一句「它现在是什么值?」—— 答不上来就是这儿错了array 一律带 {}。这和「全局数组自动全 0、函数里的 int t[5]; 是垃圾值」是同一句话。② 想按 `second` 排,却直接 `sort` —— 它排的是 `first`
first 后 second),不会因为你「心里想按 y 排」就变。
★ 后果是静默的:输出照样整齐、看着也像那么回事,只是顺序不对 —— 题目要按 y 排,你交的是按 x 排,WA。编译器、运行时都不会提醒你。直接 sort(): (1, 2) (2, 7) (3, 1)
我想要的是按 y 排: (3, 1) (1, 2) (2, 7) sort(v.begin(), v.end()); // 按 first 排
// 要按 second 排,二选一:
sort(v.begin(), v.end(), [](auto &a, auto &b) {
return a.second < b.second;
});
// 或者存的时候就把 y 放进 first(不用写比较器)
v2.push_back({y, x});first —— 换不了位置时才写比较器(第 1 块那套)。③ `bitset` 的下标 0 在**最右边**,和 `string` 正相反
bitset<8> b("10110100") 打印出来是 10110100,可 b[0] 读到的是最右边那个 0,b[7] 才是最左边的 1。
★ 后果:你按「第 i 个字符」的直觉去取位,拿到的整排是左右颠倒的 —— 按位还原、拼二进制数、判第几位全错,而程序不报错。to_string() = 10110100
b[0] b[1] b[2] ... b[7] = 0 0 1 0 1 1 0 1 bitset<8> b("10110100");
b.to_string(); // → "10110100" 想按字符串的样子看就用它
b[7]; // → 1 最左边那一位
// 要「从左往右第 i 个字符」:
char c = b.to_string()[i];to_string()。本书哪一章真的用到它
⚠ 下表里的文件路径和那串 probe 是逐字验过的(check:docs 第 ⑳ 条)—— 改了那份代码这儿会红,正是要的。点章号直接去看那一页。
| 章 | 书里的哪一份代码 | 用它来干什么 |
|---|---|---|
| 第 23 章 | code/23-knapsack-01/p1049Bitset.cpp | ★★ 第 23 章 01 背包:f |= f << x 一句话就是「加上这件物品」—— 移位和或整排一起算 |
| 第 11 章 | code/11-merge-inversions/p1966.cpp | ★★ 第 11 章排名:{值, 下标} 装进 pair 后 sort 一把,下标跟着一起走 —— 不用写比较器 |
| 第 11 章 | code/11-merge-inversions/p1966Bf.cpp | ★ 排列长度固定是 8:长度不会变时 array<int, 8> 就是「定长的那一串」 |
| 第 49 章 | code/49-hash/p4551.cpp | ★ 第 49 章 01-Trie:每个结点固定两个儿子位,array<int, 2> 正好当那两个坑 |
例题(2 道)
每道都给全套:题面 → 样例 → 参考代码 → 一步步的讲解。先自己读题想一遍,再看代码。
例题 ① pair:按 (x, y) 排序,一个比较器都不用写
n 个点,每个点给两个整数。把它们按 x 从小到大排(x 相同按 y),再验证 pair 自带的三条比较,最后演示「按 y 排」该怎么改。
★ 这一题想证明的是:「按 x 排、x 相同按 y 排」这句话,sort 是白送的。
n。之后 n 行,每行两个整数 x y。输入
5 3 1 1 2 2 7 1 3 3 2
输出
按 (x, y) 排好: (1, 2) (1, 3) (2, 7) (3, 1) (3, 2) (1,2) < (1,3) ? 是 ← first 相同,比 second (1,2) < (0,9) ? 否 ← 先比 first,1 > 0 所以否 (1,2) == (1,2) ? 是 按 y 排(把 y 存进 first): (3, 1) (1, 2) (3, 2) (1, 3) (2, 7)
(1, 2) (1, 3) (2, 7) (3, 1) (3, 2):先看 first,所以 (3, 1) 跑到了 (2, 7) 后面 —— 这是「按 x 排」,不是按 y。// STL 专题 · pair —— 「按 x 排、x 相同按 y 排」不用写比较器//// ★ pair 自带比较规则,而且是**固定的**:// 先比 first,first 相同再比 second// 所以 vector<pair> 直接 sort() 就行,一个比较器都不用写。//// ⚠ 顺序是你自己定的:要「按分数排」就把分数放在 first。// 想按 second 排 —— 要么把顺序倒过来存,要么自己写比较器(第 0 块)。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n; cin >> n; vector<pair<int, int>> v; for (int i = 0; i < n; i++) { int x, y; cin >> x >> y; v.push_back({x, y}); }
sort(v.begin(), v.end()); // ★ 什么都不用写 cout << "按 (x, y) 排好:\n"; for (auto &[x, y] : v) cout << "(" << x << ", " << y << ") "; cout << "\n\n";
// pair 直接就能比 —— 拿两个具体的值看一眼 pair<int, int> a = {1, 2}, b = {1, 3}, c = {0, 9}; cout << "(1,2) < (1,3) ? " << (a < b ? "是" : "否") << " ← first 相同,比 second\n"; cout << "(1,2) < (0,9) ? " << (a < c ? "是" : "否") << " ← 先比 first,1 > 0 所以否\n"; cout << "(1,2) == (1,2) ? " << (a == pair<int, int>{1, 2} ? "是" : "否") << "\n";
// 真要按 y 排:把 y 放到前面存一份 vector<pair<int, int>> byY; for (auto &[x, y] : v) byY.push_back({y, x}); sort(byY.begin(), byY.end()); cout << "\n按 y 排(把 y 存进 first):\n"; for (auto &[y, x] : byY) cout << "(" << x << ", " << y << ") "; cout << "\n"; return 0;}点「运行 ▶」看结果
- ★★ 读进来就存成 pair:
v.push_back({x, y})—— 花括号就是 pair 的字面量。 - 一句
sort(v.begin(), v.end()),什么都不用传:它按first排,first相同再按second—— 正好是题面的规矩。 - ★★ 三条比较直接看:
(1,2) < (1,3)是(first相同,轮到second);(1,2) < (0,9)否(先比first,1 > 0)。 - 按 y 排:把
{y, x}存一份再排 —— 让想排的关键字站到first的位置上,就又不用写比较器了。
★★★ pair 的比较规则是固定的:先 first 后 second。「按哪个关键字排」是你存的时候决定的,不是 sort 决定的 —— 这就是陷阱 ②。
用到的写法:pair<int,int> p = {a, b}; sort(v.begin(), v.end()) p < q / p == q
例题 ② bitset:一排开关的置位、数 1、翻转和移位
读一个 8 位的 0/1 串和一个下标 i,构造 bitset<8>,依次演示 count()、按下标读、any()/none()、flip()、左右移和 reset()。
★ 一句话:bitset 就是「长度写死的一排开关」 —— 动作和第 46 章对单个整数做位运算一模一样,只是整排一起做。
i(0~7)。输入
10110100 3
输出
存进去的是 10110100 count() 数 1 的个数 = 4 下标 3 那一位是 0 ← b[i] 读出来就是 0 或 1 any() 有没有 1?有 none() 是不是全 0?不是 flip() 整排翻转 01001011 ← 等价于 flip() flip() 真跑一遍 01001011 左移一位 <<1 01101000 右移一位 >>1 01011010 reset() 之后 00000000 count() = 0 none()?是
10110100、i = 3:count() 是 4(四个 1),而 b[3] 是 0 —— 下标 0 在最右边,别按字符串位置去数(陷阱 ③)。// STL 专题 · bitset —— 一排开关:置位、数 1、翻转、移位//// ★ bitset<N> 就是 N 个 0/1 开关,下标 0 在最右边。// N 必须是**编译期已知的常量**(这里是 8,所以输入也得正好 8 个字符)。//// ⚠ 它和「一个整数当开关」(第 46 章)是同一套规则,只是长度不限于 32/64。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
string s; int i; cin >> s >> i;
bitset<8> b(s); cout << "存进去的是 " << b << "\n"; cout << "count() 数 1 的个数 = " << b.count() << "\n"; cout << "下标 " << i << " 那一位是 " << b[i] << " ← b[i] 读出来就是 0 或 1\n"; cout << "any() 有没有 1?" << (b.any() ? "有" : "没有") << " none() 是不是全 0?" << (b.none() ? "是" : "不是") << "\n\n";
cout << "flip() 整排翻转 " << (b ^ bitset<8>(string(8, '1'))) << " ← 等价于 flip()\n"; bitset<8> f = b; f.flip(); cout << "flip() 真跑一遍 " << f << "\n";
bitset<8> l = b, r = b; l <<= 1; // ← b << 1 也能用,这里为了复用变量写成 <<= r >>= 1; cout << "左移一位 <<1 " << l << "\n"; cout << "右移一位 >>1 " << r << "\n\n";
b.reset(); cout << "reset() 之后 " << b << " count() = " << b.count() << " none()?" << (b.none() ? "是" : "不是") << "\n"; return 0;}点「运行 ▶」看结果
- ★★
bitset<8> b(s):直接吃一个 0/1 字符串,打印出来就是10110100,和输入一模一样。 - ★★
b.count()数 1,b[i]读第 i 位(读出来就是 0 或 1,可以直接喂给cout)。 any()/none():有没有 1 / 是不是全 0 —— 别自己写个循环去数。flip()整排取反(带下标是翻那一位),<<= 1/>>= 1整排移位 —— 右移空出来的位补 0,所以10110100 >> 1是01011010。reset()整排清零,之后count() = 0、none()为真。
★★★ 「一个整数当开关」(第 46 章)和「一排开关」是同一套规则:那章的 x >> i & 1、x |= 1 << i,在 bitset 上就是 b[i]、b.set(i);而整排的 & | ^ << 一句就算完。
用到的写法:bitset<N> b; bitset<8> b("10110100") b[i] b.count() b.any() b.none() b.set(i) / b.reset(i) / b.flip() b <<= 1 / b & c b | c
练习(2 道)
只给题面、样例和一句提示,答案是折叠着的 —— 先自己写一版。
练习 ① array:定长数组的读入、求和与排序
读 10 个整数放进 array<int, 10>,输出长度、内容、和、平均、最小与最大,最后排好序再输出一遍。
★ 这一题想让你亲手体会:它用起来和 vector 一模一样,只是长度不能再变。
输入
7 3 9 1 4 8 2 6 0 5
输出
size() = 10 ← 编译期就是 10,不是读进来的 内容:7 3 9 1 4 8 2 6 0 5 和 = 45 平均 = 4.5 最小 = 0 最大 = 9 排好:0 1 2 3 4 5 6 7 8 9
★ 声明时别忘了 {}(陷阱 ①);a.size() 直接当循环上界 —— ⚠ 它是无符号的,和 int 比大小要么写 (int)a.size(),要么循环变量就用 size_t。
★★ array = vector 减去「改长度」:size()、[]、begin()/end()、sort、min_element 全都一样,而它就是那串数据本身。
练习 ② bitset:埃氏筛筛质数
读一个 n,用 bitset 打标记筛出所有 ≤ n 的质数:从 2 开始,把每个质数的倍数按下去,最后还是 0 的就是质数。
★ 这一题练的是「用一个下标当开关」的整套写法,以及 count() 数 1 的妙用。
n(2 ≤ n ≤ 100000 —— 上限就是 bitset<100005> 写死的那个长度)。count() 算出来的质数个数、逐个数出来的个数与质数列表(每行 10 个),最后是 flip() 之后的 count()。输入
100
输出
count() = 76 ← 被按下去的个数(0、1 也在里面) 所以质数有 (n+1) - count() = 25 个 逐个数一遍 = 25 个 质数: 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 flip() 之后 count() = 99929 = 100005 - 76(整排都翻了)
n = 100 时 count() = 76:它数的是整排里被按下去的,0 和 1 也在内 —— 所以质数是 (n+1) - count() = 25,和逐个数出来的 25 对上了。★ 外层到 i * i <= n 就够,内层从 i * i 起(更小的倍数早被按过了);⚠ sieve[0] = sieve[1] = 1; 这一句别漏 —— 0 和 1 不是质数。
★★★ bitset 的移位就是「筛」:第 23 章的 f |= f << x 和这里的逐位按下去是同一种思路(一个动整排、一个动一位)。⚠ 而 count() 永远数整排 N 位,别忘了减掉你没筛到的那部分。
用到的写法:bitset<N> b; b[i] b.count() b.set(i) / b.reset(i) / b.flip()
← 上一块:set:去重、查在不在、从小到大 · 回 STL 专题目录 · 下一块:对拍与随机数据:让 bug 自己跑出来 →
⚠ 本页说的是 STL 怎么用。算法本身(Dijkstra、单调栈、状压……)在53 章正课里,而每一章末尾都会告诉你它用到了哪些 STL。