课程 · C++ 速查 · STL 专题 · 第 10 块

bitset / array / pair:三个顺手的小件

学会三个「个数写死」的现成小件:pair 自带比较规则、array 长度写死、bitset 当一排开关。

第 10 块(共 12 块已写好)13 条动作3 处陷阱2 道例题2 道练习4 份可运行代码

读这一页之前,先知道读完要能做什么

  • 写出来用 pair<int,int> 排序,并说清「要按哪个关键字排,就把它放进 first」
  • 说清 array<int, N> 和 vector 的差别:长度编译期写死,而且不替你清零
  • 用 bitset<N> 的下标、count()、flip() 和移位,并说清「下标 0 在最右边」

这是什么

5 段

一句话: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。

完整动作表

13 条

复杂度写的是这个动作自己的代价,不是整个程序的。「这一栏最容易错的地方」那一列是重点。

写法干什么代价
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 == qpair 自带比较:先 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;」是同一个病。
g++ -Wall -O2 说(实测,就这几行)
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`

pair 的比较规则是写死的(先 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 个字符」的直觉去取位,拿到的整排是左右颠倒的 —— 按位还原、拼二进制数、判第几位全错,而程序不报错。
实测输出:字符串开头是 `1`,可 `b[0]` 是 `0`
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];
★★ 记法:下标是「从右往左数的位号」(0 是最低位),不是「字符串里的位置」。要按字符串的样子处理,用 to_string()。

本书哪一章真的用到它

4 处

⚠ 下表里的文件路径和那串 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) 排序,一个比较器都不用写

pair-sort.cpp

n 个点,每个点给两个整数。把它们按 x 从小到大排(x 相同按 y),再验证 pair 自带的三条比较,最后演示「按 y 排」该怎么改。

★ 这一题想证明的是:「按 x 排、x 相同按 y 排」这句话,sort 是白送的。

输入格式第一行 n。之后 n 行,每行两个整数 x y。
输出格式排好的点列、三条 pair 比较的结果,以及按 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。
pair-sort.cpp参考代码
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. ★★ 读进来就存成 pair:v.push_back({x, y}) —— 花括号就是 pair 的字面量。
  2. 一句 sort(v.begin(), v.end()),什么都不用传:它按 first 排,first 相同再按 second —— 正好是题面的规矩。
  3. ★★ 三条比较直接看:(1,2) < (1,3) 是(first 相同,轮到 second);(1,2) < (0,9) 否(先比 first,1 > 0)。
  4. 按 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、翻转和移位

bitset-bit.cpp

读一个 8 位的 0/1 串和一个下标 i,构造 bitset<8>,依次演示 count()、按下标读、any()/none()、flip()、左右移和 reset()。

★ 一句话:bitset 就是「长度写死的一排开关」 —— 动作和第 46 章对单个整数做位运算一模一样,只是整排一起做。

输入格式第一行:8 个 0/1 字符(无空格)。第二行:一个整数 i(0~7)。
输出格式构造出来的位模式、1 的个数、指定位的值、翻转与移位之后的样子,以及清零后的状态。

输入

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 在最右边,别按字符串位置去数(陷阱 ③)。
bitset-bit.cpp参考代码
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. ★★ bitset<8> b(s):直接吃一个 0/1 字符串,打印出来就是 10110100,和输入一模一样。
  2. ★★ b.count() 数 1,b[i] 读第 i 位(读出来就是 0 或 1,可以直接喂给 cout)。
  3. any() / none():有没有 1 / 是不是全 0 —— 别自己写个循环去数。
  4. flip() 整排取反(带下标是翻那一位),<<= 1 / >>= 1 整排移位 —— 右移空出来的位补 0,所以 10110100 >> 1 是 01011010。
  5. 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:定长数组的读入、求和与排序

array-fixed.cpp

读 10 个整数放进 array<int, 10>,输出长度、内容、和、平均、最小与最大,最后排好序再输出一遍。

★ 这一题想让你亲手体会:它用起来和 vector 一模一样,只是长度不能再变。

输入格式一行 10 个整数。
输出格式长度、原始内容、和与平均、最小与最大、排好序的内容。

输入

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-fixed.cpp参考答案
记住这一句

★★ array = vector 减去「改长度」:size()、[]、begin()/end()、sort、min_element 全都一样,而它就是那串数据本身。

用到的写法:array<int, N> a{}; a.size() a[i] / a.begin() a.end()

练习 ② bitset:埃氏筛筛质数

bitset-sieve.cpp

读一个 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-sieve.cpp参考答案
记住这一句

★★★ 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。