直接开干蓝桥杯备赛STL和基本数学为什么是第一优先级每年蓝桥杯备赛季我都会收到一堆“现在学还来得及吗”“STL到底要不要背”之类的问题。说实话蓝桥杯这种以算法为主的竞赛STL和基本数学恰恰是所有参赛者最该先花时间吃透的两块内容。你不会图论、不会DP很多题做不出来那很正常但如果你连vector和sort都用不利索连gcd和快速幂都要现场推半小时那真的就是白白送分。这篇文章不聊虚的直接把我历次参赛和日常带新人备赛时沉淀下来的STL容器用法、基本数学模板、常见坑位全部拆开讲。面向的是准备参加蓝桥杯的人群不管是第一次参加的萌新还是想补短板的二次参赛选手都可以照着这篇文章的节奏走。每一节都有可直接复制的代码和对应的使用场景“为什么这么做”也会一并说清楚。1. 蓝桥杯题目到底在考什么STL和基本数学的定位1.1 蓝桥杯题型的底层规律蓝桥杯从省赛到国赛题型结构大体上稳定在“若干道填空 若干道编程大题”的格局。填空题最典型的解法其实不是“背答案”而是用暴力枚举、全排列、搜索硬算出来的而这些操作高度依赖STL的容器和算法函数。编程大题则是围绕枚举、模拟、搜索、动态规划、图论、数论等常见套路展开其中模拟题占比非常可观。我经常跟备赛的同学说一句话**蓝桥杯的高频场景里STL是“手”基本数学是“眼”。**手不熟就写得慢眼不够就看不穿题目的本质。很多题目看着是“难题”扒掉外壳之后无非是一个stack模拟括号匹配、一个map统计次数、一个priority_queue做TopK。而这些操作你只要把STL用熟写起来速度能比手撸数据结构快两三倍。那有人会问蓝桥杯允许用STL吗允许当然允许。蓝桥杯使用的是标准C环境标准模板库随便用。把STL当成“偷懒”是完全错误的想法——比赛拼的是谁能在有限时间内把解法落地STL本身就是C的一部分用熟了就是核心竞争力。1.2 基本数学考查的是哪几板斧“基本数学”听起来范围很大但蓝桥杯真正高频出现的数学知识点其实非常集中最大公约数和最小公倍数、素数判定与筛法、快速幂、前缀和与差分、组合数、位运算。这些知识点有个共同特点——它们本身不难但经常作为题目的“前置条件”出现比如让你先求一堆数的最小公倍数再用它做状态转移或者让你在计算过程中反复取模防溢出。我见过太多选手栽在数学细节上gcd模板背错了、快速幂忘了long long直接爆掉、组合数用递推溢出。这些扣分不是不会而是不熟。本篇文章后面会把这些模板全部整理出来配合赛场上最常用的写法保证你背下来就能上考场。2. STL核心容器实战拆解从会用到用好2.1 vector与string最高频的两个容器vector是STL里的“万金油”容器动态数组的特性决定了它既可以当普通数组用也可以当栈的平替。蓝桥杯里最常见的vector场景是存图的邻接表、枚举时存临时结果、配合sort做排序、当二维数组的行指针容器等。#include bits/stdc.h using namespace std; int main() { vectorint a; // 空数组 a.push_back(5); // 尾部添加均摊O(1) a.push_back(3); a.push_back(9); sort(a.begin(), a.end()); // 排序O(n log n) // 输出3 5 9 vectorvectorint g(10); // 10个空vector邻接表常见写法 g[1].push_back(2); g[1].push_back(3); vectorint b(5, -1); // 5个-1 fill(b.begin(), b.end(), 0); // 全部填0 return 0; }vector有三个细节必须注意。第一reserve和resize的区别。reserve只预留内存不改变sizeresize会改变size且新元素默认初始化。提前reserve能避免频繁扩容造成的性能损耗但普通题目数据量不大时不敏感。第二删除元素用erase配合remove比如a.erase(remove(a.begin(), a.end(), val), a.end())这是按值删除的标准写法直接erase中间元素会移动后面所有元素慢。第三迭代器失效问题往vector里push_back导致重新分配内存后之前保存的迭代器可能全部失效比赛时尽量避免保存vector元素的迭代器。string是处理字符串题目的首选蓝桥杯里经常出现日期格式化、串匹配、字符串压缩这类题。string的find、substr、erase这几个API要背熟。stoi和to_string是类型转换最方便的两个函数注意C11以上才稳定支持。string s abc123def; int pos s.find(123); // 返回下标3找不到返回 string::npos string t s.substr(3, 3); // 123 s.erase(pos, 3); // 删除3个字符 string num to_string(2025); // 2025 int val stoi(num); // 2025注意find找不到时返回npos判断要用if (pos string::npos)千万别和-1比较之后再踩坑。实际开发中我见过太多次因为这句判断写错导致index out of range的崩溃。2.2 stack、queue与priority_queue模拟题的三大件栈和队列是蓝桥杯模拟题的常客尤其是括号匹配、表达式求值、BFS迷宫这类经典考法。优先队列堆则经常用于贪心题和TopK问题。stackint st; st.push(1); st.pop(); st.top(); // 栈顶 queueint q; q.push(1); q.pop(); q.front(); // 队首 priority_queueint pq; // 默认大顶堆 pq.push(3); pq.push(1); pq.push(2); // top()为3 // 小顶堆要这样写 priority_queueint, vectorint, greaterint minpq;优先队列默认是大顶堆这在“每次取最大”的贪心场景里非常自然。如果需要小顶堆就必须手写那三个模板参数很多人第一次接触会觉得怪但背下来就好因为greaterint在绝大多数OJ环境里都可用。自定义结构体进优先队列时要重载运算符这是蓝桥杯里最容易卡住的地方。正确做法是在结构体内部重载小于号注意大顶堆和小顶堆时重载语义相反。struct Node { int dist, id; bool operator (const Node other) const { return dist other.dist; // 注意这样写出来是小顶堆 } }; priority_queueNode pq;我们以Dijkstra为例分析一下这个坑。优先队列默认是大顶堆即“比较大的元素排前面”所以如果想让权值小的先出队重载小于号时要让“dist大的Node”被认为是“小于”“dist小的Node”这样dist小堆顶的就成了优先级最高的。很多新手理不清这一点代码里测试样例能过一到斜率和边界数据就错。2.3 map、set与unordered容器查找与去重的利器map和set底层是红黑树插入、删除、查找都是O(log n)。unordered_map和unordered_set底层是哈希表查找均摊O(1)。蓝桥杯里map最常见的场景是“统计频次”比如字符统计、单词次数统计。mapstring, int cnt; cnt[apple]; cnt[apple]; cout cnt[apple] \n; // 输出2 // 遍历 for (auto p : cnt) { cout p.first p.second \n; }map的[]运算符有个隐藏行为访问不存在的key时会自动插入一个默认值。这有时候是好事比如统计频次时直接cnt[apple]很方便但有时候是坑比如你想“查询一下有没有这个key”用if (cnt[key])会误插入一条脏数据正确做法是用count(key)或find(key)。set用来去重和判重典型场景是“判断某个数是否已经存在”。multiset允许重复元素可以替代一个可重复的优先队列但实际比赛用它不如用priority_queue多。还有一个冷门但好用的容器是bitset在处理状态压缩、位运算相关题目时非常高效你可以把bitset当成一个可以按位操作的布尔数组蓝桥杯里出现二进制枚举时能用它让代码简洁很多。setint s; s.insert(3); s.insert(3); s.insert(1); // size()为2自动去重 s.erase(1); if (s.count(3)) { /* 存在 */ } unordered_setint us; us.insert(100); if (us.find(100) ! us.end()) { /* 存在 */ }用unordered容器时键值类型必须支持哈希自定义结构体需要手动提供哈希函数这个在蓝桥杯高级题里偶尔会遇到。普通情况下用int、string这些内置类型就行。2.4 algorithm库里的高频函数盘点algorithm库提供了大量算法函数蓝桥杯场景下最常用的就这几个sort、lower_bound/upper_bound、next_permutation、reverse、max_element/min_element、unique。sort的稳定性是个容易被忽略的问题。sort内部是快排的实现不稳定如果题目要求按某种规则排序但需要保持相等元素的原始相对顺序应该用stable_sort。sort还可以自定义比较函数比如按绝对值排序、按结构体里某个字段排序。sort(v.begin(), v.end()); // 升序 sort(v.begin(), v.end(), greaterint()); // 降序 stable_sort(v.begin(), v.end()); // 稳定排序 // 自定义比较 sort(v.begin(), v.end(), [](int x, int y) { return abs(x) abs(y); // 按绝对值升序 });lower_bound和upper_bound是二分查找利器适用于有序序列。lower_bound返回第一个“大于等于”目标值的位置upper_bound返回第一个“大于”目标值的位置。两者相减可以得到某个值在数组里出现的次数。vectorint v {1, 3, 5, 5, 7, 9}; auto it1 lower_bound(v.begin(), v.end(), 5); // 指向第一个5 auto it2 upper_bound(v.begin(), v.end(), 5); // 指向7 int cnt it2 - it1; // 5出现的次数2next_permutation是暴力枚举全排列的核心函数。蓝桥杯填空里经常出现“多少种排列满足某条件”的题直接枚举全排列然后判断即可。注意next_permutation默认按字典序从小到大生成用前最好先排序。vectorint p {1, 2, 3}; do { // 处理当前排列 for (int x : p) cout x; cout \n; } while (next_permutation(p.begin(), p.end()));unique配合erase是“去重”的标准三段式。先sort再unique把重复元素移动到容器尾部最后erase删掉尾部多余元素。这个组合我在填空题“集合去重后求元素个数”里用了无数次。sort(v.begin(), v.end()); v.erase(unique(v.begin(), v.end()), v.end());实操心得在比赛代码里我一般直接写#include bits/stdc.h把整个标准库一次性引入。蓝桥杯官方编译器支持这个头文件能省很多记忆量和编译报错时间。只是新手要清楚这不是标准头文件换到某些非竞赛环境下不一定能通过编译。3. 基本数学模块模板从这里开始积累3.1 最大公约数与最小公倍数最大公约数用欧几里得算法写递归版本简洁迭代版本更稳妥。最小公倍数等于两数乘积除以它们的最大公约数。这里有个极其经典的坑先乘后除会溢出。所以必须写成a / gcd(a,b) * b的顺序。typedef long long ll; ll gcd(ll a, ll b) { return b 0 ? a : gcd(b, a % b); } ll lcm(ll a, ll b) { return a / gcd(a, b) * b; // 先除后乘防溢出 }读入数据时如果是int范围内的数可能不觉得先乘后除有问题但如果题目给了1e9级别的数直接a * b就已经爆int了。蓝桥杯题目里$a$、$b$范围到$10^9$很常见long long是保命底线。扩展一点gcd还有一个等效写法是std::gcd在algorithm或numeric头文件里C17提供。但竞赛中自己手写一个更稳不依赖编译器标准版本。3.2 素数判定与筛法素数判定最基础的是试除法判断到根号n即可。但蓝桥杯经常需要判大量素数试除法不够用这时候就要上筛法。埃氏筛适用于$10^7$以内的数据量时间复杂度O(n log log n)实现简单。欧拉筛线性筛能保证每个合数只被筛掉一次时间复杂度严格O(n)是追求极致性能时的标准答案。const int MAX 1000000; bool isPrime[MAX 1]; void sieve() { fill(isPrime, isPrime MAX 1, true); isPrime[0] isPrime[1] false; for (int i 2; i * i MAX; i) { if (isPrime[i]) { for (int j i * i; j MAX; j i) { isPrime[j] false; } } } }埃氏筛里有个细节j从i * i开始筛而不是从2 * i开始因为小于i * i的合数已经在更小的素数处理时被标记过了。这虽然是个小优化但在复杂度分析时体现了埃氏筛“去冗余”的核心思想。欧拉筛稍微复杂一点但它能同时预处理每个数的最小质因子这是做质因子分解和线性筛莫比乌斯函数的前置能力。vectorint primes; bool notPrime[MAX 1]; void linearSieve() { for (int i 2; i MAX; i) { if (!notPrime[i]) primes.push_back(i); for (int p : primes) { if (i * p MAX) break; notPrime[i * p] true; if (i % p 0) break; } } }如果只是单次判断一个数是不是素数用试除法就可以但如果题目是“求区间内的所有素数”或者“多次询问素数性”那就直接上筛法。蓝桥杯国赛的搜索题里经常预处理一张素数表用来剪枝这时候错一个筛法实现可能直接导致超时。3.3 快速幂从乘方到矩阵快速幂是蓝桥杯数论题的入门必备本质是二分幂$a^b$可以不断把指数除以2底数平方根据指数的二进制位决定是否累乘。复杂度O(log b)。核心代码如下ll quickPow(ll a, ll b, ll mod) { ll res 1; a % mod; while (b) { if (b 1) res res * a % mod; a a * a % mod; b 1; } return res; }代码里三个细节值得展开。其一a % mod是在进入循环前先让底数落在mod范围内避免第一次乘法就溢出。其二b 1用来判断当前二进制位是否为1等价于b % 2 1位运算更快。其三每次循环都要a a * a % mod因为即使当前位不乘底数也要为下一位平方做准备。矩阵快速幂是快速幂的进阶版核心思想一模一样只是把数的乘法换成矩阵乘法。蓝桥杯里出现矩阵快速幂往往是配合斐波那契类递推、图上路径计数等问题。矩阵乘法部分要单独封装函数注意矩阵乘法的三层循环顺序以及中途取模防止溢出。矩阵快速幂模板我会在进阶准备中单独给出一版这里先记住“快速幂的思想 重载乘法运算”这个关键点。3.4 前缀和与差分数组上的小学数学前缀和用来快速求区间和差分用来快速做区间增减。这两个技巧单独看简单但它们是很多“看似需要数据结构”题目的隐藏解法。一维前缀和核心代码vectorint a(n 1), s(n 1, 0); for (int i 1; i n; i) { cin a[i]; s[i] s[i - 1] a[i]; } // 查询[l, r]区间和 int sum s[r] - s[l - 1];这里为什么会想到用前缀和因为如果每次都遍历区间求和复杂度是O(n)一次查询多次查询就是O(nm)超时。前缀和把查询降到了O(1)代价只是O(n)预处理。这就是典型的“空间换时间”蓝桥杯里大量题目吃这套思路。差分则反过来适用于“多次把某区间所有元素加同一个值最后求整个数组”的场景。差分的修改是O(1)最后求一次前缀和恢复原数组。vectorint diff(n 2, 0); // 区间[l, r]加上val diff[l] val; diff[r 1] - val; // 最后统一前缀和还原 for (int i 1; i n; i) { diff[i] diff[i - 1]; cout diff[i] ; }注意差分结束后要输出原数组的值必须把它累加恢复出来。经常有选手建了差分数组却忘记恢复结果全是差分数值白丢分。二维前缀和蓝桥杯也考过多次特别是矩阵求和类题目。公式是s[i][j] s[i-1][j] s[i][j-1] - s[i-1][j-1] a[i][j]查区间时用“大的减两块加一块”。这个公式初次接触容易背串建议自己手推一遍理解“加重复了需要减掉重叠部分”的容斥逻辑。4. 组合数学与位运算进阶必踩的坑4.1 组合数的三种写法组合数$C(n, m)$在蓝桥杯里出现在概率、递推、状态转移等题目中常见的有三种写法。数据范围小时用杨辉三角递推单次计算但n不大时用阶乘逆元n非常大时用Lucas定理。杨辉三角递推写法最简单适合n在1000以内的场景long long C[1005][1005]; void initC() { for (int i 0; i 1000; i) { C[i][0] C[i][i] 1; for (int j 1; j i; j) { C[i][j] (C[i - 1][j - 1] C[i - 1][j]) % MOD; } } }这个递推式的直觉是“第i个元素选还是不选”。选的情况是$C(i-1,j-1)$不选的情况是$C(i-1,j)$。理解了这一点就不需要死背二维数组的更新顺序。阶乘逆元写法更通用适合n到1e6的场景。这需要配合快速幂求逆元理解费马小定理模数是素数时$a^{p-2}$就是$a$的逆元。ll fac[MAXN], invFac[MAXN]; ll qpow(ll a, ll b, ll mod) { ll ans 1; while (b) { if (b 1) ans ans * a % mod; a a * a % mod; b 1; } return ans; } void init(int n) { fac[0] 1; for (int i 1; i n; i) fac[i] fac[i - 1] * i % MOD; invFac[n] qpow(fac[n], MOD - 2, MOD); for (int i n; i 1; i--) invFac[i - 1] invFac[i] * i % MOD; } ll C(int n, int m) { if (m 0 || m n) return 0; return fac[n] * invFac[m] % MOD * invFac[n - m] % MOD; }这个模板里invFac倒着推有个好处一次快速幂拿到invFac[n]之后用阶乘的递推关系倒推出所有invFac比每个都求快速幂快得多。Lucas定理用于n和m很大但模数较小的场景公式是$C(n,m) \equiv C(n/p, m/p) \times C(n%p, m%p) \pmod p$。蓝桥杯主流分组中这个考得不频繁但国赛偶尔出现属于“会了就多一层保障”的储备模板。4.2 位运算实用技巧位运算在蓝桥杯里经常作为“奇技淫巧”出现比大整数快代码也更简练。最常用的是判断奇偶x 1取二进制第k位(x k) 1乘2除以2直接移位交换两个数a ^ b; b ^ a; a ^ b;判断是否是2的幂x 0 (x (x - 1)) 0。枚举子集是位运算在组合题里的精华用法。一个集合可以用一个整数表示某一位为1表示包含该元素那么枚举所有子集就是枚举0到1n之间的所有整数int n 3; for (int mask 0; mask (1 n); mask) { // mask的二进制位表示当前选取的状态 for (int i 0; i n; i) { if (mask (1 i)) { // 选取了第i个元素 } } }这种“状态压缩”写法在蓝桥杯填空题里特别常用尤其是“枚举所有可能的选择组合后判断条件是否满足”的题目。n不超过20时2^n的枚举是可行的n超过25就要警惕超时了。避坑提醒1 n默认是int运算n 31时直接溢出。如果n可能更大要用1LL n。同时注意位运算优先级低于比较运算符写条件时一定要加括号比如if ((mask (1 i)) 0)漏掉括号会出非常隐蔽的bug。5. 常见问题与排查技巧实录5.1 STL使用中的经典坑第一个高频坑迭代器失效。vector在插入、删除后之前拿到的迭代器可能失效。特别是循环里边遍历边删除最容易翻车。推荐做法是先用remove_if把满足条件的元素挪到尾部再统一erase或者用erase返回下一个有效迭代器的写法。vectorint v {1, 2, 3, 4, 5}; for (auto it v.begin(); it ! v.end(); ) { if (*it % 2 0) it v.erase(it); // erase返回下一个有效迭代器 else it; }第二个高频坑sort自定义比较器没严格弱序。比较器必须满足“如果a不比b小且b不比a小则a和b等价”如果写出的比较器违反这个规则比如把return a b写成返回true会导致sort未定义行为甚至运行崩溃。比较器里永远用或不要用和。第三个高频坑map的[]运算符意外插入。查询用count或find尤其在需要判断key是否存在时。还有一个相关坑是遍历map时修改当前元素map的键不可修改但值可以修改如果改了键代码直接编译错误。要修改键的唯一办法是删除旧键插入新键。第四个高频坑cin/cout没有关同步。蓝桥杯的数据量有些题比较大默认同步的iostream可能超时。开场写一句ios::sync_with_stdio(false); cin.tie(nullptr);是常规操作但注意和scanf/printf混用就会出问题关同步之后就不要再用C风格的输入输出去读同一数据流了。5.2 基本数学模板的经典错误快速幂最常见的错误是忘记取模或先乘再模导致溢出。你写res res * a % mod时如果res和a都已经对mod取了模乘积最大是(mod-1)*(mod-1)大约mod^2 - 2mod 1mod在1e9级别时这已经超过int的范围所以相关变量必须用long long。gcd递归版有个边界细节gcd(0, a)返回a因为任何数和0的最大公约数是这个数本身。但如果你用负数调用gcdC的%结果符号可能不符合预期所以最好在函数入口对参数取绝对值。素数筛的经典错误是筛的区间越界。埃氏筛内层循环j i如果写成j i * i就会漏筛。另外如果用vectorbool它虽然省空间但底层的位压缩可能导致某些操作变慢如果追求性能可以直接用vectorchar。组合数取模的经典错误是不判断m n的情况。当输入数据里m可能大于n而你的组合数函数没有处理时会返回一个奇怪的大数。正确写法是开头加if (m 0 || m n) return 0;这也是国际OJ上常见的WA点。5.3 比赛时的时间分配与模板准备一个很现实的问题是蓝桥杯的题量不小省赛四个小时如果每一道题都从零敲起根本不现实。我的习惯是赛前把所有常用模板全部写好背到“肌肉记忆”级别。具体包括快速幂含矩阵版本、素数筛、gcd/lcm、组合数递推、前缀和与差分、Dijkstra、并查集、KMP、最长上升子序列等。比赛开始后建议先花10分钟把全部题目过一遍给每道题标一个“会/部分会/不会”的记号。会做的题先写保证拿分部分会的题先把暴力分拿到不会的题如果时间充裕再尝试用搜索拿部分分。STL和数学模板在这个策略里的作用是让“会做的题”写起来尤其快为攻难题留出足够的时间。调试时有个小技巧先把题目的样例跑通再构造边界数据自测。边界数据包括最小输入、最大输入、全相同值、数列两端最大值、空串空数组等。我见过太多选手样例一过就交结果WA在一堆边界情况上非常可惜。用stl的debug也方便比赛环境支持断点调试的话可以直接看容器内容如果只支持纯命令行就多用freopen重定向输出中间结果。5.4 省赛到国赛的进阶路线参考蓝桥杯省赛和国赛对STL与基本数学的要求区别主要体现在“迁移”和“组合”上。省赛里STL经常直接考某个容器的用法比如用map统计、用priority_queue做堆。国赛则更倾向于让这些基础能力作为更大算法的零件比如在状态压缩DP里用位运算枚举子集在组合数学题里用逆元预处理阶乘在图论题里用优先队列实现Dijkstra。备赛节奏上我给出的建议是前两周把STL容器过一遍所有API至少手敲三遍不要只看不练。第三四周集中刷“模拟枚举”类真题刻意要求自己用STL和数学模板写不要手写数据结构。第五六周刷数论模板题把gcd、素数筛、快速幂、组合数、前缀和全部过一遍整理成自己的代码模板。赛前一周每天做一套完整真题模拟比赛时间训练时间分配和心态。这套节奏对大部分学生来说强度合理既不会因为追求高难度算法而焦虑也不会因为只刷简单题而缺乏手感。我自己带过的一些选手按这个路线坚持了两个月省赛拿奖的概率有明显提升。最后再分享一个我踩过几次的坑比赛前一晚绝对不要刷新题也不要把没背熟的模板硬塞进脑子。人的短期记忆在应激状态下很不靠谱赛前一晚最该做的是早点睡把已经会的模板在脑子里过一遍第二天上考场心态稳敲代码手才不抖。蓝桥杯说到底考的不是你“会不会”那个偏门的算法而是你“能不能在有限时间内稳定输出自己已经掌握的能力”。STL和基本数学正是保证这个“稳定输出”的地基。地基打牢了剩下的就是爬到哪一层的问题。