☰
蓝桥杯B组C++赛后复盘:从数据范围到算法实现的完整思路拆解
2026/10/15 21:59:56 网站建设 项目流程

1. 考完第16届B组C++,先别急着对答案

第16届蓝桥杯B组C++考完,最典型的状态是:考场门口一堆人对着各自写下的代码复盘,回到宿舍一刷手机,发现各路题解铺天盖地,每份代码都写得漂亮,注释也清晰。可你合上手机自己重新写一遍,大概率还是卡在考场上卡住的同一个位置。

这个现象和"会不会算法"关系不大,真正的问题是:题解能告诉你正确解是什么,但没法还原你在考场上那几十分钟里到底怎么思维断档的。所以我这篇不打算把第16届B组C++每道题从头到尾贴一遍完整代码,而是把这类比赛反复出现的考点、算法选择路径、C++实现细节拆开,按赛后的复盘逻辑重新走一遍。适合三类人看:刚考完想系统复盘的,明年备赛想提前了解套路的,算法课学了不少但一上机就手生的同学。

1.1 看懂题解和会做题之间差了哪些东西

很多人把"看懂题解"当成"掌握了这道题",这是竞赛复盘里最亏的心态。一道题的题解通常只给你一条最顺的路径:定义dp[i]表示什么、转移方程怎么写、边界怎么处理。但你自己在考场上不是沿着这条路走的,你可能先试了贪心,又试了暴力,最后才隐约觉得要动态规划,却不知道状态怎么定义。看题解时跳过了中间所有弯路,自然觉得"不过如此"。

我复盘时有一个硬性要求:不看新题解,先看自己考场上的提交记录。把每道题从读题到第一版代码写完,中间花了多少时间、写过几个版本、删掉过几段逻辑,全都写下来。然后对照题解,找到那个真正的断点:是题目没读懂,是模型没识别出来,还是代码实现细节出错?只有定位到这个断点,下次才不会在同一个地方摔第二次。

1.2 复盘一份题目的标准动作

一份好用的赛后复盘不是把题目和答案抄一遍,而是做一张表格。我会在每道题后面记录几个固定的维度:考点类型、我的第一反应、最终做法、卡住的位置、正确思路的关键跳转、需要补的专题。这样一轮下来,整个第16届B组C++到底考了哪几类东西、我自己在哪一类上最弱,一目了然。

题号考点分类我卡在哪儿题解的关键跳转待补专题
A题模拟题目规则没吃透按时间轴拆事件复杂模拟
B题枚举/剪枝不敢写暴力数据范围允许O(n^3)复杂度估算
C题贪心排序方向搞反先按右端点排序区间贪心
...............

这张表的重点不是"做对了没有",而是"卡在哪一层"。统计十几道题之后你会发现,自己反复出错的地方往往就那么两三个:要么是状态压缩的细节,要么是取模时的减法,要么是输入输出的处理。把这些薄弱点单独拎出来做针对性训练,比漫无目的地刷一百道新题有效得多。

2. 数据范围是选算法最该看的信号灯

第16届B组C++整体给我的感受是:题目本身的模型并不算偏,很多题你都知道大概用什么算法,但数据范围卡得很精准,它决定了你能不能贪心、能不能暴力、要不要上log级别的优化。数据范围就是题目在开考之后发给你的第一张提示卡,用好了能省大量时间。

2.1 一张表看懂"什么规模用什么算法"

很多同学拿到题先想算法名字,这是反的。正确顺序是先看数据范围,反推可接受的复杂度,再倒过来选算法。我在做题时会直接在草稿纸上写下这个对应关系:

数据范围可接受的复杂度常见算法
n <= 10O(n!) / O(2^n)全排列、状压枚举
n <= 20O(2^n)状态压缩DP、子集枚举
n <= 100O(n^3)Floyd、三重循环DP
n <= 1000O(n^2)双重循环、朴素DP
n <= 1e5O(n log n)排序、二分、优先队列
n <= 1e6O(n)线性筛、单调队列
n >= 1e9O(sqrt(n)) / O(log n)快速幂、数论公式

举个例子,一道题说n不超过20,那状态压缩基本是首选方向;如果n到了1e5,无脑的双重循环肯定超时,要么排序贪心,要么二分,要么用数据结构优化。这个判断做得越快,考场上的心态越稳。

2.2 暴力枚举敢不敢写,算清楚上限再说

暴力不是丢人的解法,反而是第16届B组这类赛事的保底分来源。很多人明明会写暴力却不敢写,怕超时,结果空在那儿想最优解想太久,最后连部分分都没拿到。其实只要把复杂度算明白,心里就有底。比如一段三重循环,n=100时是100万次操作,C++一秒轻松跑完;n=1000时是10亿次,肯定没戏。

