STL set、map
STL set
例题:P5250 【深基17.例5】木材仓库
分析:这个问题可以抽象为:维护一个集合,可以插入一个元素 \(x\),同时判断 \(x\) 是否已经存在;查询 \(x\) 的前驱后继,\(x\) 的前驱定义为小于 \(x\) 的最大的数,\(x\) 的后继定义为大于 \(x\) 的最小的数;可以删除指定元素
STL 中的 set 可以很方便地解决这个问题,set 的本质是红黑树(一种比较优秀的平衡二叉查找树)
使用 set 需要通过 #include <set> 引入相应头文件,其常用方法如下:
set<int> s创建一个名字叫做 s 的、元素类型为 int 的集合s.insert(x)在集合中插入元素 \(x\),如果这个数已经存在,则什么都不干,时间复杂度为 \(O(\log n)\)s.erase(x)在集合中删除元素 \(x\),如果这个数不存在,则什么都不干,时间复杂度为 \(O(\log n)\)s.erase(it)删除集合中迭代器 it 指向的元素,时间复杂度为 \(O(\log n)\)s.end()返回集合中最后一个元素的下一个元素的迭代器,很少直接使用,通常配合其他方法进行比较,以确认某个元素是否存在s.find(x)如果 \(x\) 不在集合中,返回 s.end(),否则返回指向元素 \(x\) 的迭代器s.lower_bound(x)查询大于等于 \(x\) 的最小的元素在集合中对应的迭代器,如果没有这样的元素,返回 s.end()s.uppper_bound(x)查询大于 \(x\) 的最小的元素在集合中对应的迭代器,如果没有这样的元素,返回 s.end()s.empty()如果集合是空的,返回 true,否则返回 falses.size()返回集合中元素的个数
迭代器: set 中的迭代器是一种“双向访问迭代器”,不支持“随机访问”,支持星号“*”解引用(即获取迭代器对应的元素值),仅支持“++”和“--”这两个与算术相关的操作。
设 it 是一个迭代器,例如 set<int>::iterator it;
若把 it++,则 it 将会指向“下一个”元素。这里的“下一个”是指在元素按从小到大的顺序中,排在 it 下一名的元素。同理,若把 it--,则 it 会指向排在“上一个”的元素。执行操作前后,务必仔细检查,避免迭代器指向的位置超出首、尾迭代器的范围。
本题中进货操作可以直接在集合中用 insert(),查询操作可以用 lower_bound() 操作实现,出货删除操作可以使用 erase() 实现,lower_bound 返回的是仓库中大于等于要求长度的最短的木棍,所以还需要和比这根还短一些的那根木棍来比较一下,看看哪根木棍离要求的木棍长度更接近。
参考代码
#include <cstdio>
#include <set>
using namespace std;
int main()
{
set<int> s;
int m; scanf("%d", &m);
while (m--) {
int op, len; scanf("%d%d", &op, &len);
if (op == 1) {
if (s.find(len) != s.end()) printf("Already Exist\n");
else s.insert(len);
} else {
if (s.size() == 0) {
printf("Empty\n"); continue;
}
auto iter = s.lower_bound(len); // 这里的iter实际上是set<int>::iterator类型
// 迭代器类似于指针 *iter 解引用
if (iter == s.end()) { // 说明最大的都没有len大
iter--; printf("%d\n", *iter); s.erase(iter);
} else if (iter == s.begin()) { // 说明最小的都大于等于len,等于说没有前一个元素了
printf("%d\n", *iter); s.erase(iter);
} else {
auto tmp = iter; tmp--; // 因此如果iter是s.begin(),没法--
if (len - *tmp <= *iter - len) {
printf("%d\n", *tmp); s.erase(tmp);
} else {
printf("%d\n", *iter); s.erase(iter);
}
}
}
}
return 0;
}
例题:P10466 邻值查找
类似于上一题 P5250 【深基17.例5】木材仓库,用 set 快速查询最接近的。
参考代码
#include <cstdio>
#include <utility>
#include <set>
// 使用 using 简化代码
using namespace std;
using pi = pair<int, int>; // 定义 pi 为 pair<int, int> 的别名
int main()
{
int n;
scanf("%d", &n);
// 使用 std::set 来维护一个有序的集合。set 底层是平衡二叉搜索树。
// 存储 pair<int, int>,分别代表 {数值, 原始下标}。
set<pi> s;
// 循环处理 n 个输入的数
for (int i = 1; i <= n; i++) {
int x;
scanf("%d", &x);
if (i == 1) { // 第一个数没有前驱,直接加入集合
s.insert({x, i});
} else {
// 对于后续的数,在集合 s 中查找其邻值
// 1. 查找后继 (Successor)
// lower_bound 返回第一个不小于 {x, i} 的元素的迭代器。
// 这就是 x 在集合 s 中的后继(或 x 本身,但题目保证元素不同)。
auto ge = s.lower_bound({x, i});
// 2. 查找前驱 (Predecessor)
// 前驱是后继的前一个元素。
// 3. 分情况讨论并比较
if (ge == s.begin()) {
// 情况A: x 比集合中所有数都小,没有前驱。
// 最近的邻值一定是最小的那个元素,即后继 *ge。
printf("%d %d\n", ge->first - x, ge->second);
} else {
// 情况B: x 不是最小的,存在前驱。
auto lt = prev(ge); // lt 指向 x 的前驱
if (ge == s.end() || x - lt->first <= ge->first - x) {
// ge == s.end(): x 比集合中所有数都大,没有后继。
// 最近的邻值一定是最大的那个元素,即前驱 *lt。
// x - lt->first <= ge->first - x: 比较与前驱和后继的差值。
// 如果与前驱更近,或者距离相等,则选择前驱。
// (距离相等时选值小的,即前驱,题目要求)
printf("%d %d\n", x - lt->first, lt->second);
} else {
// 与后继更近
printf("%d %d\n", ge->first - x, ge->second);
}
}
// 处理完当前数的查询后,将其插入集合,供后续的数查询
s.insert({x, i});
}
}
return 0;
}
本题还有一种离线处理的思路,离线算法不按输入顺序处理查询,而是将所有信息读入后,按一种更方便的顺序进行处理,最后再按顺序要求输出答案。
首先,读入整个序列 \(A\),但先不做处理。创建一个索引数组 \(\text{idx}\),并通过 \(\text{idx}\) 对整个序列 \(A\) 按数值大小进行排序。排序后,\(\text{idx}\) 数组就存储了原序列所有元素的下标,且这些下标是按其对应的值从小到大排列的。
基于这个排序结果,可以构建一个双向链表,链表的“节点”就是原序列的下标 \(1, \dots, n\),链表的“指针” \(\text{pre}_i\) 和 \(\text{nxt}_i\) 分别指向在数值上与 \(A_i\) 相邻的前一个和后一个元素的下标。例如,如果 \(A_5\) 是 \(A_2\) 在数值上的后继,那么 \(\text{nxt}_2 = 5\) 且 \(\text{pre}_5 = 2\)。这一步的瓶颈在于排序,需要 \(O(n \log n)\) 时间。
从后往前遍历原序列,即从 \(i=n\) 循环到 \(i=2\)。
当处理下标为 \(i\) 的元素 \(A_i\) 时,如果 \(\{ A_{i+1}, \dots, A_n \}\) 都从链表中删去了,那么双向链表中剩下的所有节点,恰好就是下标在 \([1,i]\) 范围内的所有元素。
因此,\(A_i\) 在当前链表中的前驱 \(\text{pre}_i\) 和后继 \(\text{nxt}_i\),就是可能的邻近元素,只要每次处理完 \(A_i\) 之后顺便把 \(A_i\) 从链表中删去。
当逆序循环结束后,已经计算并存储了所有 \(i=2, \dots, n\) 的答案。最后,再从 \(i=2\) 到 \(n\) 循环一次,按顺序输出结果。
参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1e5 + 5;
// --- 全局变量 ---
int a[N]; // 存储输入的序列值
int idx[N]; // 存储索引,用于间接排序
int pre[N]; // pre[i] 表示值 a[i] 在全局排序中的前驱节点的索引
int nxt[N]; // nxt[i] 表示值 a[i] 在全局排序中的后继节点的索引
int ans[N]; // ans[i] 存储 a[i] 的邻值的索引
int main()
{
int n; scanf("%d", &n);
for (int i = 1; i <= n; i++) {
idx[i] = i; // 初始化索引数组
scanf("%d", &a[i]);
}
// --- 1. 排序 ---
// 不直接对 a 数组排序,而是对索引数组 idx 进行排序。
// 排序的依据是索引对应的 a 数组中的值。
// 排序后, idx[k] 就是第 k 小的数在原序列 a 中的下标。
sort(idx + 1, idx + n + 1, [](int i, int j) {
return a[i] < a[j];
});
// --- 2. 建立双向链表 ---
// 根据排序后的 idx 数组,建立一个以原下标为节点,按值排序的双向链表。
// 0 和 n+1 作为链表的哨兵节点(头和尾)。
pre[idx[1]] = 0; nxt[0] = idx[1];
for (int i = 2; i <= n; i++) {
pre[idx[i]] = idx[i - 1];
nxt[idx[i - 1]] = idx[i];
}
pre[n + 1] = idx[n]; nxt[idx[n]] = n + 1;
// --- 3. 逆序处理 ---
// 从 i = n 到 1 倒序遍历,计算每个 a[i] 的答案。
// 这种离线处理方式保证了在计算 a[i] 时,链表中只包含下标 <= i 的节点。
for (int i = n; i >= 2; i--) {
// 在当前链表中,a[i] 的邻值候选就是它的前驱和后继。
if (nxt[i] == n + 1) { // 没有后继,邻居只能是前驱
ans[i] = pre[i];
} else if (pre[i] == 0) { // 没有前驱,邻居只能是后继
ans[i] = nxt[i];
} else {
// 同时存在前驱和后继,需要比较距离
int l = pre[i], r = nxt[i];
// 如果与前驱的距离更小或相等,根据题意(相等时选值小的)选择前驱
if (a[i] - a[l] <= a[r] - a[i]) {
ans[i] = l;
} else {
ans[i] = r;
}
}
// 计算完 a[i] 的答案后,将其从链表中“删除”。
// 这是一个 O(1) 的操作,使得下一次循环 (i-1) 看不到 i。
pre[nxt[i]] = pre[i];
nxt[pre[i]] = nxt[i];
}
// --- 4. 按顺序输出答案 ---
for (int i = 2; i <= n; i++) {
printf("%d %d\n", a[ans[i]] >= a[i] ? a[ans[i]] - a[i] : a[i] - a[ans[i]], ans[i]);
}
return 0;
}
习题:P2286 [HNOI2004] 宠物收养场
解题思路
题意涉及到找出一个序列中与 \(x\) 差值最小的数,而且数据保证读入的数值不重复,可以用 set 维护特点值。
同一时间在收养所中的要么全是宠物,要么全是领养者。如果读入的是宠物,当前没有领养者,则向维护宠物特点值的 set 中插入,反之则肯定有领养者可以和宠物配对。接下来用 lower_bound 找出最接近特点值的元素,选择大于等于的最小值和小于的最大值中更优的那一个配对(注意特判边界情况),更新答案,并删去对应元素。读入领养者和读入宠物没有本质区别,因此可以将配对过程抽成函数,减少代码量。
参考代码
#include <cstdio>
#include <set>
using namespace std;
typedef long long LL;
const int MOD = 1000000;
void find_closest(set<int>& s, int key, int& ans) {
auto iter = s.lower_bound(key);
if (iter == s.end()) {
--iter; ans += (key - *iter); ans %= MOD; s.erase(iter);
} else if (iter == s.begin()) {
ans += (*iter - key); ans %= MOD; s.erase(iter);
} else {
auto tmp = iter; tmp--;
if (key - *tmp <= *iter - key) {
ans += (key - *tmp); ans %= MOD; s.erase(tmp);
} else {
ans += (*iter - key); ans %= MOD; s.erase(iter);
}
}
}
int main()
{
int n;
scanf("%d", &n);
set<int> pet, adopter;
int ans = 0;
while (n--) {
int a, b; scanf("%d%d", &a, &b);
if (a == 0) {
if (adopter.size() > 0) find_closest(adopter, b, ans);
else pet.insert(b);
} else {
if (pet.size() > 0) find_closest(pet, b, ans);
else adopter.insert(b);
}
}
printf("%d\n", ans);
return 0;
}
STL multiset
multiset 是一个允许元素重复的、自动排序的集合容器,同 set 一样,它的底层也通常用红黑树实现。
#include <iostream>
#include <set>
#include <string>
using namespace std;
// 辅助函数,用于打印 multiset 的状态
void print_multiset_status(multiset<int>& ms, const string& message) {
cout << "--- " << message << " ---\n";
if (ms.empty()) {
cout << "Multiset is empty.\n\n";
return;
}
cout << "Content: { ";
for (int val : ms) {
cout << val << " ";
}
cout << "}\n";
cout << "Smallest element (begin): " << *ms.begin() << "\n\n";
}
int main()
{
// 1. 创建 multiset
multiset<int> ms;
print_multiset_status(ms, "Initial state");
// 2. 插入 (insert)
ms.insert(40);
ms.insert(20);
ms.insert(50);
ms.insert(20); // 插入重复值
ms.insert(40);
print_multiset_status(ms, "After inserting {40, 20, 50, 20, 40}");
// 3. 查找 (find)
cout << "--- Finding elements ---\n";
auto it_20 = ms.find(20);
if (it_20 != ms.end()) {
cout << "Found '20'. Iterator points to: " << *it_20 << "\n";
}
auto it_99 = ms.find(99);
if (it_99 == ms.end()) {
cout << "Did not find '99', find() returned end().\n";
}
cout << "\n";
// 4. 删除 (erase)
// 4.1 按迭代器删除一个 '20'
ms.erase(it_20);
print_multiset_status(ms, "After erasing ONE '20' using an iterator");
// 4.2 按值删除所有 '40'
ms.erase(40);
print_multiset_status(ms, "After erasing ALL '40's by value");
// 5. lower_bound
ms.insert(30);
ms.insert(30);
print_multiset_status(ms, "After inserting two '30's");
cout << "--- Using lower_bound ---\n";
auto lb = ms.lower_bound(25); // 查找第一个 >= 25 的元素
if (lb != ms.end()) {
cout << "The first element not less than 25 is: " << *lb << "\n";
}
cout << "\n";
// 6. 清空 (clear)
cout << "--- Clearing the multiset ---\n";
ms.clear();
print_multiset_status(ms, "After calling clear()");
return 0;
}
习题:CF1889C1 Doremy's Drying Plan (Easy Version)
解题思路
首先考虑暴力做法。
对于每一个位置 \(i=1,2,3,...,n\) 可以通过差分与前缀和预处理每个位置被线段覆盖的次数。首先,对于每个覆盖次数为 \(0\) 的位置,可以放到最后再考虑,因为这部分点的数量与移除的两条线段无关,设这部分点的数量为 \(A\),再设移除两条线段后产生的新的不被覆盖的点的数量是 \(B\),则本题的答案为 \(A + \max B\)。
要计算 \(B\),考虑每一对线段 \(I_1, I_2\):
- 如果两者不交叉,\(B\) 等于 \(I_1\) 和 \(I_2\) 中刚好覆盖一次的点的数量之和;
- 如果两者交叉,假设两者交叉的线段为 \(J\),则 \(B\) 等于\(I_1\) 和 \(I_2\) 中刚好覆盖一次的点的数量之和再加上 \(J\) 中恰好覆盖两次的点的数量。
如果直接枚举每一对线段,则时间复杂度为 \(O(n + m^2)\),考虑优化这个算法:
- 对于两线段不交叉的情况,实际上直接从所有线段中挑出覆盖一次的点的数量最多的两条即可;
- 对于两线段交叉的情况,实际上真正有用的最多只有 \(n\) 对线段:对于每一个位置 \(i\),如果它恰好被覆盖两次,此时就考虑覆盖这个点的两条线段即可。
对于某一个点,如何维护覆盖它的线段?这里我们可以把每个线段 \([l,r]\) 看成是在 \(l\) 位置开始计算时引入,在 \(r\) 位置完成计算后移除,可以利用一个 multiset 来维护当前涉及到的线段,这样一来时间复杂度优化到 \(O(n+m \log m)\)。
参考代码
#include <cstdio>
#include <vector>
#include <set>
#include <algorithm>
using namespace std;
const int N = 200005;
// left[i]: 存储所有左端点为 i 的区间的右端点
// right[i]: 存储所有右端点为 i 的区间的左端点
vector<int> left[N], right[N];
// diff: 差分数组
// cnt[i]: 城市 i 被覆盖的次数
// sum1[i]: cnt值为1的城市数量的前缀和
// sum2[i]: cnt值为2的城市数量的前缀和
int diff[N], cnt[N], sum1[N], sum2[N];
// s: 存储当前活跃的区间 {l, r}
multiset<pair<int, int>> s;
int main()
{
int t; scanf("%d", &t);
while (t--) {
int n, m, k; scanf("%d%d%d", &n, &m, &k); // k=2 in this version
// --- 初始化 ---
for (int i = 1; i <= n + 1; i++) {
left[i].clear(); right[i].clear();
diff[i] = 0;
}
// --- 读入区间并构建差分数组 ---
for (int i = 1; i <= m; i++) {
int l, r; scanf("%d%d", &l, &r);
left[l].push_back(r); right[r].push_back(l);
diff[l]++; diff[r + 1]--;
}
// --- 计算每个城市的覆盖次数 cnt 和前缀和 sum1, sum2 ---
for (int i = 1; i <= n; i++) cnt[i] = cnt[i - 1] + diff[i];
for (int i = 1; i <= n; i++) {
sum1[i] = sum1[i - 1]; sum2[i] = sum2[i - 1];
if (cnt[i] == 1) sum1[i]++;
else if (cnt[i] == 2) sum2[i]++;
}
// --- 策略一:取消两个不相交的区间 ---
// 目标是找到两个区间,它们各自解放的 cnt=1 的城市数之和最大
int max1 = 0, max2 = 0; // 记录解放 cnt=1 城市数最多的和次多的
for (int i = 1; i <= n; i++)
for (int x : left[i]) { // 遍历每个区间 [i, x]
int cur = sum1[x] - sum1[i - 1];
if (cur > max1) {
max2 = max1; max1 = cur;
} else if (cur > max2) max2 = cur;
}
int ans = max1 + max2; // 策略一的候选答案
// --- 策略二:取消两个可能相交的区间 ---
s.clear();
for (int i = 1; i <= n; i++) {
// 将所有以 i 为左端点的区间加入活跃集合 s
for (int x : left[i]) s.insert({i, x});
// 如果当前城市 i 被恰好两个区间覆盖
if (cnt[i] == 2) {
// 这两个区间一定是活跃集合 s 中左端点最小的两个
auto it = s.begin();
int l1 = it->first, r1 = it->second;
it++;
int l2 = it->first, r2 = it->second;
// a,b,c,d 是 l1,r1,l2,r2 排序后的结果
// [a,d] 是并集,[b,c] 是交集
int points[] = {l1, r1, l2, r2};
sort(points, points + 4);
int a = points[0], b = points[1], c = points[2], d = points[3];
// 计算收益:(并集内的cnt=1城市数) + (交集内的cnt=2城市数)
// sum1[d] - sum1[a-1] 是并集内的cnt=1城市数
// sum2[c] - sum2[b-1] 是交集内的cnt=2城市数
ans = max(ans, sum1[d] - sum1[a - 1] + sum2[c] - sum2[b - 1]);
}
// 将所有以 i 为右端点的区间移出活跃集合 s
for (int x : right[i]) s.erase(s.find({x, i}));
}
// --- 最终结果 ---
// 加上原本就干燥的城市 (cnt[i]==0)
for (int i = 1; i <= n; i++) if (cnt[i] == 0) ans++;
printf("%d\n", ans);
}
return 0;
}
习题:P1110 [ZJOI2007] 报表统计
解题思路
题目描述的 INSERT i k 是在第 i 个原始元素相关的块之后插入。因为只有块头和块尾会影响到相邻元素的差值,可以使用两个数组 a 和 b,a 存储初始序列(块头),b 数组则用来存储每个块的最后一个元素。当执行 INSERT i k 时,实际上是将 k 插入到 b[i] 和 a[i+1] 之间,然后 k 成为了这个块新的最后一个元素,所以更新 b[i] = k 即可。
MIN_GAP 是序列中所有相邻元素差的绝对值中的最小值。当执行 INSERT i k 时,序列的变化是:... a[i], ..., b[i], a[i+1], ... 变成了 ... a[i], ..., b[i], k, a[i+1], ...。因此,旧的差值 |b[i] - a[i+1]| 需要被移除。两个新的差值 |b[i] - k| 和 k - a[i+1] 需要被加入。可以用 multiset 存储所有这些差值,并快速提供最小值。
MIN_SORT_GAP 是所有数值中最接近的两个数的差。这个值与元素在序列中的位置无关。当一个新值 k 被加入序列时,它可能会创造一个新的最小差值,这个新差值只会出现在 k 和它在所有数值的有序排列中的前驱与后继之间。可以用 multiset 维护序列中所有数字的有序集合。用一个整型变量存储当前已知的最小值。在程序开始时,完整计算一次初始的 MIN_SORT_GAP。当 INSERT i k 发生时,检查 k 与其前驱和后继形成的差值,用以更新最小值,再将 k 加入到存储所有序列数值的 multiset 中。
参考代码
#include <iostream>
#include <set>
#include <cmath>
#include <algorithm>
#include <string>
using namespace std;
const int N = 5e5 + 5;
const int INF = 1e9;
// a[] 存储初始序列,之后不再改变
// b[i] 存储与原始第 i 个元素关联的“块”的最后一个元素的值
int a[N], b[N];
int main()
{
int n, m;
cin >> n >> m;
// gap: 使用 multiset 存储所有相邻元素的差值
// num: 使用 multiset 存储序列中所有的数字,保持有序
multiset<int> gap, num;
// --- 初始化 ---
for (int i = 1; i <= n; i++) {
cin >> a[i]; b[i] = a[i]; // 初始时,每个块的最后一个元素就是它自己
if (i > 1) gap.insert(abs(a[i] - a[i - 1]));
num.insert(a[i]);
}
// 计算初始的 MIN_SORT_GAP
int min_sort_gap = INF, pre = -1;
for (int x : num) {
if (pre != -1) {
min_sort_gap = min(min_sort_gap, x - pre);
}
pre = x;
}
// --- 处理 m 次操作 ---
for (int t = 1; t <= m; t++) {
string op; cin >> op;
if (op == "INSERT") {
int i, k; cin >> i >> k;
// --- 更新 gap (相邻差值) ---
// 1. 移除旧的差值 |b[i] - a[i+1]|
// b[i] 是第 i 个块的最后一个元素,a[i+1] 是下一个块的第一个元素
if (i < n) {
gap.erase(gap.find(abs(b[i] - a[i + 1])));
}
// 2. 插入新差值 |b[i] - k| 和 |k - a[i+1]|
gap.insert(abs(b[i] - k));
if (i < n) { // 只有当不是在序列末尾插入时,才有 a[i+1]
gap.insert(abs(a[i + 1] - k));
}
// 3. 更新第 i 个块的最后一个元素为 k
b[i] = k;
// --- 更新 min_sort_gap ---
// 1. 在有序集合 num 中找到新值 k 要插入的位置
auto iter = num.lower_bound(k);
// 2. 检查 k 与其后继的差值
if (iter != num.end()) {
min_sort_gap = min(min_sort_gap, *iter - k);
}
// 3. 检查 k 与其前驱的差值
if (iter != num.begin()) {
iter--;
min_sort_gap = min(min_sort_gap, k - *iter);
}
// 4. 将新值 k 插入有序集合 num
num.insert(k);
} else if (op == "MIN_GAP") {
cout << *gap.begin() << "\n";
} else { // MIN_SORT_GAP
cout << min_sort_gap << "\n";
}
}
return 0;
}
STL map
例题:P5266 【深基17.例6】学籍管理
这样的学籍管理系统也是一个集合,但是功能更加复杂,需要根据索引找到对应的元素,并对这些元素进行操作。可以通过使用 STL 里面的 map 来解决这个问题。
map 关联集合的本质也是一棵红黑树,可以看作是一个下标可以是任意类型的数组。其头文件是 map,可以调用 map 实现如下几个基础功能:
map<A, B> mp:建立一个名字叫做mp,下标类型为A,元素类型为B的映射表,例如map<string, int>就是一个将string映射到int的映射表。这种映射结构俗称“键-值对”。mp[A] = B:把这个“数组”中下标为A的位置的值变成B,这里下标可以是任意类型,不一定限定为大于 0 的整数,比如map<string, string> mp,就可以进行mp["username"] = "password"的操作。mp[A]:访问这个“数组”中下标为A的元素,比如可以进行cout << mp["username"] << "\n";这样的操作。mp.end():返回映射表中最后一个元素的下一个元素的地址,这个很少单独使用,而是配合其他方法进行比较,以确认某个元素是否存在。mp.find(x):查询x在映射表中的地址,如果这个数不存在,则返回mp.end()。mp.empty():如果映射表是空的,则返回true,否则返回false。mp.size():返回映射表中的元素个数。mp.erase(A):删除这个“数组”中下标为A的元素。注意:在使用mp[A]访问“数组”下标为A的元素时,如果这个下标对应的元素不存在,则会自动创建下标为A、值为默认值(例如,所有数值类型的默认值是 0,string类型是空字符串)的元素。
#include <iostream>
#include <string>
#include <map> // 包含 map 头文件
using namespace std;
// sys 是一个 map 容器,用于存储学籍信息
// 键(key)是 string 类型的学生姓名
// 值(value)是 int 类型的学生分数
map<string, int> sys;
int main()
{
int n; cin >> n; // 操作的总次数
for (int i = 0; i < n; ++i) {
int op; cin >> op; // 操作类型
if (op == 1) {
// --- 操作 1: 插入或修改 ---
string name;
int score;
cin >> name >> score;
// 使用 map 的 [] 操作符
// 如果 name 已存在,则更新其对应的分数为 score
// 如果 name 不存在,则插入一个新的键值对 {name, score}
sys[name] = score;
cout << "OK\n";
} else if (op == 2) {
// --- 操作 2: 查询 ---
string name; cin >> name;
// 使用 map 的 find 方法查找是否存在键为 name 的元素
// find 返回一个迭代器。如果找不到,返回 map.end()
if (sys.find(name) != sys.end()) {
// 如果找到了,输出其对应的分数
cout << sys[name] << "\n";
} else {
// 如果没找到,按要求输出
cout << "Not found\n";
}
} else if (op == 3) {
// --- 操作 3: 删除 ---
string name; cin >> name;
// 同样,先用 find 检查是否存在
if (sys.find(name) == sys.end()) {
// 如果不存在,输出 "Not found"
cout << "Not found\n";
} else {
// 如果存在,使用 erase 方法删除该键值对
sys.erase(name);
cout << "Deleted successfully\n";
}
} else { // op == 4
// --- 操作 4: 汇总 ---
// 使用 map 的 size 方法获取当前元素的数量
cout << sys.size() << "\n";
}
}
return 0;
}
习题:P5149 会议座位
解题思路
可以将这个问题转化为求逆序对。
- 建立映射关系:首先,以初始的座位表为基准,建立一个从老师名字到其初始位置(从 1 到 \(n\))的映射。例如,如果初始座位是
Stan Kyle Kenny,那么映射关系就是Stan -> 1,Kyle -> 2,Kenny -> 3。 - 构造数字序列:接下来,根据打乱后的座位表,构造一个新的数字序列,序列中的每个数字是该位置老师的初始位置编号。沿用上面的例子,如果打乱后的座位是
Kyle Stan Kenny,那么构造出的数字序列就是[2, 1, 3]。 - 问题转化:要求的不满值就是构造出来的这个数字序列中逆序对的数量。
参考代码
#include <cstdio>
#include <iostream>
#include <map>
#include <string>
using namespace std;
using ll = long long;
const int N = 1e5 + 5;
// 使用 map 存储老师名字到其初始位置的映射
map<string, int> mp;
// a[] 存储最终排列对应的初始位置序列,tmp[] 是归并排序用的临时数组
int a[N], tmp[N];
// ans 用于累计逆序对的数量
ll ans;
// 归并排序算法,在合并过程中计算逆序对
void mergesort(int l, int h) {
// 如果区间为空或只有一个元素,直接返回
if (l >= h) return;
// 分治
int mid = (l + h) / 2;
mergesort(l, mid); // 递归处理左半部分
mergesort(mid + 1, h); // 递归处理右半部分
// 合并两个有序子数组,并计算逆序对
int p = l, q = mid + 1, r = l;
while (p <= mid && q <= h) {
if (a[p] <= a[q]) {
// 左边元素较小,不构成逆序对
tmp[r++] = a[p];
p++;
} else {
// 右边元素 a[q] 较小,说明 a[q] 与左边剩余的所有元素 a[p...mid] 都构成了逆序对
ans += mid - p + 1;
tmp[r++] = a[q];
q++;
}
}
// 处理剩余的元素
while (p <= mid) tmp[r++] = a[p++];
while (q <= h) tmp[r++] = a[q++];
// 将排序好的临时数组内容复制回原数组
for (int i = l; i <= h; i++) a[i] = tmp[i];
}
int main()
{
int n; cin >> n;
// 1. 读取初始序列,建立名字到初始位置的映射
for (int i = 1; i <= n; i++) {
string name; cin >> name;
mp[name] = i;
}
// 2. 读取最终序列,并将其转换为初始位置的数字序列
for (int i = 1; i <= n; i++) {
string name; cin >> name;
a[i] = mp[name];
}
// 3. 使用归并排序计算数字序列的逆序对数量
mergesort(1, n);
// 输出结果
printf("%lld\n", ans);
return 0;
}
习题:P8289 [省选联考 2022] 预处理器
解题思路
需要一个能将宏名(string)映射到其展开内容(string)的数据结构,map<string, string> 是一个理想的选择,它能够提供高效的查找、插入和删除操作。
当一个宏 A 正在展开时,如果其展开内容中又出现了 A,则不应该再次展开。为了跟踪“正在展开的宏”,可以用一个 set 来记录它们。在开始展开一个宏之前,将其加入 set,展开结束后,将其移出 set。
逐行读取输入,检查行的第一个字符是否为 #,如果是 #,则为预处理命令,否则,为普通文本。
根据预处理命令是 #define 还是 #undef,解析出宏名 <name> 和宏内容 <content>。由于格式固定,可以通过从特定索引开始遍历,直到遇到非标识符字符来提取 <name>。对于 #define,空格之后的所有剩余部分都是 <content>。根据 #define 和 #undef,在 map 中执行相应的插入/删除操作。
处理普通文本是本题的核心难点,需要将一行文本分解为“标识符”和“其他字符”。遍历字符串,判断当前字符类型,如果是非标识符字符,直接拼接到结果中,如果是标识符字符,则继续向后查找,直到找到整个连续的标识符段。对于每个被识别出的标识符,如果满足展开条件,就展开它,由于展开的内容本身也可能包含红,所以可以结合递归调用的方式实现展开。如果不满足展开条件,则原样拼接到结果中。
参考代码
#include <iostream>
#include <string>
#include <map>
#include <set>
using namespace std;
// tb (table): 使用 map 存储宏定义,键是宏名,值是其展开内容。
map<string, string> tb;
// lock: 使用 set 跟踪当前正在展开的宏,用于防止无限递归。
set<string> lock;
/**
* @brief 检查一个字符是否是标识符的一部分(字母、数字、下划线)。
* @param ch 待检查的字符。
* @return 如果是标识符字符则返回 true,否则返回 false。
*/
bool check(char ch) {
if (ch>='0' && ch<='9') return true;
if (ch>='a' && ch<='z') return true;
if (ch>='A' && ch<='Z') return true;
return ch=='_';
}
/**
* @brief 递归地展开一个字符串。
* 该函数会遍历输入字符串,识别出其中的标识符,并尝试展开它们。
* @param s 待展开的字符串。
* @return 完全展开后的字符串。
*/
string expand(string s) {
int len = s.size();
string res; // 用于存储最终展开的结果
int i = 0;
while (i < len) {
// 如果当前字符不是标识符的一部分,直接原样保留
if (!check(s[i])) {
res += s[i];
i++;
} else {
// 如果是标识符字符,则提取出完整的标识符
int j = i;
// j 一直延伸到标识符的结尾
while (j < len && check(s[j])) {
j++;
}
// 此时 s[i...j-1] 就是一个完整的标识符
string id = s.substr(i, j - i);
// 判断这个标识符是否可以展开:
// 1. 它必须在宏定义表 tb 中存在。
// 2. 它不能处于“正在展开”的状态(即不在 lock 集合中)。
if (tb.find(id) != tb.end() && lock.find(id) == lock.end()) {
lock.insert(id); // 加锁:标记为“正在展开”
res += expand(tb[id]); // 递归地展开其内容
lock.erase(id); // 解锁:展开完成,取消标记
} else {
// 如果不能展开,则原样保留该标识符
res += id;
}
// 从标识符之后的位置继续处理
i = j;
}
}
return res;
}
int main()
{
int n;
cin >> n;
string line;
// 读取 cin >> n 后留下的换行符,防止影响后续的 getline
getline(cin, line);
for (int i = 1; i <= n; i++) {
getline(cin, line);
// 判断是预处理命令还是普通文本
if (line[0] == '#') {
if (line[1] == 'd') { // #define 命令
// #define <name> <content>
// 012345678
int pos = 8; // 查找 <name> 和 <content> 之间的空格
while (pos < line.size() && line[pos] != ' ') {
pos++;
}
// 提取 <name> 和 <content>
string name = line.substr(8, pos - 8);
string content = line.substr(pos + 1);
tb[name] = content;
} else { // #undef 命令
// #undef <name>
// 01234567...
string name = line.substr(7);
if (tb.find(name) != tb.end()) {
tb.erase(name);
}
}
// 预处理命令执行后,输出一个空行
cout << "\n";
} else {
// 普通文本行,调用 expand 函数进行展开并输出
cout << expand(line) << "\n";
}
}
return 0;
}
map 的遍历
#include <iostream>
#include <string>
#include <map>
// std::map 中的元素是键值对 (key-value pair),类型为 std::pair<const Key, T>。
// 遍历时,map 会保证元素是按照键的升序顺序被访问的。
int main() {
// 创建并初始化一个 map,用于后续的遍历演示
std::map<std::string, int> student_scores;
student_scores["LiWei"] = 95;
student_scores["ZhangSan"] = 78;
student_scores["WangWu"] = 86;
student_scores["Alice"] = 99;
// 使用基于范围的 for 循环 (Range-based for loop)
// 这是自 C++11 起最推荐的方法,代码简洁、易读且不易出错。
std::cout << "--- Range-based for loop (C++11) ---" << std::endl;
// 'const auto&' 是一个好习惯:
// 'const' 保证我们不会意外修改 map 中的值。
// 'auto' 让编译器自动推断元素类型 (即 std::pair<const std::string, int>)。
// '&' (引用) 避免了每次循环都复制一遍键值对,提高了效率。
for (const auto& pr : student_scores) {
// pr.first 是键 (key)
// pr.second 是值 (value)
std::cout << "Key: " << pr.first << ", Value: " << pr.second << std::endl;
}
std::cout << std::endl;
return 0;
}
习题:P6627 [省选联考 2020 B 卷] 幸运数字
解题思路
一个幸运数字 \(x\) 的最终优惠额度,只在 \(x\) 穿过某个条件的边界点时才会发生改变。因此,我们只需要考察所有这些“关键点”即可。
对于不等型条件,一个 \(x \ne B\) 的条件,对除了 \(B\) 之外的所有 \(x\) 都生效。可以先假设所有 \(x\) 都满足了所有不等型条件,得到一个“基础”优惠额度,这个额度就是所有不等型条件 \(w\) 的总异或和。然后,对于每个特定的点 \(B\),再进行“修正”——如果幸运数字 \(x\) 恰好等于 \(B\),就需要把当初算上的 \(w\) 再异或一次以抵消掉。可以用一个 map<int, int> 来存储每个 \(B\) 点需要修正的值。
区间型和相等型条件可以归为一类处理,因为 \(x=A\) 这样的条件,可以看作是一个长度为 \(1\) 的区间 \([A,A]\)。对于这些区间型的异或操作,可以使用差分数组的思想。对于一个区间 \([L,R]\) 的条件 \(w\),在 \(L\) 点的差分值上异或 \(w\),在 \(R+1\) 点的差分值上再异或 \(w\)。这样,当从左到右扫描 \(x\) 时,在进入 \(L\) 点后,总的异或和会包含 \(w\);在离开 \(R\) 点(即进入 \(R+1\))后,\(w\) 的影响被抵消。这里差分数组由于下标从 \(-10^9 \sim 10^9\) 都有可能,也可以使用 map<int, int>。
参考代码
#include <cstdio>
#include <map>
#include <cmath>
using namespace std;
const int INF = 2e9;
// award: 使用 map 存储差分数组,键是坐标点,值是该点对应的异或差分值
// ne (not equal): 单独存储所有不等型条件 (x != B) 的信息
map<int, int> award, ne;
int main()
{
int n; scanf("%d", &n);
// t3: 存储所有不等型条件 (x != B) 的 w 的总异或和
int t3 = 0;
award[0] = 0; // 对于一个负数到正数的区间,可能最后答案是0
// --- 预处理所有条件 ---
for (int i = 1; i <= n; i++) {
int t; scanf("%d", &t);
if (t == 1) { // --- 区间型条件: L <= x <= R ---
int l, r, w; scanf("%d%d%d", &l, &r, &w);
// 确保所有相关的边界点都存在于 map 中,以便后续扫描
award[l - 1] ^= 0; award[l] ^= w;
// 应用差分:在 l 点异或 w,在 r+1 点再次异或 w 以抵消
award[r] ^= 0; award[r + 1] ^= w;
} else if (t == 2) { // --- 相等型条件: x = A ---
int a, w; scanf("%d%d", &a, &w);
// 确保边界点存在
award[a - 1] ^= 0;
// 等价于区间 [A, A],在 A 点异或 w, 在 A+1 点抵消
award[a] ^= w; award[a + 1] ^= w;
} else if (t == 3) { // --- 不等型条件: x != B ---
int b, w; scanf("%d%d", &b, &w);
// 确保 B 点及其邻居作为关键点存在于 map 中
award[b] ^= 0; award[b - 1] ^= 0; award[b + 1] ^= 0;
// ne[b] 存储在 b 点需要修正掉的异或值
ne[b] ^= w;
// t3 不断异或所有不等型条件的 w,作为全局基础异或值
t3 ^= w;
}
}
int ans = -2e9, maxw = -1; // ans: 最优幸运数字, maxw: 最大优惠额度
int w = 0; // w: 当前扫描线位置,由类型1和2贡献的累积异或和
// --- 扫描线求解 ---
// 遍历 map 中所有预先设置的关键点
for (auto x : award) {
int num = x.first; // 当前关键点坐标
int val = x.second; // 该点的差分值
w ^= val;
// 计算在 num 这个点上的总优惠额度
// 基础额度是 w ^ t3
// 如果 num 是一个不等型条件的特殊点 B,则需进行修正
int cur = ne.find(num) == ne.end() ? w ^ t3 : w ^ t3 ^ ne[num];
if (cur > maxw) { // 更新最优解
maxw = cur; ans = num;
} else if (cur == maxw && abs(num) <= abs(ans)) {
// 如果优惠额度相同,按题目要求取绝对值更小的;如果绝对值还相同,取值更大的
ans = num;
}
}
printf("%d %d\n", maxw, ans);
return 0;
}
用自定义类型作为 map 的键
#include <iostream>
#include <string>
#include <map>
// std::map 是一个有序的关联容器,它按照键的顺序来存储元素。
// 当使用自定义类型作为键时,map 必须知道如何比较两个键的大小,以便进行排序和查找。
//
// 有两种主要的方法来实现这一点:
// 1. 在自定义类型中重载小于运算符 (operator<)。
// 2. 为 map 提供一个自定义的比较器类(仿函数)。
// --- 方法一:重载小于运算符 (operator<) ---
// 这是最直接和常见的方法。如果你的类型有一个“自然”的排序顺序,重载 operator< 是最佳选择。
struct Point {
int x, y;
// 为了让 Point 能作为 std::map 的键,我们必须定义比较规则。
// 这里我们重载 operator<。
// 规则:先比较 x 坐标,如果 x 相同,再比较 y 坐标。
//
// 这个函数必须是 const 成员函数,因为它不应该修改当前对象。
// 参数也应该是 const 引用,因为它也不应该修改被比较的对象。
bool operator<(const Point& other) const {
if (x < other.x) {
return true;
}
// 如果 x 相等,则根据 y 来决定大小
if (x == other.x && y < other.y) {
return true;
}
// 其他所有情况,当前对象都不“小于”other
return false;
}
};
void demo_with_operator_lt() {
std::cout << "--- Demo 1: Using operator< ---" << std::endl;
// 创建一个以 Point 为键,std::string 为值的 map。
// map 会自动使用 Point::operator< 来进行内部排序。
std::map<Point, std::string> point_map;
// 添加元素
point_map[{2, 3}] = "Data_A";
point_map[{1, 8}] = "Data_B";
point_map[{2, 1}] = "Data_C"; // x坐标与A相同,但y坐标更小
point_map[{0, 9}] = "Data_D";
// 遍历 map。元素将会按照我们定义的排序规则输出。
// 预期顺序: {0, 9}, {1, 8}, {2, 1}, {2, 3}
for (const auto& pr : point_map) {
const Point& p = pr.first;
const std::string& data = pr.second;
std::cout << "Key: (" << p.x << ", " << p.y << "), Value: " << data << std::endl;
}
std::cout << std::endl;
}
// --- 方法二:提供自定义比较器 ---
// 当你无法修改自定义类型的源代码(例如,它来自第三方库),
// 或者你想根据不同场景使用不同的排序规则时,这个方法非常有用。
// 这是一个没有定义 operator< 的自定义类型
struct Rect {
int width, height;
int area() const { return width * height; }
};
// 我们创建一个“仿函数”(Functor),它是一个重载了 operator() 的类。
// map 将使用这个类的实例来比较两个 Rect 对象。
struct RectCompare {
// 规则:根据矩形的面积来排序。面积小的在前。
// operator() 必须接收两个 const 引用参数,并且自身也必须是 const。
bool operator()(const Rect& a, const Rect& b) const {
return a.area() < b.area();
}
};
void demo_with_custom_comparator() {
std::cout << "--- Demo 2: Using a custom comparator ---" << std::endl;
// 在定义 map 时,需要将比较器类型作为第三个模板参数传入。
std::map<Rect, std::string, RectCompare> rect_map;
// 添加元素
rect_map[{5, 5}] = "Rect_A (25)"; // 面积 25
rect_map[{10, 2}] = "Rect_B (20)";// 面积 20
rect_map[{3, 8}] = "Rect_C (24)"; // 面积 24
rect_map[{2, 5}] = "Rect_D (10)"; // 面积 10
// 遍历 map。元素将会按照 RectCompare 定义的面积大小规则排序。
// 预期顺序: D(10), B(20), C(24), A(25)
for (const auto& pr : rect_map) {
const Rect& r = pr.first;
const std::string& data = pr.second;
std::cout << "Key: {" << r.width << "x" << r.height << "}, Value: " << data << std::endl;
}
std::cout << std::endl;
}
int main() {
// 演示第一种方法
demo_with_operator_lt();
// 演示第二种方法
demo_with_custom_comparator();
return 0;
}
习题:CF1133D Zero Quantity Maximization
解题思路
题目的目标是选择一个实数 \(d\),使得数组 \(c\) 中 \(0\) 的数量最多,其中 \(c_i = d \ast a_i + b_i\)。
要让 \(c_i = 0\),即 \(d \ast a_i + b_i = 0\)。这个方程的解 \(d\) 取决于 \(a_i\) 和 \(b_i\) 的值,目标是找到一个 \(d\),这个 \(d\) 能同时满足尽可能多的 \(i\)。这意味着,要找到一个出现次数最多的 \(d\) 值。
如果 \(a_i \ne 0\),方程可以变形为 \(d = -b_i / a_i\)。所以具有相同 \(-b_i/a_i\) 比率的 \(i\),都可以通过选择这同一个 \(d\) 值来满足。因此,问题转化为统计所有 \(-b_i/a_i\) 这个比率的出现次数,并找出最大频次。
如果 \(a_i = 0\),方程变为 \(d \ast 0 + b_i = 0\),即 \(b_i = 0\)。如果 \(b_i = 0\),方程是 \(0=0\),这个等式对于任何 \(d\) 值都成立。这意味着,只要 \(a_i\) 和 \(b_i\) 都是 \(0\),无论为了其他 \(i\) 选择了哪个 \(d\),这个 \(c_i\) 都将是 \(0\)。这些情况是“免费的午餐”,可以把它们的数量单独统计出来,最后加到答案上。如果 \(b_i \ne 0\),方程是 \(b_i = 0\),这是一个矛盾的等式。这意味着,对于这种情况,无论 \(d\) 取何值,\(c_i\) 永远不可能为 \(0\),所以可以忽略这些 \(i\)。
直接计算 \(d = -b_i/a_i\) 会引入浮点数,比较浮点数是否相等并不可靠。可以用分数来表示这个比率,比率 \(-b_i/a_i\) 和 \(-b_j/a_j\) 相等,等价于分数 \(b_i/a_i\) 和 \(b_j/a_j\) 相等。为了能对分数进行比较和计数,需要将它们化为最简形式。例如,\(2/4\) 和 \(3/6\) 都应该被当做 \(1/2\)。这可以通过分子分母同时除以它们的最大公约数来实现。
同时,为了保证分数的唯一表示(例如,\(-1/2\) 和 \(1/-2\) 应该被视为同一个),需要规定一个标准形式,比如统一将负号放在分子上。
使用一个 map,键是一个表示最简分数的结构体,值是这个分数出现的次数。
参考代码
#include <cstdio>
#include <algorithm>
#include <map>
#include <cmath>
using namespace std;
const int N = 200005;
int a[N];
/**
* @brief 计算两个整数的最大公约数 (GCD)。
* @param x 第一个整数。
* @param y 第二个整数。
* @return x 和 y 的最大公约数。
*/
int gcd(int x, int y) {
return y == 0 ? x : gcd(y, x % y);
}
/**
* @brief 自定义分数结构体,用于作为 map 的键。
* a 代表分子,b 代表分母。
*/
struct Fraction {
int a, b;
// 重载小于号,使得 Fraction 可以被 map 用作键。
// map 会根据这个比较函数来排序和查找键。
bool operator<(const Fraction &other) const {
// 优先比较分子,如果分子不同,则分子小的分数小。
// 如果分子相同,则比较分母,分母小的分数小。
return a != other.a ? a < other.a : b < other.b;
}
};
int main()
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
// 使用 map 来统计每个化简后的分数出现的次数。
// 键是 Fraction 结构体,值是该分数出现的次数。
map<Fraction, int> cnt;
int flag = 0; // 用于单独统计 a[i]=0 且 b[i]=0 的情况
for (int i = 1; i <= n; i++) {
int b_val;
scanf("%d", &b_val);
// --- 情况 1: a[i] = 0, b[i] = 0 ---
// 方程变为 d*0 + 0 = 0,即 0 = 0。这对任何 d 都成立。
// 这样的 (a[i], b[i]) 对,无论我们选择哪个 d,都会对答案产生 1 的贡献。
// 所以我们把它们单独计数,最后加到最终答案上。
if (a[i] == 0 && b_val == 0) {
flag++;
continue;
}
// --- 情况 2: a[i] != 0, b[i] = 0 ---
// 方程变为 d*a[i] + 0 = 0,因为 a[i] != 0,所以 d 必须为 0。
// 我们将这种情况映射为一个特殊的分数 {1, 0} 来统计。
// 实际上,任何非零分子和零分母的组合都可以,例如 {a[i], 0}。
// 但为了统一,使用 {1, 0} 更简洁。
if (b_val == 0) { // a[i] 必然不为0,因为 a[i]=0, b[i]=0 的情况已被处理
cnt[{1, 0}]++;
}
// --- 情况 3: a[i] = 0, b[i] != 0 ---
// 方程变为 d*0 + b[i] = 0,即 b[i] = 0。但这与 b[i] != 0 矛盾。
// 所以这种情况下,无论 d 取何值,c[i] 都不可能为 0。我们直接忽略这种情况。
else if (a[i] != 0) { // b_val 也必然不为0
// --- 情况 4: a[i] != 0, b[i] != 0 ---
// 方程为 d*a[i] + b[i] = 0,解得 d = -b[i] / a[i]。
// 所有能产生相同 d 值的 (a[i], b[i]) 对,可以被同时满足。
// 我们需要统计每个 d 值出现的次数。
// d = -b/a,为了避免浮点数精度问题,我们将其表示为最简分数 -b/a。
// 注意,d 的符号不影响我们计数,因为我们只关心 -b/a 这个比率是否相同。
// 所以我们统计 b/a 这个比率。
int g = gcd(abs(a[i]), abs(b_val)); // 计算最大公约数以化简分数
int num = a[i] / g; // 分子
int den = b_val / g; // 分母
// 为了保证分数的唯一性(例如 -2/3 和 2/-3 是同一个数),
// 我们统一将负号放在分子上。如果分母为负,则分子分母同时取反。
if (den < 0) {
den = -den;
num = -num;
}
cnt[{num, den}]++;
}
}
int ans = 0;
// 遍历 map,找到出现次数最多的那个比率(即能同时满足的最多数量)
for (auto p : cnt) {
ans = max(ans, p.second);
}
// 最终答案是:出现次数最多的那个d值能满足的数量 + 那些无论d取何值都能满足的数量
printf("%d\n", ans + flag);
return 0;
}

浙公网安备 33010602011771号