当前位置: 首页 > news >正文

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(k+\log n)\),其中 \(k\) 为被删除掉的元素个数
  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;
}

习题: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. 创建 multisetmultiset<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_boundms.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 为左端点的区间加入活跃集合 sfor (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 为右端点的区间移出活跃集合 sfor (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_GAPint 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 个块的最后一个元素为 kb[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 插入有序集合 numnum.insert(k);} else if (op == "MIN_GAP") {cout << *gap.begin() << "\n";} else { // MIN_SORT_GAPcout << 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;
}
http://www.sczhlp.com/news/9893/

相关文章:

  • 8.10XS模拟赛
  • 企业经营分析指南:从供产销研运5大维度,用数据找准优化方向 - 智慧园区
  • 软工8.11
  • 补题祭day1
  • 2-SAT 学习报告
  • ces
  • CSP-J 模拟1解析
  • 20250811
  • 《Effective C++》(1,2)
  • 数组
  • CSP-S模拟赛11 总结
  • CSP-S模拟赛12 总结
  • 旋转表达:blender下骨骼重映射的公式推导 bone animation retarget
  • 一名OIER的开始
  • springboot监听redisKey过期 - br
  • 你好我好一切都好 - Karry
  • 数据库操作例题
  • 02010901 表达式和运算符
  • 浏览器面试题及详细答案 88道(01-11) - 详解
  • WBLT学习笔记
  • 【自学嵌入式:stm32单片机】旋转编码器记次
  • 乌班图静态网址动态网址
  • 用户以及赋权还有备份数据库
  • 立个Flag,重新开始使用cnblog - by
  • 做题日志2025.8
  • 02010803 类和继承03-静态类、扩展方法、命名约定
  • 我设计的IP地址(3)
  • base44
  • 2025.8.11总结 - A
  • ftp服务详解