STL set、map

STL set

例题:P5250 【深基17.例5】木材仓库

分析:这个问题可以抽象为:维护一个集合,可以插入一个元素 \(x\),同时判断 \(x\) 是否已经存在;查询 \(x\) 的前驱后继,\(x\) 的前驱定义为小于 \(x\) 的最大的数,\(x\) 的后继定义为大于 \(x\) 的最小的数;可以删除指定元素

STL 中的 set 可以很方便地解决这个问题,set 的本质是红黑树(一种比较优秀的平衡二叉查找树)

使用 set 需要通过 #include <set> 引入相应头文件,其常用方法如下:

  1. set<int> s 创建一个名字叫做 s 的、元素类型为 int 的集合
  2. s.insert(x) 在集合中插入元素 \(x\),如果这个数已经存在,则什么都不干,时间复杂度为 \(O(\log n)\)
  3. s.erase(x) 在集合中删除元素 \(x\),如果这个数不存在,则什么都不干,时间复杂度为 \(O(\log n)\)
  4. s.erase(it) 删除集合中迭代器 it 指向的元素,时间复杂度为 \(O(\log n)\)
  5. s.end() 返回集合中最后一个元素的下一个元素的迭代器,很少直接使用,通常配合其他方法进行比较,以确认某个元素是否存在
  6. s.find(x) 如果 \(x\) 不在集合中,返回 s.end(),否则返回指向元素 \(x\) 的迭代器
  7. s.lower_bound(x) 查询大于等于 \(x\) 的最小的元素在集合中对应的迭代器,如果没有这样的元素,返回 s.end()
  8. s.uppper_bound(x) 查询大于 \(x\) 的最小的元素在集合中对应的迭代器,如果没有这样的元素,返回 s.end()
  9. s.empty() 如果集合是空的,返回 true,否则返回 false
  10. s.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 个原始元素相关的块之后插入。因为只有块头和块尾会影响到相邻元素的差值,可以使用两个数组 aba 存储初始序列(块头),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 实现如下几个基础功能:

  1. map<A, B> mp:建立一个名字叫做 mp,下标类型为 A,元素类型为 B 的映射表,例如 map<string, int> 就是一个将 string 映射到 int 的映射表。这种映射结构俗称“键-值对”。
  2. mp[A] = B:把这个“数组”中下标为 A 的位置的值变成 B,这里下标可以是任意类型,不一定限定为大于 0 的整数,比如 map<string, string> mp,就可以进行 mp["username"] = "password" 的操作。
  3. mp[A]:访问这个“数组”中下标为 A 的元素,比如可以进行 cout << mp["username"] << "\n"; 这样的操作。
  4. mp.end():返回映射表中最后一个元素的下一个元素的地址,这个很少单独使用,而是配合其他方法进行比较,以确认某个元素是否存在。
  5. mp.find(x):查询 x 在映射表中的地址,如果这个数不存在,则返回 mp.end()
  6. mp.empty():如果映射表是空的,则返回 true,否则返回 false
  7. mp.size():返回映射表中的元素个数。
  8. 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. 建立映射关系:首先,以初始的座位表为基准,建立一个从老师名字到其初始位置(从 1 到 \(n\))的映射。例如,如果初始座位是 Stan Kyle Kenny,那么映射关系就是 Stan -> 1Kyle -> 2Kenny -> 3
  2. 构造数字序列:接下来,根据打乱后的座位表,构造一个新的数字序列,序列中的每个数字是该位置老师的初始位置编号。沿用上面的例子,如果打乱后的座位是 Kyle Stan Kenny,那么构造出的数字序列就是 [2, 1, 3]
  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;
}

posted @ 2025-08-11 21:10  RonChen  阅读(83)  评论(0)    收藏  举报