1. 从“蓝桥杯2021国赛CB题解”说起:一份迟到的复盘与深度拆解
最近在整理资料时,翻到了2021年蓝桥杯国赛C++ B组(以下简称CB组)的题目。虽然时隔几年,但作为国内覆盖面最广的编程赛事之一,其国赛真题依然具有极高的参考价值。对于正在备赛的同学,它是最好的模拟战场;对于已经参赛过的选手,它是一次绝佳的复盘机会,能帮你查漏补缺,看清自己思维上的盲区。今天,我就以一名老选手和过来人的视角,带大家重新走进这套题,不光是给出答案,更重要的是拆解每道题背后的核心考点、解题思路的构建过程,以及那些在考场上容易忽略的“坑点”。我会假设你具备C++语言基础和基本的算法知识,目标是让你不仅能看懂“怎么做”,更能理解“为什么这么做”以及“下次遇到类似的该怎么想”。
这套题整体上延续了蓝桥杯“思维+实现”并重的风格,既有考验数学思维和逻辑推理的题目,也有对代码实现细节和算法效率要求较高的题目。我们将按照题目顺序,逐一进行深度剖析,我会在讲解中融入我自己的解题心路历程和踩过的坑,希望能给你带来一些不一样的启发。
2. 试题A:空间(基础中的基础,但别掉以轻心)
这道题通常是“送分题”,考察的是最基础的计算机常识和单位换算。题目可能会给出类似“一个32位二进制整数,在内存中占用多少字节?”或者“256MB的存储空间,可以存放多少个32位整数?”这样的问题。
2.1 核心考点与解题钥匙
这里的核心就两个:位(bit)、字节(Byte)、字(word)的关系,以及二进制与十进制的换算。
- 1 Byte = 8 bits。这是铁律。
- 32位整数,意味着每个整数需要用32个二进制位来存储。
- 所以,一个32位整数占用的字节数 = 32 bits / 8 = 4 Bytes。
如果题目问的是存储容量能放多少个,比如:“256MB的内存可以存放多少个32位整数?” 解题步骤:
- 统一单位:将容量全部转换为字节(B)。1 MB = 1024 KB, 1 KB = 1024 B。所以 256 MB = 256 * 1024 * 1024 B。
- 计算整数个数:总字节数 / 每个整数占用的字节数 = (256 * 1024 * 1024) / 4。
- 简化计算:256 * 1024 * 1024 / 4 = 64 * 1024 * 1024。这其实就是 64M 个整数。这里“M”是数量单位百万(10^6),注意和容量单位MB(MegaByte)区分。
2.2 考场上的“坑”与提速技巧
这道题真正的难点不在于计算本身,而在于考场的紧张环境下,你是否能快且准。
- 坑点1:单位混淆。一定要看清题目给的是 MB, GB, KB 还是 bit。如果给的是 Mb (兆比特,常用于网络带宽),那和 MB (兆字节) 相差8倍!
- 坑点2:1024 vs 1000。在计算机存储领域,K, M, G 通常是以1024为进制(即2^10)。虽然有些场合(如硬盘厂商)会用1000,但在蓝桥杯这类编程竞赛的语境下,默认使用1024进制,除非题目特别说明。
- 提速技巧:记住一些常用换算结果。例如:2^10 = 1024, 2^20 ≈ 1e6 (1048576), 2^30 ≈ 1e9。对于上面的例子,看到256MB和32位,可以快速反应:32位是4B,256MB是 256 * 2^20 B。两者相除,256/4=64,结果就是 64 * 2^20,即64M个。这样心算就能出答案,节省大量时间。
注意:蓝桥杯填空题通常需要提交整数答案。对于
64 * 1024 * 1024这种,要老老实实算出来是67108864,不要直接填64*1024*1024或64M。
3. 试题B:卡片(模拟与思维边界)
“卡片”题是蓝桥杯非常经典的一类题,通常给你一堆数字卡片(比如数字0-9各有若干张),问你从1开始拼数字,最多能拼到哪个数字。2021年这题也不例外,但往往藏着一些需要仔细推敲的细节。
3.1 问题建模与朴素解法
假设我们拥有数字卡片0-9,每种卡片2021张(这是2021年题目的一个特色数字)。我们从1开始,拼出数字1,消耗一张‘1’卡片;拼出数字2,消耗一张‘2’卡片……以此类推,直到某种卡片不够用为止。
最直接的思路就是模拟:
- 初始化一个长度为10的数组
cnt[10],记录0-9每种卡片的剩余数量,初始值均为2021。 - 设一个变量
i = 1,开始循环。 - 对于当前数字
i,将其每一位数字分解出来。例如i=123,分解出 ‘1‘, ’2‘, ’3‘。 - 检查
cnt[1],cnt[2],cnt[3]是否都大于0。如果都大于0,则将这些计数减1,表示消耗了这些卡片,然后i++继续下一个数字。 - 如果某一位数字
d对应的cnt[d] == 0,说明卡片d已经用完,无法拼出数字i。那么能拼出的最大数字就是i-1。
3.2 代码实现与细节
#include <iostream> using namespace std; int main() { int cnt[10]; for (int i = 0; i < 10; i++) cnt[i] = 2021; // 初始化每种卡片2021张 int num = 1; // 从1开始拼 while (true) { int temp = num; // 分解数字num的每一位 while (temp > 0) { int digit = temp % 10; // 取出个位 if (cnt[digit] == 0) { // 卡片不够用了 cout << num - 1 << endl; // 能拼到的最大数字是前一个 return 0; } cnt[digit]--; // 消耗一张卡片 temp /= 10; // 去掉个位 } num++; } return 0; }3.3 深入思考:为什么不是“1”最先用完?
很多同学直觉上会觉得数字‘1’用得最多,因为它出现在1, 10, 11, 12..., 21, 31... 等多个数字中。模拟结果也往往确实是‘1’最先耗尽。但这并不是绝对的真理,它取决于初始卡片的数量。如果‘1’卡片给得特别多,而其他某个数字(比如‘0’)给得很少,那么瓶颈就可能转移。
这道题的精髓在于模拟的准确性和对“耗尽”条件的判断。在循环中,必须对当前数字num的每一位进行“预检查”或“即时检查并回滚”,确保不会出现部分位数卡片被消耗后,后几位卡片不足导致状态错误的情况。上面的代码采用的就是“即时检查并消耗”的方式,一旦发现某一位不足,立刻宣布num无法拼出,此时num-1就是答案。
一个常见的错误是:先消耗所有位数的卡片,然后再判断。如果数字是101,你先消耗了‘1’和‘0’,然后发现‘1’卡片不够了(假设只剩1张),但此时‘0’卡片已经被错误地消耗掉了一张。这会导致后续计数不准。所以,要么在消耗前做完全检查,要么像上面代码一样,在消耗每一位时立即判断,一旦失败,整个数字num就失败了,且因为我们是顺序尝试,之前的状态都是正确的。
4. 试题C:直线(几何、精度与去重)
这道题是当年讨论度很高的题目,它要求计算平面上一系列给定整点(比如0 <= x, y <= 19的网格点)所能确定的不同直线的数量。这题完美融合了数学、编程和思维严谨性。
4.1 思路分析:如何表示一条直线?
两点确定一条直线。最直观的想法是枚举所有点对(A, B),然后求出它们确定的直线,最后去重。关键就在于“如何表示一条直线”以便于去重。
常用的表示方法有:
- 斜截式:
y = kx + b。用(k, b)作为直线的“指纹”。但这里有个大坑:斜率不存在(竖直线)的情况需要单独处理。 - 一般式:
Ax + By + C = 0(A, B不同时为0)。可以约化为最简形式,即A, B, C除以它们的最大公约数,并保证第一个非零系数为正。这样(A, B, C)的三元组可以作为唯一标识。 - 两点式衍生:使用
(Δx, Δy, x0, y0)或其他组合,但本质上还是要归一化。
我推荐使用一般式,因为它能统一处理所有情况(包括竖直线)。对于两点(x1, y1)和(x2, y2):
A = y2 - y1B = x1 - x2// 注意这里是 x1 - x2,这样A*x1 + B*y1 + C = 0推导时符号正确C = x2*y1 - x1*y2
得到A, B, C后,我们需要将其化为最简形式:
- 计算
g = gcd(gcd(A, B), C)。gcd是求最大公约数的函数。 - 如果
g != 0,令A/=g, B/=g, C/=g。 - 符号标准化:为了使同一条直线有唯一的表示,我们约定让第一个非零的系数为正数。遍历
A, B, C,找到第一个不为0的数,如果它是负数,则将A, B, C同时乘以-1。
4.2 代码实现与去重技巧
#include <iostream> #include <set> #include <cmath> using namespace std; struct Point { int x, y; }; int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } int main() { vector<Point> points; // 假设网格点是 x, y 从 0 到 19 for (int x = 0; x <= 19; x++) { for (int y = 0; y <= 19; y++) { points.push_back({x, y}); } } set<tuple<int, int, int>> lines; // 使用集合自动去重 int n = points.size(); for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { // j从i+1开始,避免重复枚举同一点对 Point& p1 = points[i]; Point& p2 = points[j]; int A = p2.y - p1.y; int B = p1.x - p2.x; int C = p2.x * p1.y - p1.x * p2.y; // 化为最简整数比 int g = gcd(abs(A), gcd(abs(B), abs(C))); if (g != 0) { A /= g; B /= g; C /= g; } // 符号标准化:让第一个非零系数为正 if (A < 0 || (A == 0 && B < 0) || (A == 0 && B == 0 && C < 0)) { A = -A; B = -B; C = -C; } lines.insert({A, B, C}); } } cout << lines.size() << endl; return 0; }4.3 浮点数陷阱与整数化的必要性
如果你尝试用double类型的(k, b)来存储斜截式,你会遇到灾难性的精度问题。因为点的坐标是整数,计算出的k和b可能是分数。由于浮点数存储和比较的不精确性,两条数学上相同的直线,其k和b的浮点表示可能略有不同,导致set或map认为它们是不同的直线,从而造成错误计数。
例如,由点 (0,0) 和 (2,1) 确定的直线,斜率k=0.5。由点 (0,0) 和 (4,2) 确定的直线,斜率k=0.5。理论上它们应该相同。但在浮点数计算中,(double)1/2和(double)2/4在内存中的二进制表示可能完全一致,也可能因为中间运算过程不同而产生极其微小的差异。依赖==运算符或直接存入set<double>是不可靠的。
因此,必须使用整数来表示直线的参数,并通过最大公约数(GCD)进行归一化,才能保证比较的绝对准确性。这是解决此类几何计数问题的核心技巧。
5. 试题D:货物摆放(数论与因子枚举)
这道题是经典的因子组合问题。题目通常会给一个很大的整数N(例如2021041820210418),问有多少种不同的三元组(a, b, c)满足a * b * c = N,并且考虑顺序(即(1,1,2)、(1,2,1)、(2,1,1)算作不同的方案)。
5.1 暴力枚举的不可行性
最傻的办法是三层循环枚举a, b, c,但N的规模巨大(10^16量级),这显然会超时。我们必须寻找更聪明的方法。
5.2 优化思路:枚举因子
关键在于意识到,如果a * b * c = N,那么a、b、c都必须是N的因子。因此,我们可以:
- 先求出
N的所有因子,存储在一个数组factors中。 - 然后三层循环枚举
factors中的元素作为a,b,c。 - 检查
a * b * c == N,计数。
这样,循环次数就从O(N^(3/2))降到了O(d(N)^3),其中d(N)是N的因子个数。对于N在10^16这个量级,其因子个数通常不会超过几万个(实际上远小于这个数),这使得枚举成为可能。
5.3 高效求所有因子与代码实现
求所有因子可以通过遍历1到sqrt(N)来实现。
#include <iostream> #include <vector> #include <cmath> using namespace std; typedef long long LL; int main() { LL N = 2021041820210418LL; vector<LL> factors; // 求N的所有因子 for (LL i = 1; i <= sqrt(N); i++) { if (N % i == 0) { factors.push_back(i); if (i != N / i) { // 避免重复添加平方根 factors.push_back(N / i); } } } LL ans = 0; int size = factors.size(); // 三层循环枚举因子 for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { // 一个小优化:如果 a*b 已经大于 N,那么 c 必然小于1,不可能为整数因子,可以跳过 // 但更简单的是直接枚举c,因为因子个数不多 for (int k = 0; k < size; k++) { if (factors[i] * factors[j] * factors[k] == N) { ans++; } } } } cout << ans << endl; return 0; }5.4 进一步优化与思维延伸
上述代码在因子个数较多时(例如几千个),三层循环(O(d^3))可能还是有点慢。可以进一步优化:
- 我们可以只枚举
a和b,然后计算c = N / (a * b)。只需要判断c是否为整数(即N % (a*b) == 0)即可。这样复杂度降为O(d^2)。 - 更进一步,由于因子是成对出现的,枚举时可以做一些剪枝。
但就蓝桥杯的时限和N的具体数值而言,O(d^3)的暴力枚举通常也能在1秒内完成。这道题考察的就是选手能否从“暴力枚举所有数”的思维,跳跃到“只枚举因子”的思维。这是一种非常重要的优化思路,在解决与整除、因子相关的问题时非常常用。
6. 试题E:路径(图论与最短路)
“路径”题通常是一个图论最短路问题。题目描述了一个有N个节点(编号1到N)的图,节点a和节点b之间有一条边,边权是lcm(a, b)(a和b的最小公倍数)。要求计算从节点1到节点N的最短路径长度。
6.1 问题抽象与建图
这是一个标准的单源最短路问题。图的顶点是1, 2, ..., N。对于任意两个不同的顶点a和b,如果它们满足某种条件(比如abs(a-b) <= 21,这是2021年题目的一个关键约束),那么它们之间就有一条无向边,边权为lcm(a, b)。
如果没有abs(a-b) <= 21这个限制,那么这就是一个完全图,边数约为N^2/2,对于N=2021来说,边数超过200万,虽然仍可处理,但内存和时间消耗较大。加上这个限制后,每个节点只与附近最多21个节点相连,总边数约为21N,大大减少了计算量。
6.2 算法选择:Dijkstra 算法
由于边权均为正数(最小公倍数肯定为正),求单源最短路的标准算法是Dijkstra 算法。我们可以使用优先队列(堆)优化的版本,时间复杂度为O((V+E) log V),其中V是顶点数N,E是边数~21N,对于N=2021来说非常快。
6.3 代码实现细节
#include <iostream> #include <vector> #include <queue> #include <climits> using namespace std; typedef long long LL; typedef pair<LL, int> P; // (距离, 节点编号) LL lcm(LL a, LL b) { return a / __gcd(a, b) * b; // 先除后乘,防止溢出 } int main() { const int N = 2021; const int MAX_DIFF = 21; // 建图:邻接表 vector<vector<P>> graph(N + 1); for (int a = 1; a <= N; a++) { for (int b = a + 1; b <= N && b - a <= MAX_DIFF; b++) { LL weight = lcm(a, b); graph[a].push_back({weight, b}); graph[b].push_back({weight, a}); // 无向图 } } // Dijkstra vector<LL> dist(N + 1, LLONG_MAX); vector<bool> visited(N + 1, false); priority_queue<P, vector<P>, greater<P>> pq; // 最小堆 dist[1] = 0; pq.push({0, 1}); while (!pq.empty()) { auto [d, u] = pq.top(); pq.pop(); if (visited[u]) continue; visited[u] = true; if (u == N) break; // 找到终点,提前结束 for (auto& [w, v] : graph[u]) { if (!visited[v] && dist[u] + w < dist[v]) { dist[v] = dist[u] + w; pq.push({dist[v], v}); } } } cout << dist[N] << endl; return 0; }6.4 注意事项与常见错误
- 边权计算:
lcm(a, b) = a * b / gcd(a, b)。注意计算顺序,a*b可能会溢出int范围(2021*2021约4百万,在int范围内,但先乘后除是良好习惯)。使用long long更安全。 - 无向图:记得添加双向边。
- Dijkstra的标记:使用
visited数组来标记已确定最短距离的节点是必要的,可以避免重复入队和错误更新。从优先队列中取出的节点,如果其距离值大于当前记录的dist[u],说明这是旧的、无效的队列条目,直接跳过。 - 终点优化:当从队列中取出的节点
u就是终点N时,它的距离已经是最短距离,可以直接跳出循环。这是一个有效的优化。
这道题是经典的模板题,考察的是选手对基础图论算法(Dijkstra)的掌握和实现能力,以及对问题抽象和建图的理解。
7. 试题F:时间显示(模拟与取模运算)
这是一道简单的模拟题,考察对时间单位换算和取模运算的掌握。题目会给一个毫秒级的时间戳(从1970年1月1日00:00:00开始经过的毫秒数),要求你输出这个时间戳对应的HH:MM:SS格式,忽略年月日,只显示时分秒,并且毫秒部分要四舍五入或直接舍去(具体看题目要求,通常是直接舍去)。
7.1 解题步骤分解
- 去除整天数:1天 = 24小时 = 24 * 60 * 60 * 1000 毫秒。用总毫秒数
t对这个值取模,得到当天内的毫秒数t_day。t_day = t % (24 * 60 * 60 * 1000)。 - 计算小时:1小时 = 60 * 60 * 1000 毫秒。用
t_day除以这个数,得到小时数HH。HH = t_day / (60 * 60 * 1000)。 - 计算分钟:从
t_day中减去小时部分占用的毫秒数,得到剩余毫秒数t_remain。然后,1分钟 = 60 * 1000 毫秒。用t_remain除以这个数,得到分钟数MM。MM = t_remain / (60 * 1000)。 - 计算秒:再从
t_remain中减去分钟部分占用的毫秒数,得到最后的毫秒数,再除以1000得到秒数SS。注意题目要求,通常直接取整(舍去毫秒),即SS = t_remain / 1000。
7.2 代码实现与格式化输出
#include <iostream> #include <iomanip> using namespace std; typedef long long LL; int main() { LL t; cin >> t; // 输入时间戳(毫秒) // 1. 去除整天数,得到当天内的毫秒数 LL millisecondsPerDay = 24 * 60 * 60 * 1000; LL t_in_day = t % millisecondsPerDay; // 2. 计算小时、分钟、秒 LL millisecondsPerHour = 60 * 60 * 1000; LL millisecondsPerMinute = 60 * 1000; LL millisecondsPerSecond = 1000; LL hours = t_in_day / millisecondsPerHour; t_in_day %= millisecondsPerHour; LL minutes = t_in_day / millisecondsPerMinute; t_in_day %= millisecondsPerMinute; LL seconds = t_in_day / millisecondsPerSecond; // 直接舍去毫秒 // 3. 格式化输出,不足两位补0 cout << setfill('0') << setw(2) << hours << ":" << setw(2) << minutes << ":" << setw(2) << seconds << endl; return 0; }7.3 易错点分析
- 数据范围:时间戳
t可能很大,必须使用long long(或int64_t) 来存储。 - 取模运算的理解:
t % (24*60*60*1000)这一步是关键,它直接去掉了所有“天”的影响,只留下不足一天的余数。很多同学会先除以1000转换成秒再计算,这也可以,但要注意在转换过程中不要丢失精度或溢出。 - 四舍五入 vs 直接舍去:务必仔细阅读题目要求。蓝桥杯的这道题通常要求直接舍去毫秒,而不是四舍五入。所以
seconds = t_remain / 1000是整数除法,直接截断小数部分。 - 格式化输出:输出必须是
HH:MM:SS的两位数字格式,需要用setw(2)和setfill('0')来控制。这是基础但容易忘记的细节。
这道题是典型的“签到题”,旨在稳定军心。只要细心,确保单位换算和取模运算正确,就能稳稳拿分。
8. 试题G:砝码称重(动态规划或DFS)
这是一道经典的**动态规划(DP)**问题,也可能用深度优先搜索(DFS)配合记忆化来解决。题目通常给出若干种重量的砝码,每种有若干个,问用这些砝码(可以放在天平左右两边)能称出多少种不同的正整重量。
8.1 问题理解与状态定义
关键点在于砝码可以放在天平左右两边。放在左边(物品盘)相当于“加”,放在右边(砝码盘)相当于“减”。因此,每个砝码有三种状态:不用、放左边(+w)、放右边(-w)。
我们可以把问题转化为:给定一个可正可负的砝码重量集合,通过给每个砝码分配一个系数(-1, 0, 1),求所有可能的系数组合下,总和的绝对值有多少种不同的正整数值。
定义DP状态:dp[i][j]表示考虑前i个砝码(这里的“个”是指种类,需要处理数量),能否称出重量j。由于重量可能为负,我们需要一个偏移量Bias将负下标映射到正数。j的范围是[-sum, sum],其中sum是所有砝码总重量之和。
更常见的做法是定义dp[i][j]为前i种砝码能否称出重量j(j为偏移后的非负索引)。
8.2 动态规划转移方程
假设我们有n种砝码,第i种重量为w[i],数量为c[i]。 总重量上限M = sum(w[i] * c[i])。 偏移量B = M,这样dp数组的第二维大小设为2*M+1,下标j对应实际重量j - B。
初始化:dp[0][B] = true(一个砝码都不用,能称出重量0)。
对于第i种砝码,我们有c[i]个。这是一个多重背包问题。我们可以将其转化为“二进制拆分”的01背包,或者直接进行三重循环(但效率较低)。对于蓝桥杯的规模,直接三重循环(遍历种类、遍历状态、遍历该种类砝码的使用个数)有时也能过。
简化版(假设每种砝码只有一个,即01背包)的状态转移:dp[i][j] = dp[i-1][j] || dp[i-1][j-w[i]] || dp[i-1][j+w[i]]意思是,当前状态j可以由前i-1个砝码直接称出(不用第i个),或者由前i-1个砝码称出j-w[i](第i个放左边),或者称出j+w[i](第i个放右边)。
对于多重砝码,我们需要在最内层循环中,遍历使用该种砝码k个(k从0到c[i]),并且每个砝码可以加可以减,情况会复杂一些。更清晰的做法是使用布尔DP结合滚动数组优化。
8.3 代码实现示例(布尔DP)
#include <iostream> #include <vector> #include <bitset> using namespace std; int main() { int n; cin >> n; vector<int> w(n), c(n); int total = 0; for (int i = 0; i < n; i++) { cin >> w[i] >> c[i]; total += w[i] * c[i]; } int B = total; // 偏移量 int SIZE = 2 * total + 1; // 使用bitset优化空间和时间,dp[j]表示当前能称出偏移后的重量j bitset<200005> dp; // 大小根据total估算,这里假设total最大为10万 dp.set(B); // 初始化,重量0(对应下标B)是可达的 for (int i = 0; i < n; i++) { // 遍历砝码种类 for (int k = 0; k < c[i]; k++) { // 对于每个砝码,逐个处理(转化为01背包) // 注意:因为砝码可以放左或右,我们需要同时更新加和减的状态 // 使用临时bitset记录本轮更新,避免新状态影响本轮其他状态的判断 bitset<200005> temp = dp; dp |= (temp << w[i]); // 放左边(加) dp |= (temp >> w[i]); // 放右边(减) } } // 统计能称出的正整重量种类(排除重量0) int ans = 0; for (int j = B + 1; j <= B + total; j++) { // 只统计正重量部分 if (dp[j]) { ans++; } } cout << ans << endl; return 0; }8.4 使用bitset的巧妙之处
上面的代码使用了bitset,这是一个非常强大的工具,尤其适合这种布尔状态的DP。
dp.set(B)将第B位设为1,表示重量0可达。temp << w[i]表示将所有可达状态“加”上w[i]。temp >> w[i]表示将所有可达状态“减”去w[i]。dp |= ...将新的可达状态合并到原状态中。
这种方法高效且代码简洁,完美地处理了砝码可加可减的情况。最终,dp中所有为1的位,就代表了所有可以称出的重量(带偏移)。我们只需要统计偏移后大于B(即实际重量为正)的位有多少个即可。
这道题是动态规划的经典应用,考察选手将实际问题转化为背包模型,并运用位运算技巧进行优化的能力。
9. 总结与备赛建议
复盘2021年蓝桥杯国赛CB组的题目,我们可以清晰地看到其考察脉络:从基础的计算机原理和模拟(A, B, F),到数学几何与算法思维(C, D),再到经典的图论和动态规划算法(E, G)。题目难度梯度明显,既有送分题保证基础分,也有需要深入思考和精巧实现的题目来拉开差距。
对于备赛的同学,我有以下几点建议:
- 吃透基础:像A、B、F这类题,目标必须是满分。任何单位换算、模拟细节、格式化输出的错误都是不可原谅的。平时练习就要追求一次通过,培养“零失误”的稳定心态。
- 掌握核心算法模板:最短路(Dijkstra, Floyd)、动态规划(背包、线性DP)、搜索(DFS, BFS)、并查集、最小生成树、二分查找等,必须做到熟练默写,理解其适用场景和变种。试题E和G就是模板的直接或变形应用。
- 提升数学与思维能力:试题C和D要求更高的思维水平。C题考验的是在几何问题中处理精度和唯一性的能力,D题考验的是将暴力枚举优化为因子枚举的洞察力。这需要平时多做题,多总结,看到“求方案数”、“有多少种”这类问题,要下意识地想到排列组合、因子、容斥原理等数学工具。
- 注意数据范围与精度:这是蓝桥杯,也是所有算法竞赛的永恒考点。看到题目先看数据范围,决定使用
int还是long long。涉及浮点数比较时,优先考虑能否转化为整数运算(如C题)。涉及大数运算时,考虑是否会溢出。 - 实战模拟与时间管理:在最后冲刺阶段,一定要进行全真模拟。用历年真题,设定4小时倒计时,完整地做一套。练习如何分配时间:简单题快速通过(30分钟内),中等题稳扎稳打(60-90分钟),难题尽力而为。切忌在一道题上卡死过久。
国赛的舞台,比拼的不仅是知识储备,更是心态、速度和准确性。希望这份针对2021年CB组的超详细题解,能帮助你更好地理解题目背后的思维逻辑和实现细节。真正的提升来自于动手实践,不妨现在就打开编译器,把这几道题自己从头到尾实现一遍,遇到卡壳的地方再回来看解析,这样的收获才是最大的。祝你在未来的比赛中取得理想的成绩!