蓝桥杯抱佛脚第一天:简单模拟、set、map的使用
距离蓝桥杯还有几天,如果你是那种“平时没怎么刷题、临到比赛才开始慌”的人,这篇就是写给你的。我当年也是这么过来的——真正认真准备的时间不到一周,最后能拿个省奖,靠的就是把“简单模拟、set、map”这几个最基础的考点吃透。别小看它们,蓝桥杯填空和编程大题里,有很大一部分都在考这些基本功,只是包装得花里胡哨而已。
这篇文章带你用第一天的节奏,把简单模拟题的套路、set和map的常用法、以及最容易踩的坑一次性梳理清楚。全程不扯虚的,直接讲怎么用、怎么想、怎么写,看完你就能上手刷题。适合所有准备蓝桥杯、天梯赛或其他算法竞赛、但还没系统复习过基础容器的同学,也适合那些刷题时老在 set 和 map 上报编译错、超时的新手。
1. 内容整体设计与思路拆解
1.1 为什么“抱佛脚”要从这三样开始
临时抱佛脚最忌讳的是“贪多嚼不烂”。我的看法是:第一天只攻三件事——简单模拟、set、map。这三个东西恰好覆盖了蓝桥杯初级题的三大核心需求:把题读懂并实现、对数据去重排序、快速查找计数。
简单模拟题不涉及高深算法,考的是“读题能力”和“代码还原能力”,题目怎么描述,你就怎么写,这是很多基础分的来源。set和map则属于标准模板库里的关联容器,它们能帮你省掉大量手写排序、去重、查找的功夫。比赛时候拼的就是谁快谁准,能用现成容器解决的问题,别自己去写链表和二分查找。
从比赛题型的分布来看,蓝桥杯省赛的C/C++组里,前几道题经常是:日期计算、矩阵变换、字符串处理、数位拆分、统计频次。这些题用模拟加set/map的组合拳,通常几分钟就能拿下一个完整答案,性价比极高。
1.2 这三者各自扮演什么角色
简单模拟是“骨架”,决定你的代码结构顺不顺。它考验的是你能不能把一个自然语言描述的过程,翻译成一段逐行执行、状态清晰的程序。很多同学模拟题做不对,不是代码写不出来,而是漏了题目的某个条件,比如“当x大于等于10时停止”被写成了“当x大于10时停止”,这种细节一错,结果全偏。
set是“筛子”,专门处理“去重+排序”。比如数据里有一堆重复的编号,你想知道有多少种不同的编号,或者想按序输出这些编号,直接把所有数塞进set,它自动帮你排好并去掉重复值。
map是“账本”,专门干“键值对映射”的活。比如给你一串字符串,统计每个字符出现的次数,map就能做到“字符到次数的绑定”。再比如模拟比赛计分,选手编号到总分的映射,map也是首选。
这三个东西单独用很简单,但在比赛里它们往往混着来。比如先模拟读入数据存入map,再用set去重输出;再比如用map统计后再按值排序输出结果。所以第一天的目标不是单个函数会用,而是能自然地把它们串到一道题里。
2. 简单模拟:把题目翻译成代码
2.1 模拟题的经典套路:三步走
我拿到一道模拟题,不管现场多紧张,都强迫自己按三步来走。第一步,把题目里的“过程”拆成“步骤”。比如题目说“有一个数,如果它是偶数就除以2,如果是奇数就乘3加1,一直重复直到变成1”——这本质上就是一组“判断+操作”的循环,判断条件是什么,操作是什么,循环出口是什么,三条线立刻清晰。
第二步,确定需要哪些变量来记录状态。每个步骤里有哪些数会被改变?这些数需要用变量存下来吗?比如上面的例子,只需要一个变量用于保存当前数值即可。
第三步,想清楚“程序什么时候结束”,也就是退出条件。这个往往是模拟题的隐藏考点。很多题的坑在于“当满足某个条件时停止”,但这个条件可能在操作前判断,也可能在操作后判断,位置不同结果完全不同。建议在草稿纸上手动跑一遍题目给的示例,把每一步的中间结果写出来,再用代码去复现这个过程,就知道判断放哪里了。
2.2 模拟题代码框架
模拟题的代码框架其实非常统一,基本上长这样:
#include <bits/stdc++.h> using namespace std; int main() { // 1. 初始化变量 // 2. 读入数据 // 3. 主循环/主逻辑:while 或 for // 4. 输出结果 return 0; }举个例子,一道非常经典的模拟题——报数游戏。从1开始报数,报到数字中含有某个指定数字或者能被它整除的人要跳过,输出第n个报出来的数字。这种题就是典型的“翻译题目”:
#include <bits/stdc++.h> using namespace std; bool check(int x, int target) { if (x % target == 0) return true; // 能被整除 while (x) { if (x % 10 == target) return true; // 某一位包含目标数字 x /= 10; } return false; } int main() { int n, target; cin >> n >> target; int cnt = 0, cur = 1; while (cnt < n) { if (!check(cur, target)) cnt++; cur++; } cout << cur - 1 << endl; return 0; }这里有一个非常容易被忽略的点:最后输出的是cur-1而不是cur,因为循环里每次cur++之后还要再判断。如果你在草稿纸上手动模拟一遍,这个坑就能避免。我见过很多同学把输出写成cur,结果总是比标准答案大1。
2.3 日期和矩阵:模拟题的两大常客
蓝桥杯特别喜欢考日期题,比如“给你两个日期,求它们之间相隔多少天”。这类题的核心套路是:把日期转换成“从某个基准日算起的天数”,再相减。你可以把两个日期都转换成从公元1年1月1日到该日期的总天数,然后做差取绝对值。
实现的时候注意两点:判断闰年的条件是“能被4整除但不能被100整除,或者能被400整除”,很多新手写成year % 4 == 0,到了公元1900年这种百年年份就出错了。还有,计算月份天数建议用预处理的月份天数数组,而不是写一堆switch分支,数组写起来快也不容易漏。
矩阵类模拟题常考“旋转、翻转、填充”。比如将一个矩阵顺时针旋转90度,核心是找到坐标映射关系:原来的(i, j)会变成(j, n - 1 - i)。记不住公式也没关系,在草稿纸上画一个3x3的矩阵,手动推一下坐标变化,比背任何公式都可靠。这里我强烈建议写一个通用的“坐标变换小工具函数”,旋转、翻转全部自己写,方便调试。
还有一个模拟题里常见但新手容易忽略的点——多组数据处理。题目说“每个测试点包含多组测试样例”,结果很多人只处理了一组。在写模拟循环之前,先看有没有while (cin >> n)这样的写法需求。如果是多组样例,记得每组样例的输出之间要不要空行,这也是容易扣分的地方。
3. set:自动去重排序的“自动筛子”
3.1 set到底解决了什么问题
先想一个场景:给你一堆数,让你输出里面不重复的数,并且按从小到大排序。如果用普通数组,你得先排序,再去重,代码量大而且容易出错。用set的话,一行代码插入,它内部自动排序并且保证元素的唯一性。
set底层是红黑树,插入、删除、查找的时间复杂度都是O(logn)。对于比赛里那种n <= 100000的数据量,这个复杂度完全够用。如果你不需要排序、只想去重,可以考虑unordered_set,它的查找复杂度平均是O(1),但输出顺序是乱的。比赛题如果明确“按字典序输出”,那就用set;只判断“有没有出现过”,用unordered_set更合适。
set最常见的操作就这几个:
#include <bits/stdc++.h> using namespace std; int main() { set<int> s; s.insert(5); s.insert(3); s.insert(8); s.insert(3); // 重复插入,set中仍然只有一个3 // 遍历输出,结果一定是 3 5 8 for (auto x : s) cout << x << " "; cout << endl; // 查找:判断元素是否在set里 if (s.count(5)) cout << "5存在" << endl; if (s.find(3) != s.end()) cout << "3也存在" << endl; // 删除 s.erase(5); return 0; }注意count和find的区别:count返回的是0或1(因为set不允许重复),可以直接当布尔用;find返回迭代器,没找到时会返回end()。新手常犯的错是拿find的结果直接当bool判断,写成if (s.find(x)),这编译能过但逻辑是错的,因为没找到时返回的end()迭代器不等于0。我见过不少人在这一行栽跟头。
3.2 set实战:统计不同数字的个数
来看一个简单但很典型的场景:第一行给一个整数n,第二行给n个整数,问这里面有多少个不同的数字,并从小到大输出。
#include <bits/stdc++.h> using namespace std; int main() { int n; cin >> n; set<int> s; for (int i = 0; i < n; i++) { int x; cin >> x; s.insert(x); } cout << s.size() << endl; for (auto x : s) { cout << x << " "; } cout << endl; return 0; }这里set.size()直接给出不同数字的个数,遍历输出时天然有序。肉眼可见省了多少事。如果换成数组写法,你需要先排序再去重再输出,三步骤变一个步骤。
set还能存结构体,但这需要自定义排序规则,也就是重载operator<。蓝桥杯入门阶段比较少用,但如果你想扩展一下,可以了解:结构体里面定义一个比较方式,set就会按你的方式排序。要注意的是,自定义排序不能只写一个return a.x < b.x;,还要考虑如果x相等时怎么办,否则两个x相等但其他字段不同的结构体,可能会被set误认为是同一个元素。想清楚这一点,自定义类型就能安全使用了。
3.3 set的使用注意事项
set用的过程中有几个高频坑,我帮你踩过了,直接记下:
第一个坑——迭代器失效。遍历时删除当前元素要特别小心。下面的写法是错的:
for (auto it = s.begin(); it != s.end(); it++) { if (*it == 3) s.erase(it); // 删除it后,it本身已经失效 }正确写法有两种。一种是先记录下一个位置再删除:
for (auto it = s.begin(); it != s.end(); ) { if (*it == 3) it = s.erase(it); // erase返回下一个迭代器 else it++; }另一种是直接用C++11的remove_if思路结合临时容器,不过对set来说,上面那种erase返回迭代器的写法最省心。
第二个坑——用auto遍历时只能读不能改。因为在遍历时修改set元素会破坏红黑树的结构,所以元素的值本质上是const的。如果你确实要修改某个值,办法是删掉旧值再插入新值。
第三个坑——set比较字符串时默认按字典序。这通常是对的,但如果你存的是字符串形式的数字,比如"10"和"9",那字典序会认为"10"排在"9"前面。这种情况需要传入自定义比较器,或者把字符串转成数字再存入。比赛里如果出现字符串数字排序异常,十有八九是这个原因。
4. map:键值对映射的“万能账本”
4.1 map基础和常用操作
map把“键”和“值”绑定起来,键是唯一的,值可以被覆盖。你可以把map想象成一个只有特定索引才能访问的数组:数组的下标只能是整数,而map的下标可以是int、string、甚至结构体。
最常用的场景就是计数。统计一段文本中每个单词的出现次数,代码量少得惊人:
#include <bits/stdc++.h> using namespace std; int main() { map<string, int> mp; string s; while (cin >> s) { mp[s]++; } for (auto &p : mp) { cout << p.first << " " << p.second << endl; } return 0; }注意这里遍历时p.first是键,p.second是值。用auto遍历时,p是一个pair,值可以修改,所以用引用&是允许的,但如果只读,写不写&都行。map默认按键从小到大排序,所以输出单词时会自动按字典序排列。
map的其他常用操作还有:mp.count(key)判断键是否存在,mp.find(key)得到指向该键值对的迭代器,mp.erase(key)删除指定键,mp.size()得到键值对个数。这些跟set的接口高度一致,因为set和map底层都是红黑树,只不过map每个节点多存了一个值而已。
4.2 map实战:统计字符出现频率并按值排序
蓝桥杯有一类经典题,统计字符串中每个字符出现的次数,然后按次数从高到低输出。这个题光用map是不够的,因为map默认按“键”排序,而你需要按“值”排序。我的做法是先把map统计好,再把map里的数据转存到vector里,用自定义排序输出:
#include <bits/stdc++.h> using namespace std; int main() { string s; cin >> s; map<char, int> mp; for (char c : s) mp[c]++; vector<pair<char, int>> v(mp.begin(), mp.end()); sort(v.begin(), v.end(), [](const pair<char, int>& a, const pair<char, int>& b) { if (a.second != b.second) return a.second > b.second; // 先按次数降序 return a.first < b.first; // 次数相同按字典序 }); for (auto &p : v) { cout << p.first << " " << p.second << endl; } return 0; }这里有几件事必须提一下。第一,用vector<pair<char, int>> v(mp.begin(), mp.end())可以一键把map的内容“倒”进vector,这是非常省事的写法。第二,自定义排序的lambda里要写清楚:先比次数,次数一样再比字母。很多新手只写了return a.second > b.second,忘了处理次数相同时的情况,导致输出乱序。
4.3 map注意的坑:下标访问与键不存在
map的坑主要集中在下标操作上。用mp[key]++的时候,如果key不存在,map会先自动创建一个“键为key,值为0”的节点,然后再做自增。这在统计场景里很舒服,但如果你只是为了“判断key是否存在”,千万别用mp[key],因为这会无中生有,把一个不存在的键塞进map。
比如下面的代码就有问题:
if (mp["某些key"] == 1) { ... } // 如果key不存在,会先插入一个默认值0这会导致你的map变大,遍历时多出很多脏数据。判断键是否存在,用mp.count()或者mp.find()。
还有一个问题是性能。mp[key]++第一次访问时,如果key不存在,底层会做一次默认构造再加一次赋值,虽然是常数级的损耗,但如果循环百万次,而且key大量重复,这个损耗也可以感受到。更稳妥的写法是:
auto it = mp.find(key); if (it != mp.end()) it->second++; else mp[key] = 1;不过说句实话,比赛时大多数情况下直接mp[key]++就够了,不需要过度优化。只是得知道这个原理,万一题目数据给得特别大,你能快速定位到性能瓶颈在哪里。
5. 模拟、set、map的综合应用
5.1 一道题串起三样东西
学完单个容器,最怕的是实战时不知道什么时候该用哪个。我先出一道综合题,把三者全用上。
题目描述大概是这样的:某平台举办了一个打卡活动,每天有一堆人来打卡,每个人用一个字符串ID表示。现在给你若干天的打卡记录,每行一个字符串表示当天打卡人的ID,可能有人重复打卡。请你统计一共有多少人参与过活动,并按字典序输出这些人的ID;另外,统计每个人打卡的总次数,并按打卡次数从高到低输出,次数相同则按ID字典序。
好,我们来拆题。统计“一共有多少人参与过活动”且“按字典序输出ID”,这是set的经典应用。把所有人的ID都塞进set,自然就完成了去重和排序。统计“每个人打卡多少次”,用map计数。后面要按次数排序,把map转成vector再自定义排序。整个题的代码就是这三段的拼接:
#include <bits/stdc++.h> using namespace std; int main() { int lines; cin >> lines; set<string> people; map<string, int> cnt; for (int i = 0; i < lines; i++) { string id; cin >> id; people.insert(id); cnt[id]++; } cout << "参与人数:" << people.size() << endl; cout << "参与者字典序输出:"; for (auto &name : people) cout << name << " "; cout << endl; vector<pair<string, int>> v(cnt.begin(), cnt.end()); sort(v.begin(), v.end(), [](const pair<string, int>& a, const pair<string, int>& b) { if (a.second != b.second) return a.second > b.second; return a.first < b.first; }); cout << "按次数排序输出:" << endl; for (auto &p : v) { cout << p.first << " " << p.second << endl; } return 0; }这个例子展示了解决问题的思路:先判断题目要什么,再把每个要求对应到某个容器或某种操作上,最后拼接起来。比赛时遇到综合题,最忌讳的就是拿到题就动手写代码,先在脑子或草稿纸上把“我要用哪些容器”写下来,这个习惯能帮你省下大量改错时间。
5.2 综合题的调试技巧:造数据、看中间结果
综合题的难点往往不在代码本身,而在于出错了不好排查。我的调试流程非常固定,分享给你抄作业。
第一步,先把样例跑通。如果你连样例都过不了,先别急着提交,逐行检查是哪里和预期不符。第二步,生成几组简单的边界数据,比如n=1的情况、所有数据都一样的情况、输入为空的情况。很多隐藏bug在边界数据下会现出原形。第三步,在关键循环里临时加上cout输出中间变量,比如map插入后的内容、set的大小等等。等确认逻辑正确后,再把调试输出删掉。
比如上面那道综合题,你可以临时在插入每个人的cnt[id]++之后加一行cout << "debug: " << id << " " << cnt[id] << endl;,看看每个id的统计值对不对。比赛时这个操作可以帮你定位问题到底出在“读入阶段”还是“排序阶段”。
5.3 复杂度估算:别让代码卡超时
蓝桥杯很多题目的数据范围会给你明示,比如n <= 10^5。如果你用set或map,每次插入O(logn),总复杂度是O(nlogn)。对于10^5级别,这个复杂度毫无压力;对于10^6级别,勉强能跑;如果n到了10^7,那就要考虑换思路了,比如用数组加sort。
有一种情况特别容易超时——频繁查找。比如你有一个map,数据总量是10^5,但你在一个循环里反复对它进行mp.find()查找,循环次数是10^5,那总操作数大约是10^5 * log(10^5),也就是不到两百万次,没事。但如果循环次数是10^7,而且每次都find,那就会到一亿次以上,超时风险就很大。遇到这种场景,可以考虑把map中的数据预先转换到数组或哈希表里再处理。
还有一点,unordered_map和unordered_set虽然平均是O(1)查找,但在极端哈希冲突的情况下可能退化成O(n)。某些比赛数据会专门构造字符串来卡哈希表。稳妥起见,如果你对输入数据的分布没把握,用map比用unordered_map更安全,因为红黑树的复杂度是稳定的对数级别。
6. 高频踩坑与调试技巧实录
6.1 编译期错误:头文件、命名空间和类型
先列几个我见过无数次的编译错误。第一,使用set和map却忘了写头文件,虽然bits/stdc++.h能一网打尽,但如果你比赛环境不支持这个万能头,还是老老实实写#include <set>、#include <map>、#include <vector>、#include <algorithm>。第二,忘记写using namespace std;,导致set、map、cin都不能直接使用,其实编译错误信息已经告诉你了,但新手往往会盯着代码反复看也看不出所以然。
第三类是类型问题。map里p.first的类型和p.second的类型不一样,写排序lambda时参数类型要写对,不能一个写int一个写string。这里建议统一用const pair<string, int>&的形式,别嫌麻烦。
6.2 逻辑错误:多跑一个循环、少写一个break
模拟题最常见的逻辑错误就是循环次数不对。比如“输出前n个满足条件的数”,结果你循环里从头到尾跑了一遍,输出的时候发现多了一个数。解决这个问题的办法是:让循环变量用“已经找到几个数”来驱动,而不是“当前检查到哪个数”来驱动。
比如前面报数的例子,cnt是已经数到的人,cur是当前报的数,循环条件是cnt < n。这样就不会多输出。如果你用的是for (int i = 1; i <= n; i++)去循环检查每个数,那每次都必须恰好找到一个满足条件的数,一旦中间跳过几个人,计数就乱了。
另一个常见问题是break放错位置。处理完一个数后,如果你希望立即结束循环,break必须写在处理代码块的最后一行,而不是下一轮循环里。这个说起来很简单,但调试时经常绕进去。
6.3 数据边界:0、末位、空集合
最后一个高频bug来源是数据边界。对于整数运算,最容易出错的边界是“0”和“负数”。比如统计某个字符串中字符出现的次数,如果你用数组下标做映射,比如cnt[s[i] - 'a']++,那你要保证输入全是小写字母。如果题目没说全是小写,可能出现负下标或数组越界。这种情况改用map更稳妥。
对于set和map,边界问题是“空集合”。比如你遍历一个set去输出内容,如果set是空的,循环体不会执行,输出为空,这没问题。但如果你在循环里访问*s.begin(),而s为空,就会崩溃。我见过新手在遍历前先打印第一个元素,结果空容器直接跑出错误。判断空容器用if (s.empty()),不要用if (s.size() == 0),虽然效果一样,但empty()语义更清晰,也更快。
6.4 复盘:第一天该复习到什么程度
如果你真的只有一天时间来抱佛脚,建议按这个顺序过一遍:先花半小时熟练set和map的基本操作,各写两个小demo;再用一个小时做两到三道模拟题,把“三步走”的套路跑熟;最后用剩下的时间做一道综合题,把容器串起来。做题的时候,强制自己在草稿纸上先写“容器选型”,再写代码。
这里说一句个人体会:抱佛脚并不是让你真的只靠一天,而是用最短的时间把最高频的考点捡起来。set和map是蓝桥杯入门阶段性价比最高的知识点,它们能帮你快速写出正确答案,也能帮你减少代码量。我在实际比赛中遇到不少题,最后就是用set去重、map计数、vector排序这三板斧解决的。你把这套组合练熟了,至少能保证基础题不丢分。
最后分享一个小技巧:比赛前把你常用的代码模板准备好,包括set/map的头文件、遍历方式、排序的lambda写法,甚至是一个简单的fastio(比如ios::sync_with_stdio(false); cin.tie(0);)。这些琐碎的东西如果不提前备好,比赛时真的会浪费宝贵的几分钟。祝你今天抱完佛脚,明天比赛顺利。