【GESP】C++五级真题 luogu-P15799, [GESP202603 五级] 找数
2026年3月,GESP五级真题,考察快速查找(二分查找、集合或双指针),难度⭐⭐★☆☆。洛谷难度级别:普及-。
P15799 [GESP202603 五级] 找数
题目要求
题目描述
给定一个包含 $n$ 个互不相同的正整数的数组 $A$ 与一个包含 $m$ 个互不相同的正整数的数组 $B$,请你帮忙计算有多少个数在数组 $A$ 与数组 $B$ 中均出现。
输入格式
第一行包含两个整数 $n, m$。 第二行包含 $n$ 个正整数 $a_1, a_2, \dots, a_n$ 表示数组 $A$。 第三行包含 $m$ 个正整数 $b_1, b_2, \dots, b_m$ 表示数组 $B$。
输出格式
输出一个整数,表示在数组 $A$ 与数组 $B$ 中均出现的数的个数。
输入输出样例 #1
输入 #1
1
2
3
3 5
4 2 3
3 1 5 4 6
输出 #1
1
2
说明/提示
样例解释 样例 1 中,4、3 这两个数字在数组 $A$ 与 $B$ 中均出现了。
数据范围
- 对于 40% 的数据,保证 $1 \le n, m \le 1000$。
- 对于 100% 的数据,保证 $1 \le n, m \le 10^5$,$1 \le a_i, b_i \le 10^9$。
题目分析
这道题目是一道极为经典的”求两个集合交集大小”的问题。在 GESP 五级的考纲中,这是为了专门考察考生彻底摆脱”暴力双重循环”、掌握快速查找算法而设立的标准靶子题。
为什么不能暴力 $O(n^2)$? 最原始的直觉是:针对 $A$ 数组里的每一个数,我们都去 $B$ 数组里从头到尾翻找一遍看有没有。 如果 $n$ 和 $m$ 的最大规模只有 $1000$(即题目中那 40% 的测试点),$1000 \times 1000 = 100万$ 次运算,一秒内绝对能跑完。 但 100% 的数据点中,$n$ 和 $m$ 高达 $10^5$。如果还是双重循环,$10^5 \times 10^5 = 100亿$ 次运算,这就远远超出了 C++ 一秒钟能跑 1 亿次的常规限制,必定会斩获 TLE(Time Limit Exceeded,超时)。
为了拿到满分,这里提供三种达到普及组五级标准的”降维打击”解法:
解法一:排序 + 二分查找法 (Binary Search)
面对无序的寻找,我们应该先让其中一个数组”列队站好”。
- 将较长的数组(或者任意一个,假设是 $A$)使用
std::sort从小到大排序。代价是 $O(n \log n)$。 - 遍历另一个数组 $B$,对于 $B$ 中的每一个元素,使用二分查找法在排好序的 $A$ 队伍里迅速定位它是否存在。由于二分查找每次都能砍掉一半的搜索范围,查找一个数只需最多大约 $17$ 步($\log_2(100000)$)。这一步总代价是 $O(m \log n)$。
- 综合时间复杂度:极其优秀的 $O(n \log n + m \log n)$。
解法二:黑魔法 STL std::set 集合法
C++ 的标准模板库里有一个自带的高级容器——红黑树集合 std::set。
- 把数组 $A$ 的所有元素一股脑塞进
std::set<int>中。set的底层会在插入时自动为你维护好一棵平衡二叉搜索树。 - 遍历 $B$ 数组,调用内置的
.count()方法,一指神功瞬间查出 $B$ 中的数字是否在树里。
- 这种代码写起来最短,非常适合考场上极速通关。
解法三:双指针法 (Two Pointers)
- 把 $A$ 和 $B$ 分别都使用
std::sort排序。 - 设置两个排头兵指针
i和j,分别指向 $A$ 和 $B$ 的开头。 - 比较两人指着的数字:相等就双双计数并前进;如果 $A$ 的数字小,就让 $A$ 的指针
i往前走去寻找更大的数;反之亦然。就像拉拉链一样,一次就能把两个数组的交集全部摘出。
下面分别给出三种解法的示例代码。
示例代码
解法一:排序 + 二分查找法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
// 解除 cin/cout 与 stdio 的同步,加速输入输出
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int n, m;
std::cin >> n >> m;
// 分别用 vector 存放数组 A 和数组 B
std::vector<int> A(n);
std::vector<int> B(m);
// 读入数组 A
for (int i = 0; i < n; i++) {
std::cin >> A[i];
}
// 读入数组 B
for (int i = 0; i < m; i++) {
std::cin >> B[i];
}
// 第一步:将数组 A 排序,为二分查找打好基础
std::sort(A.begin(), A.end());
int ans = 0;
// 第二步:遍历数组 B 的每个元素,用二分查找去排好序的 A 里"点名"
for (int i = 0; i < m; i++) {
// binary_search 是 C++ 标准库自带的二分查找函数
// 在已排序的 A 中查找 B[i],找到返回 true
if (std::binary_search(A.begin(), A.end(), B[i])) {
ans++;
}
}
std::cout << ans << "\n";
return 0;
}
解法二:std::set 集合法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
#include <set>
int main() {
// 解除 cin/cout 与 stdio 的同步,加速输入输出
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int n, m;
std::cin >> n >> m;
// 用 set 存放数组 A 的所有元素
// set 底层是红黑树,插入和查找的时间复杂度都是 O(log n)
std::set<int> setA;
// 读入数组 A,逐个插入到 set 中
for (int i = 0; i < n; i++) {
int x;
std::cin >> x;
setA.insert(x); // 插入时 set 会自动去重并排序
}
int ans = 0;
// 读入数组 B,每读一个就去 set 里查一下有没有
for (int i = 0; i < m; i++) {
int x;
std::cin >> x;
// count() 返回元素在 set 中出现的次数(0 或 1)
if (setA.count(x)) {
ans++;
}
}
std::cout << ans << "\n";
return 0;
}
解法三:双指针法(Two Pointers)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
// 解除 cin/cout 与 stdio 的同步,加速输入输出
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int n, m;
std::cin >> n >> m;
std::vector<int> A(n);
std::vector<int> B(m);
for (int i = 0; i < n; i++) {
std::cin >> A[i];
}
for (int i = 0; i < m; i++) {
std::cin >> B[i];
}
// 第一步:把 A 和 B 都排序
std::sort(A.begin(), A.end());
std::sort(B.begin(), B.end());
int ans = 0;
int i = 0, j = 0; // 两个指针分别指向 A 和 B 的起点
// 第二步:像拉拉链一样,两个指针同时向前推进
while (i < n && j < m) {
if (A[i] == B[j]) {
// 两边相等,说明找到了一个共同元素
ans++;
i++; // 两个指针都往前走一步
j++;
} else if (A[i] < B[j]) {
// A 当前的数比 B 小,说明 A 的这个数在 B 里不存在
// 让 A 的指针往前走,去找更大的数来匹配
i++;
} else {
// B 当前的数比 A 小,同理让 B 的指针往前走
j++;
}
}
std::cout << ans << "\n";
return 0;
}
代码解析小贴士
- 解法一的核心前提——必须先排序:
std::binary_search在无序数组上会返回错误结果。二分查找的精髓在于”通过有序的大小关系,一次性砍掉一半搜索范围”,使用前必须确保数据已经sort()过。 - 解法二的取舍——代码最短,但常数较大:
std::set底层是红黑树,虽然查找复杂度也是 $O(\log n)$,但由于树节点在内存中不连续(缓存不友好),实际运行速度通常比排序+二分查找慢一些。优点是代码极短,适合考场快速通关。 - 解法三的优雅——双指针一趟扫完: 双指针法要求两个数组都排好序。排序后,两个指针各走一遍就结束了,总的时间复杂度是 $O(n \log n + m \log m)$,而且代码逻辑清晰,没有调用任何高级 API,适合加深对排序和线性扫描的理解。
- 读写加速防 TLE: 代码开头的
std::ios_base::sync_with_stdio(false); std::cin.tie(NULL);以及用"\n"代替std::endl,在数据量达到 $10^5$ 级别时几乎是必备的提速手段。
所有代码已上传至Github:https://github.com/lihongzheshuai/yummy-code
GESP 学习专题站:GESP WIKI
“luogu-”系列题目可在洛谷题库进行在线评测。
“bcqm-”系列题目可在编程启蒙题库进行在线评测。
欢迎加入:Java、C++、Python技术交流QQ群(982860385),大佬免费带队,有问必答
欢迎加入:C++ GESP/CSP认证学习QQ频道,考试资源总结汇总
欢迎加入:C++ GESP/CSP学习交流QQ群(688906745),考试认证学员交流,互帮互助
