1. 模拟赛到底在模拟什么距离省赛还有两三周的时候手里最值钱的复习资料其实不是厚厚的算法书也不是别人的笔记而是一套高质量的模拟赛题。很多同学都问过我同一个问题模拟赛和真题到底差多远我的回答是题型结构几乎一致考点覆盖高度重合差距主要集中在题目包装和个别冷门考点上。换句话说把模拟赛吃透了上了赛场至少能稳住前两三道大题的基本盘。第十一届省赛模拟赛C/C组这份题网上的讨论度不低它涉及的题目类型涵盖了枚举、模拟、搜索、贪心、动态规划、数论基础这些高频考点。认真做完这套题再对照着题解复盘一遍你基本就能摸清省赛的出题套路了。这篇题解我按题型拆开来讲每道题都会给出完整的思路推导和可运行的代码同时把我在做题过程中踩过的坑、总结出的规律一并写出来。不管你是第一次参加省赛还是想冲高名次这篇文章都能作为一份靠谱的参照。先说明一点下面涉及的题目是模拟赛环境下的典型题型和常见变体与正式赛题并非一一对应。我按考法和解法类别来编排重点是你能否通过这些题型掌握对应的算法思路。1.1 省赛的出题风格与应对策略以这项竞赛省赛为例C/C组的高频题型主要集中在几个固定方向上基础输入输出处理、字符串处理、枚举优化、简单贪心、二维数组操作、以及中等难度的动态规划或搜索。模拟赛的题目设计也很有针对性——它不会刻意出偏题怪题而是把常见的考点换一层壳重新包装。这里有个很关键的认知省赛并不是算法竞赛里难度最高的那一档它更看重代码实现的准确性和基础算法的熟练度。很多同学之所以在赛场上拿不到高分不是因为算法不会而是因为代码写得不够扎实小毛病太多或者是题目读一半就开始写结果漏了关键条件。所以刷模拟赛的意义不只是“看看自己会多少题”更是在于训练一种稳定的做题节奏——先读题、再规划、后编码、最后验证。这套流程走熟了省赛基本不会出大问题。1.2 模拟赛题目的难度分层我习惯把模拟赛题目按难度分成三档第一档是送分题通常是填空题或非常简单的应用题考察输入输出、基本循环、条件判断。这些题必须稳拿而且要在10分钟内解决不可以磨蹭。第二档是提高题需要一定的思维量比如枚举加上剪枝、简单的动态规划、高精度计算等。这一档是拉分的关键大部分认真备战的人都能做出来但速度和准确性差异很大。第三档是压轴题通常涉及搜索优化、状态压缩或更复杂的思维模型。这类题能全做出来的属于极少数但你至少要把暴力解法写出来能拿部分分就拿部分分。模拟赛的价值就在于让你看清自己目前停留在这三档的哪一档然后针对性地补弱。2. 高频考点拆解与实现思路2.1 枚举优化暴力不是万能的但没有暴力是万万不能的枚举是省赛出现频率最高的考点没有之一。模拟赛里几乎每套题都会有一道枚举题而且往往不是让你无脑穷举而是需要你在枚举基础上做一些优化否则会超时。举个例子有一类经典题目求区间内满足某个性质的数。如果条件允许第一时间想到的当然是直接遍历区间内每个数逐一检查写出来很简单。但这种做法在区间很大时必挂因为复杂度是O(n)n到10^9就玩不转了。这时候需要观察性质把枚举方式升级。以“回文数判断”为例如果题目要你统计一个区间内的回文数个数暴力做法是遍历区间内每个数然后反转判断但更优的思路是直接构造回文数——枚举左半部分拼接出右半部分再判断是否在区间内。这就把O(n)降到了O(√n × 常数)。#include iostream #include vector using namespace std; // 枚举左半部分构造回文数 int main() { int l, r; cin l r; int cnt 0; for (int i 1; ; i) { string s to_string(i); string rev(s.rbegin(), s.rend()); string palOdd s rev.substr(1); // 奇数长度回文 string palEven s rev; // 偶数长度回文 long long a stoll(palOdd); long long b stoll(palEven); if (a r b r) break; if (a l a r) cnt; if (b l b r) cnt; } cout cnt endl; return 0; }这里用了一个非常常见的技巧利用回文数自身的对称结构从目标区间反推可能的构造方式。思路的本质就是把问题从“数海捞针”变成“按特征生成”这也是枚举优化的核心思想——不要傻乎乎地遍历所有候选而是先缩小候选集合的范围。2.2 经典模拟跟着题意走但别丢分模拟题在省赛里出镜率极高因为它能直接考验参赛者的代码实现能力。这类题没有太多算法上的难度核心就是读题准确、步骤清晰、状态维护正确。模拟赛里有一道经典题描述了一个“机器人按指令移动”的场景指令包括前进、转向、循环若干次等。这种题考察的其实就三点方向如何表示、越界如何判断、循环如何展开。我代码里用的是“方向数组”的做法这是处理网格移动问题的通用手段#include iostream using namespace std; // dx/dy 按顺序表示上、右、下、左 int dx[] {-1, 0, 1, 0}; int dy[] {0, 1, 0, -1}; int main() { int n, m, x, y; cin n m x y; // 网格大小和初始坐标从0开始 string ops; cin ops; int dir 0; // 初始朝上 for (char c : ops) { if (c L) { dir (dir 3) % 4; // 左转 } else if (c R) { dir (dir 1) % 4; // 右转 } else if (c G) { int nx x dx[dir]; int ny y dy[dir]; if (nx 0 || nx n || ny 0 || ny m) continue; // 越界则不移动 x nx; y ny; } } cout x y endl; return 0; }方向数组为什么要按“上右下左”或者“北东南西”这种顺序排列因为这样可以让“左转”“右转”直接通过数组下标的加减和取模完成不需要写复杂的if-else分支。很多同学会把方向坐标写错根源在于没有把方向建模成一个可运算的数值。你也许遇到过这种情况题目一切都算对了最后输出坐标时发现行列搞反了或者方向定义和题目不一致。对此最有效的检查手段是拿题目给的样例手动模拟一遍把每一步的坐标变化写在纸上然后对拍代码输出。样例能过这道题就基本稳了。2.3 贪心策略找对排序依据就成功了一半贪心类题目在每一届省赛基本都有身影而且总是会换上各种包装比如“安排会议室”“搭配订单”“选择区间”等。模拟赛中有一道题大意是给你一堆任务每个任务有截止时间和收益问单位时间内最大收益的调度方案。这类题拿到后别着急写代码先想清楚一个问题“我贪的到底是什么”。常见的有按截止时间排序、按收益排序、按结束时间排序等多种策略选错排序依据代码再漂亮也是错的。典型的正确解法是按截止时间排序然后用一个小根堆维护已选任务的收益当新任务加入时如果总数量超过截止时间就把收益最小的任务弹出去保证每个时间点收益最大化。这就是经典的“后悔”型贪心也是优先队列的典型应用。#include iostream #include vector #include algorithm #include queue using namespace std; struct Task { int dead; int val; }; int main() { int n; cin n; vectorTask a(n); for (int i 0; i n; i) { cin a[i].dead a[i].val; } sort(a.begin(), a.end(), [](Task x, Task y) { return x.dead y.dead; }); priority_queueint, vectorint, greaterint pq; // 小根堆 long long ans 0; for (auto t : a) { pq.push(t.val); ans t.val; if ((int)pq.size() t.dead) { // 任务数超过截止时间说明必须放弃一个 ans - pq.top(); pq.pop(); } } cout ans endl; return 0; }这里“后悔”的含义是先无脑接下任务如果发现当前选择不合法就放弃收益最小的那个任务。这种思路的巧妙之处在于它把决策问题转化成了“动态维护合法集合”的问题不需要每一步都提前想清楚全局最优只是在冲突发生时做局部最优的调整。我自己刚开始学贪心时最大的障碍就是总想着一步到位直接找到最优解反而忽略了“先选、再调整、不断优化”这种策略。你得接受一个事实贪心算法的正确性不是靠直觉而是靠数学归纳或反证法验证的。平时做模拟题时养成分步验证的好习惯比赛时就不容易慌。2.4 搜索入门DFS/BFS的常见套路模拟赛里有一类搜索题难度介于提高题和压轴题之间比如“求迷宫最短路径”“统计连通块个数”等。这类题只要掌握了DFS/BFS的模板大部分都能做出来。以“统计连通块个数”为例思路是遍历整个网格每遇到一个未被访问的目标格子就从这个格子出发做一次DFS或BFS把它所在的整个连通块标记为已访问然后计数加一。核心代码如下#include iostream #include vector using namespace std; int n, m; vectorstring g; vectorvectorbool vis; int dx[] {-1, 1, 0, 0}; int dy[] {0, 0, -1, 1}; void dfs(int x, int y) { vis[x][y] true; for (int i 0; i 4; i) { int nx x dx[i]; int ny y dy[i]; if (nx 0 || nx n || ny 0 || ny m) continue; if (vis[nx][ny] || g[nx][ny] 0) continue; dfs(nx, ny); } } int main() { cin n m; g.resize(n); vis.assign(n, vectorbool(m, false)); for (int i 0; i n; i) cin g[i]; int cnt 0; for (int i 0; i n; i) { for (int j 0; j m; j) { if (g[i][j] 1 !vis[i][j]) { cnt; dfs(i, j); } } } cout cnt endl; return 0; }这里需要注意一个细节DFS的递归深度不能太大。如果网格达到几千乘几千递归栈很容易爆掉这时候应该改用BFS或者显式的栈。省赛的题虽然一般不会把数据量卡到极致的递归深度但养成“大网格优先BFS”的习惯总没有坏处。另外方向数组的顺序没有强制要求但建议始终保持固定顺序比如上右下左这样调试的时候思路不会乱。还有同学会问BFS和DFS应该怎么选——如果在最短路径问题里优先用BFS因为它天然的层次遍历性质可以保证第一次到达目标时的步数就是最短步数如果只是遍历所有可达点DFS写起来更简洁。2.5 动态规划入门从斐波那契到背包的升级路径动态规划这个考点很多人一听就发怵其实省赛的动态规划题目难度并不高主要集中在基础的线性DP和背包问题上。模拟赛里最常见的一道题就是“走楼梯”的变体——每次可以走1级或2级但中间有些台阶不能踩问到终点一共有多少种走法。这种题的解题流程非常固定主要分三步第一步定义状态。设dp[i]表示走到第i级台阶的方法数。第二步确定转移方程。dp[i] dp[i-1] dp[i-2]前提是第i级台阶可踩。第三步确定边界。dp[0] 1站在起点也算一种状态dp[1]看第一级台阶是否可踩。#include iostream #include vector using namespace std; int main() { int n; cin n; vectorbool ok(n 1, true); int m; cin m; for (int i 0; i m; i) { int x; cin x; ok[x] false; } vectorlong long dp(n 1, 0); dp[0] 1; for (int i 1; i n; i) { if (!ok[i]) continue; dp[i] dp[i - 1]; if (i 2) dp[i] dp[i - 2]; } cout dp[n] endl; return 0; }这里特别要提醒一个问题dp数组要用long long而不是int。很多同学第一步没踩坑但是到了第20多个台阶的时候方法数已经超过int的上限了这时再用int存就会出现溢出答案瞬间变成负数。这种低级失误在省赛里不少见你只要比别人细心一点就又多了一分的优势。动态规划题目做题的核心其实不是“套模板”而是“理解状态设计”。你在刷模拟题的时候每做完一道DP题都应该问自己一个问题这个dp状态为什么这样定义能不能用更少维度的状态描述把这个问题想明白省赛DP题型就基本掌握了。3. 实操中的边界处理与调试经验3.1 数据范围你的代码为何超时或溢出我在模拟赛练习阶段发现一个现象十个人里至少有五个人的代码在本地测试没有问题一放进在线评测就挂原因无非就是超时、超内存、答案错误三种。其中答案错误里有相当大比例是因为没注意到数据范围。省赛题目的数据范围一般会在题面里明确给出但容易被忽略。比如有些题会让数值达到10^18这时候int肯定不行甚至long long都需要小心。更隐蔽的情况是中间结果的溢出——即使最终结果放得下计算过程中的临时值也可能溢出。一个典型的案例是计算组合数有人直接套阶乘公式n到20的时候阶乘的结果就已经接近long long的上限了更不用说继续加。这里正确的做法是使用递推公式c[i][j] c[i-1][j] c[i-1][j-1]整个过程都不会出现中间值爆炸的情况。另一个常见的超时原因是模运算用得太多。在一些题里你需要在循环里取模但每取一次模都会消耗几十个时钟周期循环次数大到一定程度时间差距就显现出来了。正确的策略是减少取模的频率或者在确知数据不会溢出的情况下最后统一处理取模。3.2 边界条件最容易丢分的盲区边界条件是一个老生常谈的话题但每次省赛都有人在这里栽跟头。模拟赛中有一道题是“判断年份是否为闰年”看起来简单得不能再简单但很多人会在“世纪闰年”的分支上漏掉条件。闰年的定义有两条普通年份能被4整除且不能被100整除的是闰年能被400整除的也是闰年。只写year % 4 0的人会把1900年误判成闰年这题就直接零分。边界条件的本质是你对题目描述的完整建模。写代码前把可能出现的取值边界列出来最小值、最大值、空输入、重复输入、越界坐标等然后在自己的代码里逐一检查。这听起来麻烦但实际上很省时间——因为在线评测的测试数据通常会卡这些位置。3.3 调试技巧别再用cout硬输出了很多同学调试代码时习惯在关键位置加一行cout输出中间结果然后编译运行肉眼对比输出是否正确。这种做法在简单题里够用但一旦逻辑复杂你就会陷入“改了跑、跑了改”的死循环特别浪费时间。我更推荐的做法是写一个暴力解法然后和优化解法对拍。具体操作很简单写一个肯定正确的暴力枚举程序哪怕很慢再写一个你正在调试的优化程序然后用一个随机数据生成器生成大量的测试输入把两份代码的输出放到一起对比只要有任何不一致就说明优化代码有问题可以直接定位到是哪个环节出了问题。在省赛的模拟练习阶段这套“对拍”流程能帮你节省大量调试时间。平常做题时不要贪图省事绕过这一步对拍的习惯一旦养成你的代码准确率和调试速度都会明显提升。4. 常见问题与做题策略速查4.1 做题顺序先稳再冲别死磕省赛的题型顺序一般是填空、简单计算、代码实现、算法设计题。做题顺序上我的建议是从简单到难先把送分题全部拿到手再做提高题最后才去想压轴题。这种策略能保证你不会因为一开始死磕难题反而把简单题的思路扰乱了甚至没时间写。有同学喜欢一上来就做分值最高的压轴题觉得这样有挑战感但实际上压轴题不一定是最难的可能只是思维量稍大。你要知道压轴题如果写一个暴力做法也许还能拿一部分分而如果因为时间不够漏掉了简单题那是白丢分。我自己参加模拟赛时会把“5分钟没思路”作为一道题目的暂停线。超过这个时间就跳过去先做后面的等把会做的都做完了再回头来啃这块硬骨头。这种策略在我每次模拟练习中都很管用推荐给时间分配容易失控的同学。4.2 读题技巧画出关键条件再动手“题目没读完就开始写代码”这是省赛新手最常见的问题。省赛题目的描述通常会有很多背景故事铺垫真正的约束条件散落在各个段落里。你要做的第一步是把数据范围、输入输出格式、特殊条件全部圈出来。比如有一类题是“多组输入”有些同学只处理了一组就交上去连样例都过不了。还有一种情况是“输出要求保留几位小数”你看漏了这个条件就算逻辑全对输出格式不对也是零分。我习惯的做法是先把题目完整读两遍第一遍了解大意第二遍画出约束条件然后在草稿纸上写清楚输入输出的样例过程。这个习惯看着笨但在高压的比赛环境下能大大降低失误率。4.3 常见编译与运行错误排查这里把我在模拟练习中反复遇到的几类问题整理成一张速查表大家对照着排查就好。错误现象常见原因处理方案编译错误变量名拼写不一致忘了头文件优先检查拼写和include看编译器第一行报错信息运行时崩溃段错误数组越界、栈溢出、空指针检查数组下标边界把大数组移到全局区答案总是差1边界条件没考虑循环偏移手动走一遍样例边界值检查循环终止条件输出负数溢出问题int/ long long使用不当换成long long或者用减法判断替代加法判溢死循环循环变量没更新条件判断写反在循环里加一个输出调试检查变量的变化趋势这些错误看起来琐碎但正是因为琐碎才更需要平时做题时多积累。踩过一遍坑记住了下一次就不会再踩。最怕的是跳进同一个坑好几次那才真的浪费了宝贵的备赛时间。5. 从模拟赛走向省赛的经验复盘我见过很多同学在模拟赛拿了高分但真正上了省赛现场反而发挥失常。原因其实不在知识储备而在心理状态和做题节奏的把控。模拟赛给了你一次模拟真实赛场的练习机会你要珍惜这个过程把它当成真正的比赛来对待才能训练出稳定的心态。备赛阶段最重要的不是“刷了多少题”而是“每道题是否吃透”。吃透的标准是什么就是给你五分钟讲解这道题的思路你能清清楚楚地说出这道题考察了哪个算法、边界在哪里、为什么这样设计状态、有哪些容易出错的细节。如果每条都能答上来才算真正过关。我在赛后复盘模拟赛题目时会把每道错题归类为知识缺失、思路错误、实现失误、读题偏差、时间不够。这样分类的好处是我可以很清楚地看到自己的短板到底集中在哪个环节然后针对性地调整训练方向。如果你连自己丢分的原因是哪一类都说不清楚那刷再多题也很难有大的进步。这套模拟赛的题目和题解可以反复做两遍。第一遍是开卷边看题解边理解第二遍是闭卷彻底不看题解独立完成。如果第二遍能在规定时间内顺利AC那对应的题型你已经掌握得非常扎实了。最后再分享一个省赛前一周的练习节奏以赛代练每天做一套完整模拟题严格限时做完之后用一天时间彻底复盘错题和优化实现方式最后两天只复习自己整理过的核心模板和常见易错点不要再碰新题了。保持手感的同时也要保证充足的休息上场时才能有最清醒的头脑。