C++ 竞赛入门
pair 二元组
把两个相关的值打包成一个对象:字段是 first 与 second,比较按字典序。
pair 二元组
读完能做什么
- 能创建
pair,用first读出第一个字段、用second读出第二个字段,并说出这两个字段的类型。 - 能用两种方式构造
pair(直接给两个值、用make_pair),并解释哪种写法更省事。 - 能预测两组
pair比较的结果,说明为什么先比first、只在first相等时才比second。 - 能说出
pair是辅助类型而不是关联容器,以及为什么按姓名存放的pair不会自动按成绩比较。
为什么需要它
成绩表里的每一条记录都由两个信息组成:姓名和分数。用两个数组也能存:
string name[3] = {"Wang", "Li", "Zhao"};
int score[3] = {92, 85, 78}; // 片段:两个数组靠下标一一对应
cout << name[1] << " " << score[1]; // 要输出第 2 条记录,两个数组都得写一次下标
这是一段片段(省略了头文件与 main)。两个数组靠下标绑定:name[1] 和 score[1] 属于同一个人。麻烦在于这个约定没有写在代码里——排序时如果只换了 score 的顺序,name 就全错位了;函数传参也得一次传两个数组,还得记得长度一样。
我们希望有一样东西,能把“姓名和分数”绑成一个整体:它可以像单个变量一样赋值、比较、放进容器,而两个字段永远待在一起。标准库为此提供了 pair。
它是什么
一句话:pair<T1, T2> 是标准库提供的辅助类型,把两个值绑成一个对象,两个字段分别叫 first 和 second。
pair 不是关联容器。 set、map 那些按“键”组织数据的容器叫关联容器;pair 只是“两个值打包”的小工具,它既不排序也不查找,就静静地装两个值。把它放在 associative-containers 这个目录下,只是因为 map、multimap 的元素类型是 pair,配套讲解比较方便。
两个字段的类型由使用者定义。 尖括号里的 T1、T2 是你填的两个类型:pair<string, int> 表示第一个字段是字符串、第二个是整数;pair<int, int> 表示两个都是整数。first 的类型就是 T1,second 的类型就是 T2。字段的含义也是使用者定义的:pair<string, int> 既可以表示“姓名 → 分数”,也可以表示“城市 → 人口”,标准库不知道也不关心。
first 与 second 是公开的成员。 它们是两个可以直接读写的变量,写法是 p.first、p.second。p.first 就是第一个字段本身,改它就是在改这个对象。
比较规则是字典序。 两个同类型的 pair 可以比较大小:先比 first,first 小的一方就小;只有在 first 相等时,才去比 second。这和字典比单词的顺序一样——先看第一个字母,第一个字母相同再看第二个。所以 pair 的默认比较不会按 second 优先。
make_pair 让编译器推断类型。 写 pair<string, int> p("Li", 85) 要把两个类型都写全;make_pair("Li", 85) 的两个类型由实参推断出来,少打字、也不容易写错类型。但“推断出来的类型”不一定就是你想要的那个:字符串字面量 "Li" 本身是 const char*,所以 make_pair("Li", 85) 推出的是 pair<const char*, int>;它能赋给 pair<string, int>,靠的是 pair 的类型转换把 const char* 转成 string。想一开始就是 string,可以写 make_pair(string("Li"), 85),或者直接用显式类型 pair<string, int>。
适用场景:需要把两个值当成一条记录传递、排序或放进容器时。map、multimap 的元素就是 pair,所以这一页也是它们的预备知识。使用时要 #include <utility>,本页示例用 #include <bits/stdc++.h> 一次包含全部标准头文件(这两句的解释在什么是 STL)。
最小可运行示例
一条成绩记录,数据写在代码里,不需要输入:
#include <bits/stdc++.h>
using namespace std;
int main() {
pair<string, int> p("Li", 85); // first 是 string,second 是 int
cout << "first = " << p.first << '\n';
cout << "second = " << p.second << '\n';
cout << "一起输出:" << p.first << " " << p.second << '\n';
p.second = 90; // 两个字段都能单独改写
cout << "改 second 后:" << p.first << " " << p.second << '\n';
return 0;
}
输出:
first = Li
second = 85
一起输出:Li 85
改 second 后:Li 90
逐行看:
pair<string, int> p("Li", 85);创建一个pair,两个类型写在尖括号里,两个值写在圆括号里,按顺序分别初始化first和second。p.first是string,值是Li;p.second是int,值是 85。- 想“一起输出”就分别取两个字段拼起来,
pair本身不能直接cout(它没有定义输出方式)。 p.second = 90;只改第二个字段,first不受影响。
一步步看变化
先看一个 pair 的字段是怎么被读和写的:
| 步骤 | 操作 | p.first |
p.second |
输出行 |
|---|---|---|---|---|
| 0 | pair<string,int> p("Li", 85) |
Li |
85 | — |
| 1 | cout << p.first |
Li |
85 | first = Li |
| 2 | cout << p.second |
Li |
85 | second = 85 |
| 3 | cout << p.first << " " << p.second |
Li |
85 | 一起输出:Li 85 |
| 4 | p.second = 90 |
Li |
90 | 改 second 后:Li 90 |
再看比较规则。三个数值对 (2,5)、(2,3)、(1,9) 的比较结果如下:
| 左 | 右 | 先比 first |
结论 | 结果 |
|---|---|---|---|---|
(2,5) |
(2,3) |
2 == 2,相等,继续比 second |
5 < 3 为假 | (2,5) < (2,3) 是 0 |
(1,9) |
(2,5) |
1 < 2,已经分出胜负 | 不再看 second |
(1,9) < (2,5) 是 1 |
(2,5) |
(2,3) |
— | first 相等、second 不等 |
(2,5) == (2,3) 是 0 |
对应的完整程序:
#include <bits/stdc++.h>
using namespace std;
int main() {
pair<int, int> a(2, 5);
pair<int, int> b(2, 3);
pair<int, int> c(1, 9);
cout << "a(2,5) < b(2,3)? " << (a < b) << '\n'; // 第一项相同,比第二项
cout << "c(1,9) < a(2,5)? " << (c < a) << '\n'; // 第一项 1 < 2,第二项不比
cout << "a == b? " << (a == b) << '\n';
return 0;
}
输出:
a(2,5) < b(2,3)? 0
c(1,9) < a(2,5)? 1
a == b? 0
a(2,5) < b(2,3)为假(打印 0):first都是 2,所以继续比second,而 5 < 3 不成立。c(1,9) < a(2,5)为真(打印 1):first是 1 和 2,1 < 2 已经分出胜负,9 比 5 大也改变不了结果——这就是“先比第一项”的意思。a == b为假:first相等但second是 5 和 3,不相等,所以整体不相等。
这三点合起来说明:(1,9) 排在 (2,3) 前面,尽管它的第二项更大。想让第二项起决定作用,得自己写比较规则,见比较规则与函数对象。
逐项扩展核心操作
三种创建方式
#include <bits/stdc++.h>
using namespace std;
int main() {
pair<string, int> a("Li", 85); // 直接给两个值
pair<string, int> b = make_pair("Wang", 92); // 让编译器推断类型
pair<string, int> c; // 默认构造
c.first = "Zhao";
c.second = 78;
cout << a.first << " " << a.second << '\n';
cout << b.first << " " << b.second << '\n';
cout << c.first << " " << c.second << '\n';
return 0;
}
输出:
Li 85
Wang 92
Zhao 78
| 写法 | 形式 | 说明 |
|---|---|---|
| 直接构造 | pair<string, int> a("Li", 85); |
两个类型、两个值都要写出来 |
make_pair |
pair<string, int> b = make_pair("Wang", 92); |
两个值的类型由编译器推断 |
| 默认构造后赋值 | pair<string, int> c; 再写 c.first = ... |
两个字段先被默认初始化,再逐个填 |
三种写法得到的都是 pair<string, int> 类型的对象,用哪种都能跑(表里打印出的值不同,只是因为各给了不同的数据)。make_pair 的好处是不用在两个地方重复写类型;默认构造的写法适合“先声明、后面从输入里填”的场合,比如 cin >> p.first >> p.second;。
注意 pair<string, int> c; 之后 c.first 是空串、c.second 是 0——两个字段都被默认初始化了,这是有确定结果的行为(数字类型默认是 0,string 默认是空串),可以直接赋值覆盖。
拷贝、相等与比较
#include <bits/stdc++.h>
using namespace std;
int main() {
pair<string, int> p("Li", 85);
pair<string, int> copy1 = p; // 拷贝:两个字段都复制
cout << "copy1 = " << copy1.first << " " << copy1.second << '\n';
cout << "p 和 copy1 相等?" << (p == copy1) << '\n';
copy1.second = 60; // 改副本不影响原来的 p
cout << "改副本后 copy1 = " << copy1.first << " " << copy1.second << '\n';
cout << "p 还是 " << p.first << " " << p.second << '\n';
pair<string, int> other("Li", 90);
cout << "p < other? " << (p < other) << '\n'; // first 相同,85 < 90
return 0;
}
输出:
copy1 = Li 85
p 和 copy1 相等?1
改副本后 copy1 = Li 60
p 还是 Li 85
p < other? 1
| 操作 | 规则 | 输出里的证据 |
|---|---|---|
copy1 = p |
两个字段分别复制 | copy1 = Li 85,与 p 相等所以打印 1 |
copy1.second = 60 |
改的是副本 | p 仍然是 Li 85,没被带着改 |
p == copy1 |
first、second 都相等才为真 |
改副本后两者不再相等 |
p < other |
先比 first,Li == Li,再比 second,85 < 90 |
打印 1 |
= 是按值复制,复制出来的 pair 和原来的各自独立,这一点和 int、string 一样。== 要求两个字段都相等,< 按前面讲的字典序规则。
放进容器:按姓名排序
pair 经常成批出现,这时就把它放进 vector 动态数组:
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<pair<string, int> > score;
score.push_back(make_pair("Wang", 92));
score.push_back(make_pair("Li", 85));
score.push_back(make_pair("Zhao", 78));
cout << "按保存顺序输出:\n";
for (int i = 0; i < (int)score.size(); i++) {
cout << score[i].first << " " << score[i].second << '\n';
}
sort(score.begin(), score.end()); // pair 自带比较规则:先比 first
cout << "排序后输出:\n";
for (int i = 0; i < (int)score.size(); i++) {
cout << score[i].first << " " << score[i].second << '\n';
}
return 0;
}
输出:
按保存顺序输出:
Wang 92
Li 85
Zhao 78
排序后输出:
Li 85
Wang 92
Zhao 78
vector<pair<string, int> >表示每个格子放一个pair,注意最右边的两个>之间要有空格(C++17 里其实不加空格也能编过,但旧写法习惯留一个空格)。score[i]是一个pair,所以score[i].first是它的第一个字段、score[i].second是第二个字段。sort(详见 sort 排序)默认用<比较元素,而pair的<就是前面讲的字典序,所以排序后按姓名从小到大排列:Li、Wang、Zhao。分数92、85、78完全没有参与比较,排序结果也不是按分数。
这是最容易误解的一点:这里得到的是“按姓名排序”,不是“按成绩排序”。想让 pair 按 second(成绩)比较,必须自己写比较规则传给 sort,写法见比较规则与函数对象。
什么时候选它、哪里容易错
字段类型和含义必须由使用者定清楚。 pair<string, int> 的 first 是姓名还是学号、second 是成绩还是年龄,标准库一概不知。拿到一个 pair 变量时,先看它的定义,明确两个字段各代表什么,否则很容易把 p.first 和 p.second 用反。
默认比较是 first 优先,不是 second 优先。 前面 (1,9) 排在 (2,3) 前面就是这个规则的直接后果。想按成绩排序,要么自己写比较规则,要么干脆把成绩放 first——但这会让“姓名和成绩谁在前”变得别扭,所以更常见的是写比较规则。
pair 不是关联容器。 它不做排序、不去重、不能按键查找,只是打包两个值。真正的关联容器是 set 有序集合 和 map 键值映射。map 的元素类型就是 pair<const Key, T>,所以先学会 first / second 再读 map 会顺很多。
pair 只有两个字段。 需要三个及以上就换成 struct(见结构体),或者嵌套 pair——但嵌套会带来 p.first.second 这种难读的写法,不如自定义结构体清楚。
别把两个字段的初始化顺序写反。 pair<string, int> p("Li", 85) 里第一个参数给 first,第二个给 second。如果类型不一致却写反了,编译器会直接报错(85 放不进 string);如果两个字段类型相同,写反了不会报错,程序照样运行,只是数据颠倒了——这类错误只能靠检查数据来源发现。
操作速查表
| 操作 | 作用 | 备注 |
|---|---|---|
pair<T1,T2> p; |
创建对象 | 两个字段被默认初始化 |
pair<T1,T2> p(v1,v2); |
用两个值创建 | v1 给 first,v2 给 second |
make_pair(v1,v2) |
让编译器推断类型创建 | 类型由实参决定:make_pair("Li", 85) 推出 pair<const char*, int>,赋给 pair<string, int> 时靠类型转换 |
p.first |
第一个字段 | 类型是 T1,可读可写 |
p.second |
第二个字段 | 类型是 T2,可读可写 |
p = q |
整体赋值 | 两个字段一起复制 |
p == q |
两个字段都相等 | 打印成 0/1 |
p < q |
字典序:先 first,相等再 second |
>、<= 等按同一规则 |
vector<pair<T1,T2> > v; |
保存一批记录 | 每个元素是一个 pair |
动手检查
第 1 题:预测输出
下面是一段片段(省略了头文件和 main),请先手推出输出,再运行核对:
pair<int, int> x(3, 7);
pair<int, int> y(3, 2);
pair<int, int> z(2, 9);
cout << (x < y) << ' ' << (z < x) << ' ' << (x == y) << '\n';
第 2 题:修改代码
下面这段片段只改了第一个字段,请把第二个字段改成 60,并输出“姓名 成绩”两个字段,让输出变成 Li 60:
pair<string, int> p("Li", 85);
p.first = "Li"; // 只改了第一个字段
cout << p.first << ' ' << p.second << '\n';
验证条件:先说出改完之后 p.first 和 p.second 各是什么,再运行核对。想一想:只改一个字段时,另一个字段会不会被带着变?
参考结果与解释
第 1 题:输出是 0 1 0。逐个推:
| 比较 | 先比 first |
再比 second |
结果 |
|---|---|---|---|
x(3,7) < y(3,2) |
3 == 3,相等,继续 | 7 < 2 为假 | 0 |
z(2,9) < x(3,7) |
2 < 3 为真,已分胜负 | 不再比 | 1 |
x(3,7) == y(3,2) |
first 相等 |
second 是 7 和 2,不相等 |
0 |
z(2,9) 的 first 是 2,比 x 的 3 小,所以 z < x 为真——它的 second 是 9,比 7 大,但根本没轮到比较。完整程序与实测输出:
#include <bits/stdc++.h>
using namespace std;
int main() {
pair<int, int> x(3, 7);
pair<int, int> y(3, 2);
pair<int, int> z(2, 9);
cout << (x < y) << ' ' << (z < x) << ' ' << (x == y) << '\n';
return 0;
}
输出:
0 1 0
z(2,9) 的 first 是 2,比 x 的 3 小,所以 z < x 为真——它的 second 是 9,比 7 大,但根本没轮到比较。
第 2 题:把 p.second 改成 60 即可。p.first 保持 Li 不变。
#include <bits/stdc++.h>
using namespace std;
int main() {
pair<string, int> p("Li", 85);
p.second = 60; // 改第二个字段
cout << p.first << ' ' << p.second << '\n';
return 0;
}
输出:
Li 60
如果只改了 first(比如把姓名换成别人),second 还是原来的 85,输出就会是“新姓名 85”,成绩并没有被改到。两个字段各改各的,first 和 second 互不影响。
立即练习
本页不安排 OJ 题目:pair 单独出题的入门题很少,而“一条记录有多个字段”的常见题目(例如洛谷 P1093 奖学金)需要按 (总分, 语文, 学号) 三个键排序,要先学会第 12 篇比较规则与函数对象 里的自定义比较规则才能做对。本页先用上面的动手检查把 first / second 与字典序比较练熟;等学完比较规则再回头做这类题会顺得多。