所以我的习惯是:任何题先写一版最朴素的解法,用数据范围算一下最多跑多久,要是能过,立刻提交拿分;要是不能过,再想优化。这一版暴力代码也不白写,它能帮你验证小数据的结果对不对,后面优化时还能拿来对拍。

// 示例:先估算暴力枚举的可行性 // 假设需要枚举所有 i<j 的数对,n = 5000 // 直接双重循环是 5000*4999/2,约 1250 万次,完全可跑 for (int i = 1; i <= n; i++) { for (int j = i + 1; j <= n; j++) { // 处理逻辑 } } // 如果 n = 1e5,同样代码就到了 5e9 次,必须换思路

2.3 拿到题目后固定先做三件事

很多选手看到题目就上手写代码,写着写着才发现漏了条件。我在第16届B组C++的做题过程中给自己定了个流程,无论多简单的题都先走这三步:

第一,把题目里的每个数字和约束条件圈出来,特别是"保证XXX""所有数据满足XXX"这类说明,它往往直接暗示了算法方向。第二,自己动手把题目给的样例推一遍,用笔写,不要心算。推样例的过程就是在强迫自己理解规则,很多模拟题如果样例推不明白,代码写到一半必然返工。第三,在注释里先写出算法思路和复杂度,再开始敲代码。这是防止你写着写着跑偏的最好办法。

这三步看起来简单,但对稳定拿分帮助极大。第16届的题里有一类"题目很长、规则很多"的模拟题,很多人就是输在没把样例推清楚就直接编码,最后逻辑错乱、边界全崩。宁可花三分钟把样例推明白再动手,也别省这个时间。

3. 搜索题和DP题:中档题的稳定得分区

从历年B组C++的格局来看,搜索和动态规划基本是占据中档题的主力。这两类题的特点是:模型识别对了,代码写起来并不长;但模型识别错了,就会在错误的方向上浪费大量时间。搜索和DP之间也经常能互相转化,记忆化搜索本质就是一种DP,所以放在一起讲。

3.1 先判断该搜还是该推

拿到一道题,判断用搜索还是DP,关键是看你关心什么。如果题目要的是"从状态A到状态B的最短步数""最少操作次数",那BFS几乎是标准答案;如果是"有多少种方案""最大价值是多少""某种组合是否存在",那要么是DP,要么是DFS加剪枝。

另一个判断点是状态空间的大小。状态总数在十万百万级别,BFS或DFS加记忆化完全可行;状态总数到了亿级别,基本就要找规律或者设计更紧的DP了。我在第16届B组C++里遇到一种题,状态看起来是两个变量的组合,但其中一个变量始终小于另一个,这就直接把状态空间砍半,搜索剪枝也能过,DP照样能写。

3.2 BFS/DFS实现模板与状态压缩

BFS和DFS的代码结构非常固定,真正容易错的是状态表示和剪枝条件。网格类BFS要特别注意行列的对应关系和边界判断,写错了往往不是超时而是直接死循环。我用一个自己经常背的模板:

// 网格BFS模板:求从 (sx,sy) 到 (tx,ty) 的最短步数 #include <bits/stdc++.h> using namespace std; const int MAXN = 1005; int n, m; char grid[MAXN][MAXN]; int dist[MAXN][MAXN]; int dx[4] = {0, 0, 1, -1}; int dy[4] = {1, -1, 0, 0}; int bfs(int sx, int sy, int tx, int ty) { memset(dist, -1, sizeof(dist)); queue<pair<int,int>> q; q.push({sx, sy}); dist[sx][sy] = 0; while (!q.empty()) { auto [x, y] = q.front(); q.pop(); if (x == tx && y == ty) return dist[x][y]; for (int k = 0; k < 4; k++) { int nx = x + dx[k], ny = y + dy[k]; if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue; if (grid[nx][ny] == '#') continue; // 障碍 if (dist[nx][ny] != -1) continue; // 已访问 dist[nx][ny] = dist[x][y] + 1; q.push({nx, ny}); } } return -1; }

如果有多个关键物品要收集,可以把"已收集哪些物品"压成二进制状态,让dist数组多一维。这就是状态压缩BFS,比如dist[x][y][mask]表示在格子(x,y)且已收集物品集合为mask时的最少步数。写这类题时最需要注意的是:mask的位运算优先级很容易搞混,建议时刻用括号包住所有位运算表达式。

3.3 DP模型识别与滚动数组的倒序原理

DP不会做,往往不是不会写转移方程,而是看不出这道题属于哪个经典模型。第16届B组这类赛事常见的DP模型其实非常有限:01背包、完全背包、最长上升子序列、区间DP、数字三角形、路径计数。识别的方法就是把问题往这几个模型上靠:有"每个东西选或不选"的味道,就可能是背包;有"区间合并或切分"的味道,就可能是区间DP;有"从左上走到右下"的味道,就是路径类DP。

