OI-wiki 标准模板库STL容器完全指南从分类体系到实战选型【免费下载链接】OI-wiki:star2: Wiki of OI / ICPC for everyone. 某大型游戏线上攻略内含炫酷算术魔法项目地址: https://gitcode.com/GitHub_Trending/oi/OI-wiki本文基于 OI-wiki 仓库中 容器概述文档 及其配套的序列式容器、关联式容器、容器适配器、无序容器等系列文档系统讲解 C STL 容器的分类体系、模板声明规范、迭代器模型与共有接口并结合仓库中真实竞赛代码佐证各容器的适用场景。读者学完后将能够根据时间复杂度需求、有序性要求和内存约束在vector、set、map、unordered_map、priority_queue等容器之间做出正确选型并熟练使用其成员函数。为什么 OI/ICPC 选手需要掌握容器分类在 OI信息学奥林匹克与 ICPC 竞赛中STLStandard Template Library标准模板库是 C 标准库的核心组成部分NOI 和 ICPC 赛事均支持其使用。STL 提供模板化的通用数据结构和算法能兼容自定义数据类型避免大量造轮子工作。正如 STL 概述 所言合理利用 STL 可以避免编写无用算法并充分利用编译器对模板库的优化提高效率。但 STL 容器种类繁多不同容器在存储结构、迭代器能力、操作复杂度上差异巨大。例如在 队列/栈示例代码 和 优先队列示例 中仓库内大量代码用到了queue、priority_queue、vector、set等容器。选错容器轻则常数变大重则复杂度从 $O(\log n)$ 退化到 $O(n)$。因此理解容器的整体分类是高效使用 STL 的第一步。STL 容器总览四大分类STL 容器大体可以分为四类序列式容器、关联式容器、无序关联式容器与容器适配器。下图为整个分类体系的直观示意序列式容器序列式容器的特点是元素按线性顺序存储元素之间存在先后关系向量vector后端可高效增加元素的顺序表内存连续、支持常数复杂度随机访问。数组arrayC11定长的顺序表C 风格数组的简单包装牺牲动态扩容换取与原生数组几乎一致的性能。双端队列deque双端都可高效增加元素的顺序表即支持在头部和尾部均以常数复杂度插入/删除。列表list可以沿双向遍历的链表插入/删除常数复杂度但不支持随机访问。单向列表forward_list只能沿一个方向遍历的链表相比list减小了空间开销。关联式容器关联式容器基于红黑树实现内部元素自动有序集合set用以有序地存储互异元素的容器其实现是由节点组成的红黑树每个节点包含一个元素节点之间以某种比较元素大小的谓词进行排列。搜索、移除和插入均拥有 $O(\log n)$ 复杂度。多重集合multiset用以有序地存储元素的容器允许存在相等的元素。映射map由 {键值} 对组成的集合以某种比较键大小关系的谓词进行排列键唯一。多重映射multimap由 {键值} 对组成的多重集合允许键有相等情况因此不提供按键直接访问对应值的方法。什么是谓词Predicate谓词就是返回值为真或者假的函数。STL 容器中经常会使用到谓词用于模板参数例如set的默认比较谓词就是std::lessType即运算符。无序关联式容器自 C11 标准起四种基于哈希实现的无序关联式容器正式纳入 C 标准模板库无序多重集合unordered_set/unordered_multisetC11与set/multiset的区别在于元素无序只关心元素是否存在使用哈希实现。无序多重映射unordered_map/unordered_multimapC11与map/multimap的区别在于键无序只关心键与值的对应关系使用哈希实现。它们与相应关联式容器的最大区别在于普通关联式容器采用红黑树、内部元素按特定顺序排序无序容器采用哈希存储平均情况下查找、插入、删除均能在常数时间复杂度内完成但访问顺序无任何保证。详细讨论见下文无序容器效率与风险一节。容器适配器容器适配器其实并不是容器。它们不具有容器的某些特点如有迭代器、有clear()函数……。正如经典描述所言适配器是使一种事物的行为类似于另外一种事物行为的一种机制适配器对容器进行包装使其表现出另外一种行为。栈stack后进先出LIFO的容器默认是对双端队列deque的包装仅支持查询或删除栈顶元素不支持随机访问也不支持迭代器。队列queue先进先出FIFO的容器默认是对双端队列deque的包装仅支持查询或删除队首元素。优先队列priority_queue元素次序由作用于所存储值上的某种谓词决定的一种队列本质上是一个二叉堆默认是对向量vector的包装。所有容器的共同点容器声明模板类的统一范式所有容器的声明形式都是containerNametypeName,... name但模板参数内的参数的个数、形式会根据具体容器而变。本质原因在于STL 就是标准模板库所以容器都是模板类。例如vectorint v; // 一个模板参数元素类型 arrayint, 3 a; // 两个模板参数元素类型 定长 N setint s; // 一个模板参数元素类型 mapstring, int mp; // 两个模板参数键类型 值类型 priority_queueint pq; // 一个模板参数 priority_queueint, vectorint, greaterint pq_small; // 三个模板参数 unordered_mapint, int, my_hash um; // 可传入第三个模板参数自定义哈希函数迭代器统一的元素访问接口STL 容器通过**迭代器Iterator**统一访问元素。迭代器行为模式类似指针封装了有效性检查并提供统一的访问格式详见 迭代器。迭代器主要支持自增和解引用单目*两个运算符前者移动迭代器后者获取或修改其指向的元素。指向容器container中元素的迭代器类型一般为container::iteratorvectorint data(10); for (int i 0; i data.size(); i) cout data[i] endl; // 使用下标访问元素 for (vectorint::iterator iter data.begin(); iter ! data.end(); iter) cout *iter endl; // 使用迭代器访问元素 // C11 后可使用 auto iter data.begin() 简化需要注意的是不同容器支持的迭代器类别不同vector提供随机访问迭代器支持n、-n、比较运算list仅提供双向迭代器forward_list仅提供前向迭代器而stack/queue等适配器根本不提供迭代器。选型时必须考虑这一点。共有函数一套通用接口几乎所有容器都提供以下共有函数函数作用有赋值运算符以及复制构造函数begin()返回指向开头元素的迭代器end()返回指向末尾的下一个元素的迭代器end()不指向某个元素但它是末尾元素的后继size()返回容器内的元素个数max_size()返回容器理论上能存储的最大元素个数依容器类型和所存储变量的类型而变empty()返回容器是否为空swap()交换两个容器clear()清空容器/!////按字典序比较两个容器的大小关于比较运算符有几个细节值得注意比较元素大小时map的每个元素相当于setpairkey, value无序容器不支持///这些关系比较哈希实现无法定义有意义的有序关系swap()的复杂度随容器而异vector、deque、list、set、map等常规容器的swap是 $O(1)$常数复杂度而交换两个array是 $\Theta(\text{size})$ 的因为array不支持动态内存只能逐元素交换。序列式容器深度解析vector动态数组的工程与竞赛平衡std::vector是内存连续、可变长度的数组列表数据结构能够提供线性复杂度的插入删除和常数复杂度的随机访问。竞赛选手通常以静态数组为主力但vector在三个场景中价值突出动态分配内存当无法提前确定需要多大空间时例如预处理 $1\sim n$ 中所有数的约数vector能把内存占用量控制在合适范围内并支持动态扩容。重写了比较运算符及赋值运算符vector重载了六个比较运算符字典序实现方便判断两个容器是否相等复杂度与容器大小成线性关系例如可以用vectorchar实现字符串比较当然std::string更快更方便。便利的初始化从 C11 起支持列表初始化如vectorint data {1, 2, 3};配合运算符可方便地整体赋值。vector的构造方式覆盖多种场景// 1. 创建空 vector常数复杂度 vectorint v0; // 1. 预分配容量使插入前 3 个元素时保证常数时间复杂度 v0.reserve(3); // 2. 创建初始空间为 3 的 vector元素默认值为 0线性复杂度 vectorint v1(3); // 3. 创建初始空间为 3 的 vector元素默认值为 2线性复杂度 vectorint v2(3, 2); // 4. 创建初始空间为 3 的 vector元素默认值为 1并使用 v2 的空间配置器线性复杂度 vectorint v3(3, 1, v2.get_allocator()); // 5. 创建 v2 的拷贝 v4线性复杂度 vectorint v4(v2); // 6. 创建 v4 的部分拷贝内容是 {v4[1], v4[2]}线性复杂度 vectorint v5(v4.begin() 1, v4.begin() 3); // 7. 移动 v2 到新 vector v6不发生拷贝常数复杂度需要 C11 vectorint v6(std::move(v2)); // 或者 v6 std::move(v2);元素访问有五种途径v.at(pos)返回下标pos的引用越界抛出std::out_of_range异常v[pos]返回下标pos的引用不执行越界检查v.front()返回首元素的引用v.back()返回末尾元素的引用v.data()返回内部连续内存空间首元素的指针。长度与容量是两个必须区分开的概念size()长度是有效元素数量capacity()容量是已分配内存最多能容纳的元素数量。相关函数包括empty()、size()、resize(n)改变长度变大则补默认值变小则截断、max_size()、reserve()预留内存避免不必要的重新分配与拷贝、capacity()、shrink_to_fit()使容量与长度一致释放多余空间。元素增删及修改的复杂度需要牢记clear()清除所有元素insert()在某个迭代器位置插入元素复杂度与pos距离末尾长度成线性而非常数erase()删除迭代器或区间的元素复杂度与insert一致push_back()末尾插入均摊复杂度为常数最坏为线性pop_back()删除末尾元素常数复杂度swap()交换容器常数复杂度而非线性。实现细节vector底层仍是定长数组动态扩容的原理是容量检查——容器分别存储长度 $n$ 与容量 $N$当添加元素发现 $n N$ 时分配一个尺寸为 $2N$ 的新数组把旧数据拷贝过去再释放原内存。尽管单次扩容渐近复杂度为 $O(n)$但可证明其均摊复杂度为 $O(1)$。因此只要对vector尺寸估计得当、善用resize()和reserve()其效率与定长数组不会有太大差距。特别警告vectorbool标准库为bool提供了特化版本每个bool只占 1 bit 且支持动态增长但其operator[]返回类型不是bool而是vectorbool::reference使用上极易出错。如需节省空间推荐直接使用bitset也可用dequebool或vectorchar替代。arrayC11零开销的定长数组封装std::array是内存连续、固定长度的数组数据结构本质是对原生数组的直接封装。它牺牲了vector的动态扩容特性换来了与原生数组几乎一致的性能在开满优化的前提下。因此能使用 C11 时几乎可以用array替换所有定长数组用vector替换动态分配数组。其成员函数按类别整理如下元素访问at越界检查pos size()时抛std::out_of_range、operator[]不检查、front、back、data返回指向内存中数组首元素的指针。容量empty、size、max_size。由于array固定大小size()恒等于max_size()。操作fill以指定值填充、swap交换内容注意是 $\Theta(\text{size})$ 的。非成员函数operator等按字典序比较、std::get访问元素、特化的std::swap。使用示例// 1. 创建空 array长度为 3常数复杂度 std::arrayint, 3 v0; // 2. 用指定常数创建 array常数复杂度 std::arrayint, 3 v1{1, 2, 3}; v0.fill(1); // 填充数组 // 访问数组 for (int i 0; i ! v1.size(); i) cout v1[i] ;deque双端高效的双端队列std::deque双端队列提供线性复杂度的插入删除和常数复杂度的随机访问数据结构本身可参见 双端队列。其构造、迭代器、元素访问与vector基本一致at、operator[]、front、back均为常数复杂度但没有reserve()和capacity()函数仍有shrink_to_fit()也无法访问底层连续内存。增删操作相比vector额外支持头部操作push_front()/pop_front()头部插入/删除常数复杂度push_back()/pop_back()尾部插入/删除常数复杂度insert()/erase()复杂度与pos到两端距离的较小者成线性优于vector的单向线性。实现细节deque通常的底层实现是多个不连续的缓冲区缓冲区内部内存连续每个缓冲区记录首指针和尾指针标记有效数据区间一个缓冲区填满后会在之前或之后分配新缓冲区存储更多数据。这一结构使得它能在两端都高效增删同时保持接近数组的随机访问性能。list与forward_list链表家族std::list是双向链表参见 链表提供线性复杂度的随机访问和常数复杂度的插入删除。由于实现是链表它不提供随机访问接口访问中间元素必须使用迭代器仅有front()和back()。list还针对链表特性提供了 STL 算法的专有实现因为通用算法库的版本需要随机访问迭代器不适用于链表splice()拼接、remove()按值删除、sort()归并排序、unique()去重、merge()归并等。std::forward_listC11是单向链表相比list减小了空间开销但迭代器只有单向的功能受限。关联式容器深度解析有序的红黑树世界set、multiset、map、multimap四个有序关联容器通常基于 红黑树 实现搜索、移除和插入均为对数复杂度非常适合处理需要同时兼顾查找、插入与删除的场景。set互异元素的有序集合set是含有键值类型对象的已排序集与数学中的集合相似不会出现值相同的元素。需要相同元素时使用multiset用法基本相同。插入与删除操作insert(x)当容器中没有等价元素时插入 xerase(x)删除值为 x 的所有元素返回删除元素个数erase(pos)删除迭代器为 pos 的元素迭代器必须合法erase(first, last)删除[first, last)范围内的元素clear()清空容器。insert的返回值类型为pairiterator, bool。iterator指向所插入元素或容器中已存在的等值元素bool表示是否插入成功。由于set元素唯一若已有等值元素则插入失败返回false否则返回true。map的insert同理。查找操作count(x)返回键为 x 的元素数量find(x)存在则返回该元素迭代器否则返回end()lower_bound(x)返回首个不小于给定键的元素迭代器不存在则返回end()upper_bound(x)返回首个大于给定键的元素迭代器不存在则返回end()empty()/size()判空与元素个数。两个必须警惕的复杂度陷阱set自带的lower_bound/upper_bound是 $O(\log n)$但使用algorithm库中的lower_bound/upper_bound对set查询时间复杂度为$O(n)$因为其要求随机访问迭代器对链表式红黑树只能线性推进set没有自带nth_element使用algorithm库的nth_element找第 $k$ 大元素是$O(n)$。若要实现 $O(\log n)$ 的查第 $k$ 大需手写平衡二叉树/权值线段树或使用 pb_ds 库中的平衡二叉树。实战样例贪心中的删除最小的大于等于某值元素。贪心算法中经常出现找出并删除最小的大于等于某个值的元素这种操作set可以轻松完成// 现存可用的元素 setint available; // 需要大于等于的值 int x; // 查找最小的大于等于x的元素 setint::iterator it available.lower_bound(x); if (it available.end()) { // 不存在这样的元素则进行相应操作…… } else { // 找到了这样的元素将其从现存可用元素中移除 available.erase(it); // 进行相应操作…… }map以任意有序类型为下标的键值映射map是有序键值对容器键唯一搜索、移除和插入均为对数复杂度通常实现为红黑树。典型场景存储学生姓名对应的分数Tom 0、Bob 100、Alan 100。数组下标只能是非负整数无法用姓名作下标此时最简方案就是map。map重载了operator[]可以用任意定义了operator 的类型作为下标在map中称为keymapKey, T yourMap; // Key 是键的类型T 是值的类型 mapstring, int mp; // 实例map中不存在键相同的元素multimap允许多个元素拥有同一键用法基本相同但正因为允许键重复multimap没有提供按键直接访问对应值的方法。插入与删除操作直接通过下标访问进行查询或插入如mp[Alan] 100插入pairKey, T类型的值如mp.insert(pairstring,int(Alan,100));erase(key)删除键为 key 的所有元素返回删除数量erase(pos)/erase(first, last)按迭代器删除要求迭代器合法clear()清空容器。下标访问注意事项利用下标访问map中不存在的键时会自动插入一个新元素并将其值设置为默认值整数为零有默认构造函数的类型会调用默认构造函数。当下标访问过于频繁时容器中会出现大量无意义元素影响效率。因此一般推荐用find()查找特定键的元素仅在确实需要写入时才用下标。查询操作count(x)复杂度 $O(\log(\text{size}) ans)$即对数复杂度加上匹配个数、find(x)、lower_bound(x)、upper_bound(x)、empty()、size()。实战样例用map存储复杂状态。搜索中经常需要存储复杂状态坐标、无法离散化的数值、字符串等及其对应答案如到达该状态的最小步数// 存储状态与对应的答案 mapstring, int record; // 新搜索到的状态与对应答案 string status; int ans; // 查找对应的状态是否出现过 mapstring, int::iterator it record.find(status); if (it record.end()) { // 尚未搜索过该状态将其加入状态记录中 record[status] ans; // 进行相应操作…… } else { // 已经搜索过该状态进行相应操作…… }遍历关联容器可以利用迭代器遍历关联式容器的所有元素时间复杂度均为 $O(n)$setint s; using si setint::iterator; for (si it s.begin(); it ! s.end(); it) cout *it endl;注意对map的迭代器解引用后得到的是类型为pairKey, T的键值对。C11 中可用范围 for 循环简化setint s; for (auto x : s) cout x endl;自定义比较方式set默认的比较函数为非内置类型需要重载运算符。若想自定义比较方式可定义一个类并重载()运算符然后作为第二个模板参数传入。例如维护一个较大值靠前的setstruct cmp { bool operator()(int a, int b) const { return a b; } }; setint, cmp s;对于其他关联式容器可以用类似方式实现自定义比较。容器适配器深度解析栈、队列与优先队列适配器不是容器它们包装底层容器并限制其接口表现为特定行为LIFO/FIFO/堆。均不支持迭代器与随机访问以保证数据的严格有序性。栈stack后进先出#include stack std::stackTypeName s; // 使用默认底层容器 deque std::stackTypeName, Container s; // 使用 Container 作为底层容器 std::stackTypeName s2(s1); // 将 s1 复制一份用于构造 s2成员函数均为常数复杂度top()访问栈顶元素栈空时出错、push(x)插入元素、pop()删除栈顶元素、size()查询元素数量、empty()判空。std::stackint s1; s1.push(2); s1.push(1); std::stackint s2(s1); s1.pop(); std::cout s1.size() s2.size() std::endl; // 1 2 std::cout s1.top() s2.top() std::endl; // 2 1 s1.pop(); std::cout s1.empty() s2.empty() std::endl; // 1 0队列queue先进先出#include queue std::queueTypeName q; // 使用默认底层容器 deque std::queueTypeName, Container q; // 使用 Container 作为底层容器 std::queueTypeName q2(q1); // 将 q1 复制一份用于构造 q2成员函数均为常数复杂度front()访问队首元素队空时出错、push(x)、pop()删除队首元素、size()、empty()。std::queueint q1; q1.push(2); q1.push(1); std::queueint q2(q1); q1.pop(); std::cout q1.size() q2.size() std::endl; // 1 2 std::cout q1.front() q2.front() std::endl; // 1 2 q1.pop(); std::cout q1.empty() q2.empty() std::endl; // 1 0优先队列priority_queue二叉堆std::priority_queue本质是一个 堆一般为 二叉堆。std::priority_queueTypeName q; // 数据类型为 TypeName std::priority_queueTypeName, Container q; // 使用 Container 作为底层容器 std::priority_queueTypeName, Container, Compare q; // 使用 Container 作为底层容器使用 Compare 作为比较类型 // 默认使用底层容器 vector // 默认比较类型 lessTypeName此时 top() 返回最大值 // 若希望 top() 返回最小值可令比较类型为 greaterTypeName // 注意不可跳过 Container 直接传入 Compare // 从 C11 开始如果使用 lambda 函数自定义 Compare // 则需要将其作为构造函数的参数代入如 auto cmp [](const std::pairint, int l, const std::pairint, int r) { return l.second r.second; }; std::priority_queuestd::pairint, int, std::vectorstd::pairint, int, decltype(cmp) pq(cmp);成员函数常数复杂度top()访问堆顶队列不能为空、empty()、size()对数复杂度push(x)插入并维护堆序、pop()删除堆顶队列不能为空。std::priority_queueint q1; std::priority_queueint, std::vectorint q2; // C11 后模板参数间空格可省略 std::priority_queueint, std::dequeint, std::greaterint q3; // q3 为小根堆 for (int i 1; i 5; i) q1.push(i); // q1 中元素 : [1, 2, 3, 4, 5] std::cout q1.top() std::endl; // 输出结果 : 5 q1.pop(); // 堆中元素 : [1, 2, 3, 4] std::cout q1.size() std::endl; // 输出结果 4 for (int i 1; i 5; i) q3.push(i); // q3 中元素 : [1, 2, 3, 4, 5] std::cout q3.top() std::endl; // 输出结果 : 1无序容器效率与风险平均 O(1) 与最坏 O(n) 的双面性无序关联式容器unordered_set/unordered_multiset/unordered_map/unordered_multimap采用哈希存储平均情况下大多数操作查找、插入、删除为常数复杂度优于红黑树容器与容器大小成对数的时间复杂度。但必须注意两个重要警告最坏情况退化为线性当容器内出现大量哈希冲突时插入、删除、查找等操作的时间复杂度会与容器大小成线性关系常数通常较大由于哈希操作存在较大常数其效率有时并不比普通关联式容器好太多。因此应谨慎使用尽量避免滥用例如懒得离散化直接将unordered_mapint, int当作空间无限的普通数组使用。在 C11 之前无序容器属于 C 的 TR1 扩展需要将#include unordered_map改为#include tr1/unordered_map并使用std::tr1命名空间。制造哈希冲突理解复杂度退化的根源在哈希函数确定的情况下可以构造数据使容器产生大量哈希冲突将复杂度推向最坏上界。在标准库实现中每个元素的散列值是将值对一个质数取模得到的g 6 及以前版本该质数一般是126271g 7 及之后一般是107897。因此可以通过向容器插入这些模数的倍数来制造大量哈希冲突——这正是出题人构造数据卡unordered_map的原理也是选手需要警惕的坑。自定义哈希函数抵御构造数据使用自定义哈希函数可以有效避免构造数据产生的大量哈希冲突。定义方式为定义一个结构体并重载()运算符struct my_hash { size_t operator()(int x) const { return x; } };为了确保哈希函数不会被迅速破解例如在允许 hack 的比赛中针对无序容器提交构造数据可以在哈希函数中加入随机化因素如时间。一个经典且被广泛使用的抗 hack 哈希如下基于splitmix64与运行时随机种子struct my_hash { static uint64_t splitmix64(uint64_t x) { x 0x9e3779b97f4a7c15; x (x ^ (x 30)) * 0xbf58476d1ce4e5b9; x (x ^ (x 27)) * 0x94d049bb133111eb; return x ^ (x 31); } size_t operator()(uint64_t x) const { static const uint64_t FIXED_RANDOM chrono::steady_clock::now().time_since_epoch().count(); return splitmix64(x FIXED_RANDOM); } // 针对 std::pairint, int 作为主键类型的哈希函数 size_t operator()(pairuint64_t, uint64_t x) const { static const uint64_t FIXED_RANDOM chrono::steady_clock::now().time_since_epoch().count(); return splitmix64(x.first FIXED_RANDOM) ^ (splitmix64(x.second FIXED_RANDOM) 1); } };使用时作为第三个模板参数传入unordered_mapint, int, my_hash my_map;或unordered_mappairint, int, int, my_hash my_pair_map;。容器选型速查与实战建议需求推荐容器核心复杂度特征随机访问 尾部增删vector访问 $O(1)$尾插均摊 $O(1)$中部插入 $O(n)$定长数组、追求极致性能arrayC11与原生数组几乎一致swap为 $\Theta(\text{size})$双端高效增删 随机访问deque两端增删 $O(1)$随机访问 $O(1)$频繁中间插入/删除list/forward_list已知位置插入删除 $O(1)$随机访问 $O(n)$有序互异集合、区间查询set/multiset增删查 $O(\log n)$自带 $O(\log n)$ 的lower_bound键值映射键有序map/multimap增删查 $O(\log n)$只关心存在性、键无序unordered_set/unordered_map平均 $O(1)$最坏 $O(n)$需警惕哈希冲突LIFO / FIFO / 取最值stack/queue/priority_queue核心操作常数或对数复杂度无迭代器最后给出三条贯穿全文的实战提醒复杂度陷阱set/map的自带lower_bound是 $O(\log n)$但algorithm版本的lower_bound对树状容器是 $O(n)$set查第 $k$ 大也没有 $O(\log n)$ 的原生方案。下标操作副作用map的下标访问会隐式插入默认值元素查询请优先用find()unordered_map亦同。vectorbool是特例其operator[]返回vectorbool::reference而非bool需要位集时用 bitset否则用dequebool或vectorchar。结合仓库中的真实代码可以进一步印证这些选型例如 单调队列/栈优化示例 使用deque维护滑动窗口最值四边形不等式优化示例 使用priority_queue维护堆顶元素前缀和示例 使用vector存储序列数据。读者在阅读这些代码时可以对照本文的复杂度表格理解作者为何选择特定容器——这正是掌握 STL 容器体系的最终价值所在。【免费下载链接】OI-wiki:star2: Wiki of OI / ICPC for everyone. 某大型游戏线上攻略内含炫酷算术魔法项目地址: https://gitcode.com/GitHub_Trending/oi/OI-wiki创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
