资讯动态

2020蓝桥杯省赛B组真题解析:考点、坑点与考场时间分配

发布时间:2026/10/15 23:49:58 来源:尧图企业网站定制
2020年的第十一届蓝桥杯省赛是很多选手印象特别深的一次因为特殊原因比赛第一次拆成第一场、第二场进行B组第一场的题目也成了那一届被讨论最多的一份卷子。今年距离那一场已经过去了一段时间但如果你正在准备蓝桥杯这套题仍然值得拿出来反复拆解。省赛B组面向的是大多数普通本科院校的学生难度定位非常明确不是ACM那种“一道题卡三天”的极限思维训练而是“给我一个明确的数据范围用最稳妥的方式把题做对”。第一场这份卷子前五道是填空题后五道是编程大题整体难度梯度清晰前几道基本是在送分中间几道考察基础算法最后两道才是真正把人分开的地方。这篇文章我不打算只贴答案而是把每道题背后的考点、容易踩的坑、以及考场上的时间分配逻辑都过一遍。无论你是第一次参加蓝桥杯还是已经刷了很多题想找手感都可以从这套卷子里收获一些东西。1. 一张表看懂B组第一场的题量与难度1.1 十道题对应的考点和难度先给出我整理的一张总表这套题的框架基本就能看清楚了题号题目类型核心考点难度1填空模拟、循环边界处理简单2填空日期计算、闰年判断简单3填空数学期望、最优化枚举中等4填空嵌套循环模拟、栈中等5填空动态规划、卡特兰数思想中等偏上6编程整除模拟、数据类型简单7编程字符串处理简单8编程二维DP简单9编程数论、哈希统计较难10编程并查集、树上差分较难注意一个细节前五道是结果填空题只需要在提交页面填写最终答案不需要写代码提交但你可以在本地写程序算。2020年之后蓝桥杯取消了这种填空题改成了完整编程题但做题思路是相通的。这套题的难度分布其实很有代表性。第1题、第2题、第6题、第7题属于“给时间就能做对”的题目第3题、第4题、第8题需要一点基础的算法思维第5题、第9题、第10题才是真正的分水岭。1.2 为什么这套题被称为“基础算法的试金石”蓝桥杯省赛B组和ACM比赛最不一样的地方在于它考察的是“在规定时间内稳定拿到分数”的能力而不是“谁能写出更高级的数据结构”。这套题尤其明显。比如第10题网络分析看起来是图论实际上如果你能把并查集理解透再加一点差分思想就能在几十行代码内解决。如果你只是想套模板反而容易卡住。第9题整数拼接核心思路也只是一个取模运算的预处理关键在于你能不能把一个“拼接大数”的问题转化成“余数统计”的问题。所以我一直觉得准备蓝桥杯B组不需要去啃那些偏难怪的算法但基础的数据结构、数学推导、DP模型必须要练到“闭着眼睛都能写出来”的程度。这套卷子就是一个很好的自测工具如果你限时4小时做一遍能稳拿前8题最后两题有思路省赛拿奖基本就稳了。2. 前五道填空题读题准确率决定下限2.1 跑步训练模拟题的边界条件怎么审第1题是一道典型的模拟题题目大意是一个人一开始有10000体力跑步机每跑1米消耗1点体力当体力不够消耗时就需要喝一次能量饮料把体力补满问最终跑完一定距离时喝了多少次饮料。这类题在现场的正确做法就是写一个while循环按顺序模拟。很多人在这种题上丢分不是不会写代码而是没搞清楚三个边界条件体力是在“尝试跑步前”判断还是在“跑步中途耗尽”时判断。体力耗尽后补给是立即生效还是需要再花时间。循环的终止条件是“跑完目标距离”还是“体力为0”。我的建议是这种题直接把变量名写得直白一点比如remain、distance、drinkCount不要为了省事用一堆单字母缩写。然后跑几个边界例子体力刚好等于消耗值的瞬间、距离刚好等于0的时候。还有一个经验填空题允许用程序算写完代码后先用题目给的样例验证再故意改几个小参数保证逻辑在边界处也没有跳出循环。这种题通常不会卡算法复杂度小题大做反而稳。2.2 纪念日日期差的通用计算模板第2题要求计算两个日期之间相隔多少分钟。这种题在蓝桥杯出现频率很高属于最不需要“灵感”的题只要你有一套固定的日期计算模板直接套用即可。我的模板是这样的先把“从公元元年1月1日到给定日期”的总天数算出来然后两个总天数相减最后乘以24乘以60。闰年判断一定要写对能被4整除但不能被100整除或者能被400整除的年份是闰年。千万别只写一个year % 4 0在1900年、2100年这种整百年份上会翻车。下面是我常用的代码框架建议直接背下来#include bits/stdc.h using namespace std; bool isLeap(int y) { return (y % 4 0 y % 100 ! 0) || (y % 400 0); } int daysOfMonth(int y, int m) { int day[13] {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; if (m 2 isLeap(y)) return 29; return day[m]; } long long daysFromZero(int y, int m, int d) { long long days 0; for (int i 1; i y; i) { if (isLeap(i)) days 366; else days 365; } for (int i 1; i m; i) { days daysOfMonth(y, i); } days d - 1; return days; } int main() { long long d1 daysFromZero(1921, 7, 23); long long d2 daysFromZero(2020, 7, 1); cout (d2 - d1) * 24 * 60 endl; return 0; }这里的daysFromZero返回的是从基准年到该日期的天数两个返回值相减就是中间经过的天数。注意年份循环从1开始这样算出的差值是稳定的不会因为日期先后顺序产生负数。这类题的坑往往出在“计算分钟”而不是“计算天数”。很多人算出天数后直接填了答案漏乘了1440非常可惜。所以拿到这种题第一件事就是把单位圈出来是分钟还是秒。2.3 REPEAT程序嵌套循环展开与栈模拟第4题给了一段伪代码里面有类似REPEAT n和END的嵌套结构要求计算某个变量最终的值。这种题看起来有点陌生但本质上就是一个“按行解析嵌套结构”的模拟题。最直接的做法是用栈来维护循环层数。我的思路是这样的扫码每一行遇到REPEAT n就把n入栈同时用一个变量multiplier记录当前所有循环次数的乘积遇到普通赋值语句比如A A x实际累加的量是x * multiplier遇到END就出栈重新计算multiplier。伪代码逻辑如下long long A 0; stackint st; int multiplier 1; st.push(1); for (每一行) { if (这行是 REPEAT n) { st.push(n); multiplier * n; } else if (这行是 A A x) { A x * multiplier; } else if (这行是 END) { multiplier / st.top(); st.pop(); } }这里有一个非常容易出错的点multiplier的更新。如果只是简单地在入栈时乘上n、出栈时除以n那遇到嵌套循环时只有最内层的循环次数会参与乘法计算。比如REPEAT 3 REPEAT 5 A A 1 END END在进入内层循环后multiplier变成15遇到ENDmultiplier需要恢复到3再遇到END恢复到1。用栈记录每一层的循环次数然后用当前的栈顶值重新计算multiplier是最稳妥的写法。还有一个小技巧读入这种伪代码时最好整行读入然后分别判断开头是不是REPEAT而不是按空格拆词。因为题目里的行格式比较固定整行读入后用string查找子串更不容易出错。2.4 合并检测用数学期望找最优分组第3题是我个人非常喜欢的一道题因为它表面上是“数学期望”实际上只要把式子列清楚枚举就能做。题目大意是现在要对一群人做某种检测采用“把k个人分成一组先做合并检测如果结果为阳性再对每个人单独检测”的策略。每个人检测结果是阴性的概率非常高问k取多少时总检测次数期望最少。设每个人的阳性概率是p很小那么一组k个人呈阴性的概率是(1-p)^k呈阳性的概率是1 - (1-p)^k。这组的检测次数期望是阴性时1次 阳性时k1次合并的1次加上k个人的单独检测 期望 1 * (1-p)^k (k1) * [1 - (1-p)^k] ≈ 1 k^2 * p如果总人数是n那么总次数期望约等于(n/k) * (1 k^2*p) n/k n*k*p。这时k越大n/k越小但n*k*p越大两者之间有一个最优解。最简单的做法就是枚举k从1到50把每个k对应的总次数期望算出来取最小值。如果你想把p的真实值带进去算最后的答案是一个整数枚举法完全够用。这种题的启示是看到“最优”“期望”这些词别慌着推公式先建模把期望表达式写出来剩下的交给循环。蓝桥杯的填空题不在乎过程能算对答案就行。2.5 矩阵卡特兰数的动态规划写法第5题是一道让很多人卡住的填空题把1到2020这2020个数字填入一个2行1010列的矩阵中要求每个格子右边的数比左边的大下面的数比上面的大问一共有多少种填法结果对2020取模。这题的核心是理解“从小到大依次放数”的约束。我们不妨把数字按从小到大依次放入矩阵。由于右边的数必须比左边大所以第一列必须比第二列先放同理第二行某个位置必须比第一行对应位置先放。换句话说任何时刻左边列已经放的数量必须不小于右边列已经放的数量。这其实就是一个经典的“括号序列”问题也就是卡特兰数。但这里需要注意的是2020不是质数不能用费马小定理去求组合数逆元因此最好直接用动态规划递推。const int MOD 2020; long long f[1015][1015]; int main() { int n 1010; f[0][0] 1; for (int i 0; i n; i) { for (int j 0; j n; j) { if (i 0 j 0) continue; if (i j) { if (i - 1 j) f[i][j] (f[i][j] f[i-1][j]) % MOD; if (j - 1 0) f[i][j] (f[i][j] f[i][j-1]) % MOD; } } } cout f[n][n] endl; return 0; }f[i][j]表示已经放了i个第一行数字、j个第二行数字的合法方案数。每次要么在下一列放第一行数字要么放第二行数字但任何时候必须保证i j这就是这个DP的核心转移条件。这个题告诉我们遇到复杂的组合计数先不要急着套公式想一想能不能用DP把状态定义出来很多时候正确又省心。3. 后五道编程题基础算法里的得分关键3.1 整除序列long long与输出顺序第6题可以说是整套卷子里最简单的编程题输入一个整数n不断把它除以2取整输出过程中产生的每个数直到变为0。比如输入10输出10 5 2 1。这种题真正的坑只有一个数据类型。题目给的n可能非常大甚至会接近long long的上限所以定义成long long绝对没错。千万不要用int否则超过21亿就溢出了输出结果完全不对。代码很短#include bits/stdc.h using namespace std; int main() { long long n; cin n; while (n 0) { cout n ; n / 2; } cout endl; return 0; }注意是先输出当前值再除以2顺序反了就会漏掉初始值。这种题虽然简单但一旦紧张很容易在输出格式上翻车。我的建议是拿到这种题先手动模拟一遍样例过程再动笔。3.2 解码字符串边界的经典教训第7题是一个字符串压缩解压题给定一个形如a3b2这样的压缩串表示aaa bb要求把展开后的字符串输出。字符后面跟的数字表示这个字符重复出现的次数。很多人第一反应是遍历字符串遇到字母就记下来遇到数字就把前面的字母重复输出。这个思路对但有一个细节需要注意一个字母后面如果跟着数字数字处理完之后不要让循环再把这个数字当成一个新字符处理。我的写法是这样的#include bits/stdc.h using namespace std; int main() { string s; cin s; for (int i 0; i s.size(); i) { if (isalpha(s[i])) { char c s[i]; int cnt 1; if (i 1 s.size() isdigit(s[i 1])) { cnt s[i 1] - 0; i; } for (int j 0; j cnt; j) { cout c; } } } cout endl; return 0; }关键的技巧是用isalpha和isdigit判断字符类型而不是手动比较ASCII码。手动比较也容易出错比如a c c z虽然有95%的情况没问题但一旦题目夹带了大写字母你可能就会漏判。还有一个边界值得注意如果字符串末尾是一个字母后面没有数字那么它只输出一次。上面代码里cnt默认是1就处理了这个情况。这种题没什么难度就看你能不能一次写对不留边界死角。3.3 走方格二维DP与禁止点的处理第8题要求从(1,1)走到(n,m)只能向右或向下走并且不能走到两个坐标都是偶数的格子上问有多少种不同的走法。这是入门级的二维DP但出题人在“行号和列号都是偶数”这个条件上藏了一个小坑。很多人想当然地以为if (i % 2 0 j % 2 0) continue就够了确实是这样但要注意如果起点或者终点本身就是禁止格那结果直接是0。DP转移很简单#include bits/stdc.h using namespace std; int main() { int n, m; cin n m; long long dp[35][35] {0}; dp[1][1] 1; for (int i 1; i n; i) { for (int j 1; j m; j) { if (i % 2 0 j % 2 0) { dp[i][j] 0; continue; } if (i 1 j 1) continue; dp[i][j] dp[i-1][j] dp[i][j-1]; } } cout dp[n][m] endl; return 0; }这里用long long是为了以防数据范围变大虽然这道题范围不大但养成好习惯不会错。另外要注意数组下标从1开始所以要把dp[0][1]和dp[1][0]这种边界巧妙地绕过去不要让下标越界。如果你喜欢用递归写记忆化搜索也没有问题但二维DP在这个数据范围下代码量更小也不容易栈溢出。4. 压轴两题想清楚再写比写出来更重要4.1 整数拼接余数统计的拆分技巧第9题是这套卷子里我印象最深的一道。题目大意给n个整数任选两个拼成一个新数比如12和345拼成12345问有多少种拼法使得拼接后的数能被给定的K整除。直接的做法是枚举所有的(i, j)有序对拼出大数再判断。但如果n的数量级到1e5这样的暴力就完全不行了。这个题的核心思路是把“拼接”拆成两部分。设右边的数是a_j它的长度是len_j那么拼接后的数可以写成a_i * 10^len_j a_j要对K取模问题就变成了对于每一个a_j有多少个a_i满足(a_i * 10^len_j a_j) % K 0。我们可以先预处理一个计数数组cnt[L][r]表示“长度被当作L时所有a_i * 10^L % K的余数分布”。具体做法是枚举每一个数作为左边的数对每一种可能的右数长度L1到10计算出a_i * 10^L % K然后对应的cnt[L][这个余数]加1。然后枚举每一个数作为右边的数计算出它需要的左边余数need (K - a_j % K) % K 答案 cnt[len_j][need]这里还有一个很重要的去重问题当i j时左数和右数是同一个数需要判断这种情况是否被错误统计了。如果(a_j * 10^len_j a_j) % K 0那么答案要减1。下面是完整的参考代码#include bits/stdc.h using namespace std; typedef long long ll; int getLen(ll x) { int len 0; while (x) { len; x / 10; } return len; } int main() { int n; ll K; cin n K; vectorll a(n); for (int i 0; i n; i) cin a[i]; vectormapll, ll cnt(11); vectorll pow10(11); pow10[0] 1; for (int i 1; i 10; i) pow10[i] pow10[i-1] * 10 % K; // 预处理每个数作为左边数对每个可能的右数长度做一次计数 for (int i 0; i n; i) { for (int L 1; L 10; L) { ll r (a[i] % K) * pow10[L] % K; cnt[L][r]; } } ll ans 0; for (int j 0; j n; j) { int L getLen(a[j]); ll need (K - a[j] % K) % K; ans cnt[L][need]; // 去掉自己拼自己的情况 ll self ((a[j] % K) * pow10[L] % K a[j] % K) % K; if (self 0) ans--; } cout ans endl; return 0; }这个解法的复杂度是O(n * 10 n)也就是说基本上是线性的。它最精彩的地方在于我们不需要真正去拼接数字只需要用取模运算把问题转化成“查表”。很多人卡在这道题上是因为试图去求10^len_j的逆元。但K不一定是质数逆元不一定存在这条路根本走不通。正确思路是先枚举右数长度再预处理左数余数。这也是我想强调的蓝桥杯的压轴题往往不是考你多高级的算法而是考你能不能想到巧妙的分解角度。4.2 网络分析带权并查集帮你维护“集合整体加”第10题也是一个经典题有n个节点初始时互不相连。操作有两种一种是在两个节点之间连一条边让它们属于同一个连通块另一种是给某个节点所在的整个连通块内所有节点都加上一个值t。最后要求输出每个节点的权值。最暴力的做法是每次操作2都遍历整个连通块给每个节点加t。如果操作次数很多、节点数很多这必然超时。但如果我们用并查集来维护连通关系同时给每个集合打一个“懒标记”就能很优雅地解决。思路是这样的额外维护一个数组dd[x]表示节点x到它的父节点之间的“增量”。每次给某个连通块整体加t时我们只把这个t加到该连通块根节点的d上。这样当我们需要算某个节点的实际权值时只需要沿着它的父链把所有d累加起来就得到了它应该获得的所有增量之和。这里的关键是并查集合并时如何保证两个集合原有的累加值不被打乱。如果直接把一个根节点接到另一个根节点下面d值需要做相应的调整。假设两个集合的根分别是ra和rb现在要把ra接到rb下面。为了让ra集合里的所有节点在合并后仍然保持原来的权值我们让d[ra] d[ra] - d[rb]然后执行p[ra] rb。这样ra集合中某个节点原来的路径增量是d[ra]合并后它的路径增量变成了d[ra] - d[rb] d[rb]正好还是原来的值。完整代码如下#include bits/stdc.h using namespace std; const int N 100005; int p[N]; long long d[N]; int find(int x) { if (p[x] x) return x; int root find(p[x]); d[x] d[p[x]]; return p[x] root; } void merge(int a, int b) { int ra find(a); int rb find(b); if (ra rb) return; d[ra] d[ra] - d[rb]; p[ra] rb; } void add(int x, long long t) { int r find(x); d[r] t; } int main() { int n, m; cin n m; for (int i 1; i n; i) p[i] i; while (m--) { int op; cin op; if (op 1) { int a, b; cin a b; merge(a, b); } else { int x; long long t; cin x t; add(x, t); } } for (int i 1; i n; i) { find(i); cout d[i] (i n ? \n : ); } return 0; }这道题对没接触过“带权并查集”的人来说可能很陌生但它本质上是把“对整个集合快速加值”这个需求通过并查集上的差分来实现。如果你理解了它的思想再去看其他很多“区间加、单点查”的题都会有豁然开朗的感觉。如果觉得这套方法太难还有一个临时补救方案把每个连通块维护成一个vector合并时把元素少的vector并到元素多的vector里操作2直接遍历vector加值。这个“启发式合并加暴力”的复杂度是O(m log n)级别的在这道题的数据范围内其实也能过。但学会带权并查集对后续比赛更有帮助。5. 省赛现场的时间分配与避坑清单5.1 我踩过的三个考场坑第一个坑是在填空题上花太多时间。结果填空题不需要提交代码只需要填最终答案。有人习惯在本地把代码写得非常完整调试到完美才填答案这往往导致后面编程大题时间不够。我的建议是填空题想出思路后尽快用最朴素的方法算出答案一题不超过20分钟。第二个坑是没注意输出格式。蓝桥杯的评分是按输出文件比对的多一个空格、少一个换行都可能判错。编程大题一定要严格按照题目要求输出最后不要有额外的提示字符。第三个坑是忘记取模。很多涉及大数的题中间结果很容易超出long long该取模的地方务必取模。特别是像第9题这种需要在乘法过程中取模的题目一旦漏掉答案不仅错得离谱而且你很难通过样例自查出来。5.2 一套实测有效的做题节奏我的建议是把4小时的比赛时间分成三段前60分钟解决前5道填空题。填空题的难点在思路不在代码。如果一道题想了15分钟还没有明确思路先跳过不要死磕。所有填空做完后至少留30分钟给后面的大题读题。中间100分钟做第6到第8题这三道题属于“会就能拿分”的基础题做完它们能保证你晚点心里不慌。此时不要想得太复杂能用DP就用DP能暴力就暴力但要注意数据范围。最后80分钟集中攻克第9题和第10题。如果实在做不出来至少把暴力解法写出来拿部分分。蓝桥杯的编程题是按测试点给分的哪怕你只过了几个小数据也有分。5.3 赛前一周的快速复盘法最后分享一个我常用的备赛方法找近三年的省赛真题模拟比赛环境4小时完整做一遍。做完之后不要只看分数而是把每道题对应的考点标签列出来比如“并查集”“DP”“字符串”“数学期望”然后看看自己哪些标签反复出错哪些标签已经稳定。这样练上两三套题你很快就会发现蓝桥杯省赛B组的出题范围其实非常集中完全可以通过针对性的训练把最容易丢分的部分补上。比起盲目刷题这种“带标签复盘”的效率要高得多。我个人在复盘这套2020年第一场真题时最大的体会就是省赛B组并不指望你成为算法天才它更像是在检验你“能不能在有限时间内用最基础的工具解决一系列实际问题”。把基础算法练到位把该拿的分稳稳拿到手你的成绩就不会差。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