以01背包为例,很多初学者不理解为什么滚动数组必须倒序枚举容量。正着枚举会让同一个物品被选多次,相当于从01背包退化成了完全背包。类比来说,正序枚举就像发牌时每发一张牌都立刻把刚发的牌再洗进去,同一张牌可能被反复拿到;倒序枚举则保证每个物品只被考虑一次。

// 01背包滚动数组写法 int dp[MAXV]; for (int i = 1; i <= n; i++) { for (int j = V; j >= w[i]; j--) { // 必须倒序 dp[j] = max(dp[j], dp[j - w[i]] + v[i]); } }

如果题目说每个物品可以用无限次,那第二层循环改成从w[i]到V正序就是完全背包。这个正序和倒序的区别,就是背包类问题最常见的考点之一。我建议你写DP时把状态定义写在注释第一行,这样转移方程出错时,至少能回头核查是不是定义本身出了问题。

4. C++实现里的高频失分点,每一条都是真金白银

很多选手算法思路对,但代码就是过不了,最后变成"我明明会做却没分"。这种情况放在第16届B组C++的语境里尤其可惜,因为B组的很多题不是比谁思路更逆天,而是比谁代码写得更稳。C++在竞赛里的三个老坑,几乎每届都要吞掉一批人。

4.1 整数溢出:乘法没加long long直接丢分

蓝桥杯B组的数据范围设计得很喜欢卡边界。比如让你算方案数、算乘积、算前缀和,单看每一项都在int范围内,但加起来乘起来就爆了。拿1e5乘以1e5来说,结果是1e10,远超int上限约2.1e9。遇到这种情况,要么全用long long,要么在关键计算处显式转换类型。

// 错误的写法:int相乘后赋值给long long仍然会先溢出 long long bad = a * b; // 如果a和b是int,乘法可能在int范围内先溢出 // 正确的写法 long long good1 = 1LL * a * b; long long good2 = (long long)a * b;

我自己有一个检查清单:只要题目里出现了1e5以上的数据范围,或者要求取模、求乘积、求和,那就把所有相关变量全部定义成long long。多写几个long long不会扣分,少写一个可能全题报废。还有一个习惯是写出算式的草稿时,顺手估算一下中间结果会不会超过2.1e9,超过就换long long。

4.2 输入输出和字符串的脏坑

cin和cout不是不能用,但必须关同步,否则在数据量大的题上会白白超时。我在每道题的代码开头都会写这两行,已经形成肌肉记忆:

ios::sync_with_stdio(false); cin.tie(nullptr);

字符串处理是另一个重灾区。用cin读取整行字符串要用getline,但getline前如果有cin>>操作,会残留一个换行符,导致第一行读成空串。这个坑每年都有人踩。解决办法是在cin>>之后、getline之前,先把残留的换行符消费掉。

int n; cin >> n; string line; getline(cin, line); // 这里读到的可能是空串 // 需要先处理残留换行: // getchar(); 或者 getline 后再读一次

如果要按空格或逗号分割字符串,最稳的办法是逐个字符处理,或者用stringstream,尽量避免手写复杂下标。分割逻辑一旦下标混乱,调试时间会远远超过省下来的那几分钟。

4.3 递归爆栈和全局变量的正确姿势

深搜DFS的递归层数如果超过几万层,程序很容易栈溢出直接崩掉。第16届B组C++里有一类搜索题的递归深度可能达到1e5,这种情况下递归写法即使逻辑正确,也可能会在运行时崩溃。解决办法有两个方向:一是把递归改成显式的栈或队列迭代写法,二是确认题目环境允许再递归,否则从一开始就设计成非递归版本。

全局变量在竞赛代码里是常规操作,因为它能避免频繁传参、减少栈空间的消耗。但全局变量也有代价:调试时状态不清、多组数据之间忘记初始化。我的习惯是每组数据测试前,把所有全局数组用memset或循环重新赋值。特别是dist数组、vis数组这类,不重新初始化就是上一次的残留数据,错得毫无头绪。

5. 压轴题常见的两张面孔:图论与数论

每届B组C++的最后几题,或者难度较高的压轴题,翻来覆去就是图论和数论这两张面孔。图论的难点在于判断该用哪个算法,数论的难点在于模板不够熟、取模细节出错。这两个专题只要准备到位,压轴题也能稳定拿分。

5.1 图论题先判断是哪种"图任务"

图论的算法名字很多,但题目信号其实很固定。看到"求两点之间的最短时间/最少费用",就想到Dijkstra;看到"让所有点连通并且总代价最小",就想到最小生成树;看到"某些点必须在另一些点之前完成",就想到拓扑排序;看到"判断是否存在环",可以想到DFS或拓扑排序。先匹配任务类型,再决定算法,比看到图就乱试要靠谱得多。

