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 与字典序比较练熟;等学完比较规则再回头做这类题会顺得多。