// Dijkstra堆优化模板,单源最短路 using PII = pair<long long, int>; vector<PII> graph[MAXN]; // {to, weight} long long dis[MAXN]; void dijkstra(int s) { for (int i = 0; i < MAXN; i++) dis[i] = LLONG_MAX; priority_queue<PII, vector<PII>, greater<PII>> pq; dis[s] = 0; pq.push({0, s}); while (!pq.empty()) { auto [d, u] = pq.top(); pq.pop(); if (d > dis[u]) continue; // 懒删除 for (auto [v, w] : graph[u]) { if (dis[u] + w < dis[v]) { dis[v] = dis[u] + w; pq.push({dis[v], v}); } } } }

第16届B组里有道题表面是二维网格移动,本质上却是带权最短路,因为不同方向的移动代价不一样。这种"包装题"很考验你能不能拆掉外层的场景描述,直接看到底层的图模型。拆包装的办法就是问自己:这个问题的"节点"是什么?"边"是什么?"边权"是什么?三个问题回答清楚,算法自然浮出来。

5.2 数论题手里要有现成模板

数论题最怕的不是题目难,而是你在考场上临时推导公式。很多常用结论应该在赛前就背熟:快速幂、最大公约数、扩展欧几里得、质数筛、逆元、组合数取模。每个都值得准备一份可以直接默写的模板。我自己的快速幂模板长这样:

// 快速幂 a^b % mod long long qpow(long long a, long long b, long long mod) { long long res = 1; while (b > 0) { if (b & 1) res = res * a % mod; a = a * a % mod; b >>= 1; } return res; }

求逆元的两种方式也要清楚:如果模数是质数,可以直接用费马小定理,也就是qpow(a, mod-2, mod);如果模数不是质数,就要用扩展欧几里得。判断错模数性质就会得到错误结果,而且是那种很隐蔽的错误,样例能过,大数据就挂。

5.3 取模运算的边界细节

取模题看着简单,实际上藏着一堆小坑。最常见的是减法取模:在C++里(a - b) % mod可能是负数,如果a和b本身都小于mod,a - b可能是负的,取模结果也是负的,这在计数题里直接导致答案错误。正确写法是加一个mod再取模,写成((a - b) % mod + mod) % mod。

另一个容易忽略的地方是乘法取模可能爆long long,如果mod在1e18级别,两个数相乘会超过64位范围,这时得用快速乘或__int128。蓝桥杯B组大部分题目不会到这么极限,但如果题目出现了"对10^18+7取模"之类的字样,就要警惕。

组合数取模也是一类高频考点。n比较小的时候用杨辉三角递推,n到1e5以上就要预处理阶乘和逆元。我建议把"组合数模板"整个背下来,而不是考场上现推,省下的时间足够你把压轴题的细节反复检查两遍。

6. 把一次比赛变成十次练习:赛后复盘路线

每次比赛考完,最有价值的不是预估自己能拿多少分,而是从题目里榨出可复用的经验。第16届B组C++的题目在考场上已经造成了既定的结果,但赛后的复盘质量会直接影响你下一届的表现。复盘做得好,一次比赛能顶十次日常练习;复盘做得潦草,考十次也还是原地踏步。

6.1 建立自己的失分台账

复盘不能只是"哦我看懂这道题了",而是要做成台账。我的失分台账长这样:

题目类型当时时间开销失分原因正确思路同类题练习
模拟题40分钟规则没理清按事件拆函数某天安排类模拟
数据范围推断10分钟不敢写暴力复杂度上限可过暴力枚举专项
DP50分钟状态定义错了加一维状态表示约束区间DP练习
图论30分钟没识别出最短路把格子建模为带权图网格最短路练习

做完台账后,你会发现很多失分并不是"算法不会",而是"某一种能力不稳定"。可能是边界处理,可能是状态定义,可能是复杂度估算。把这些不稳定点提取出来,每周专门练一种,进步非常快。

6.2 三条让我进步最快的练习方式

第一条是刻意做"只算复杂度不写代码"的练习。随便找一组题目列表,只看数据范围,三分钟内判断出该用什么算法,不写代码,只看思路和答案对一下。这个练习能极大提升考试时的选题效率。

第二条是限时模拟。每周找一个安静的时间段,按正式比赛的时间限制做一套模拟题,过程中不看题解、不暂停、不和别人讨论。限时和不限时的做题状态完全不同,考前多模拟几次,考场上的时间分配感会好很多。

第三条是代码复写。题解看完后,合上看一遍自己的思路,然后从头到尾不参考任何资料,把代码完整写出来,提交到评测系统验证。写不出来或者写错了,就说明这道题还没真正掌握。这条最笨,但也是对你最诚实的一条。第16届B组C++这套题就算已经考完了,只要你能把里面每道题都复写通过,学到的东西依然等于完整备赛了一遍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询