算法入门模拟赛题单拆解:七题训练与赛后复盘指南 1. 从一场四小时模拟赛说起这套题单到底在练什么如果你刷过一段时间算法题大概率会在某个阶段遇到一个瓶颈单题能写但一上整套题就手忙脚乱读题慢、切题顺序乱、简单题写挂、难题没时间看。我前段时间带几个刚入门的朋友做训练就专门挑了一套结构非常典型的入门级比赛题单来拆解也就是这套被很多人当作“新手第一套完整模拟”的题目组合。它一共七道题从最简单的模拟签到一路铺到需要一点构造思维和数据结构意识的压轴题难度曲线非常平滑特别适合用来做限时整套训练和赛后复盘。这套题单的核心价值不在于题目本身有多难而在于它把入门阶段最常考的几类能力一次性摆在了你面前字符串处理、简单数学推导、模拟实现、构造思维、前缀和/计数思想、以及带一点贪心味道的枚举优化。你如果能把这一套题从头到尾独立啃下来并且每道题都能说清楚“我为什么这么写、复杂度是多少、有没有更优解”那基本就跨过了入门到普及的第一道坎。这篇文章我会按照我自己带人训练时的真实节奏把这七道题从思路到实现、从踩坑到优化完整拆一遍中间穿插大量实际写代码时才会遇到的细节问题适合刚学完基础语法、想通过整套题提升实战能力的读者也适合已经能写简单题但总是“写得出却过不了”的朋友对照排查。我先把这套题的难度分布和考察点列个表方便你心里有个整体地图后面再逐题展开。题号难度定位核心考察点典型坑点A签到条件判断、字符统计读题漏条件B签到字符串去重计数重复字母处理C简单模拟数字逐位操作前导零、边界D中等字符串集合、子串枚举枚举顺序与去重E中等构造、对称性构造合法性验证F中等偏上计数、前缀思想下标映射G压轴枚举优化、数学推导复杂度控制这张表是我自己复盘时整理的你可以先扫一眼不用急着理解每一行等看完后面的详细拆解再回头对照会有更清晰的感觉。接下来我按题号顺序把每道题的思考路径、实现细节和容易翻车的地方一个个讲透。2. 前三题签到区的稳定拿分策略签到题看起来简单但恰恰是很多人整套训练里丢分最冤的地方。我带人训练时反复强调一个观点签到题的目标不是“写出来”而是“三分钟内写对且不返工”。这三道题就是用来练这个能力的。2.1 第一题条件判断的完整性第一题通常是纯条件判断题给你几个输入让你根据规则输出对应结果。这类题的坑几乎全部集中在“条件是否写全”上。我见过太多人因为漏掉一个边界条件样例过了但隐藏测试点挂了。我的习惯做法是读题时把题目里的所有条件用自然语言列成清单然后在代码里逐条对应写完再回头核对一遍清单。比如题目说“当满足条件甲时输出X否则如果满足条件乙输出Y其余情况输出Z”那你的代码结构就应该是严格的 if-else if-else而不是两个独立的 if否则条件重叠时就会出错。// 示意结构具体条件按题目替换 if (condA) { cout X; } else if (condB) { cout Y; } else { cout Z; }注意签到题最容易犯的错是把“否则如果”写成两个并列的 if导致同时满足两个条件时输出两次或者输出错误分支。写之前先把条件关系画清楚比写完再调试省时间得多。这道题我建议你限时三分钟从读题到提交一气呵成。如果超过五分钟还没过说明你的读题习惯需要调整而不是题目本身难。2.2 第二题字符串去重的两种写法第二题开始涉及字符串处理典型问法是“统计字符串里出现了多少种不同的字符”或者“判断两个字符串的字符集合是否相同”。这道题有两个层次的写法我建议你都掌握。第一种是直接用集合结构去重代码短、不易错适合比赛时快速拿分setchar s(str.begin(), str.end()); cout s.size();第二种是手动开一个计数数组用一个布尔数组标记每个字符是否出现过最后统计标记数量。这种写法在字符集很小的时候更快而且能顺便处理“每个字符出现几次”的变体问题bool seen[26] {false}; int cnt 0; for (char c : str) { int idx c - a; if (!seen[idx]) { seen[idx] true; cnt; } }我实测下来比赛时优先用第一种因为不容易写错但如果题目后面还要求输出每个字符的出现次数那第二种写法扩展性更好。这里的关键经验是同一道题准备两套写法根据后续需求灵活切换这是熟练度的体现。2.3 第三题逐位数字操作的边界处理第三题一般是给你一个数字让你对它的每一位做某种操作比如反转、替换、求和之类。这类题的核心难点不在算法而在边界处理前导零怎么办、数字是 0 怎么办、位数不够怎么办。我踩过最典型的坑是反转数字时把前导零也输出了。比如输入 1200反转后应该是 21 而不是 0021。处理办法有两种一是把数字转成字符串操作后再转回整数二是用数学方法反转时跳过前导零。字符串方法更直观string s to_string(n); reverse(s.begin(), s.end()); // 去掉前导零 int pos 0; while (pos 1 s.size() s[pos] 0) pos; cout s.substr(pos);提示处理数字逐位问题时先想清楚“这个数字会不会是 0”“会不会有前导零”“位数会不会超出预期”把这三个问题回答了代码基本就不会挂。这三道签到题加起来应该控制在十五分钟以内。如果你发现自己在一道签到题上卡了很久别急着往下做先把卡住的原因记下来赛后专门补这个知识点。整套训练的价值一半在限时做一半在赛后复盘。3. 第四题子串枚举与集合去重的组合拳从第四题开始难度有了明显提升这道题是我认为整套里最能拉开新手差距的一道。它的典型形式是给你若干个字符串问其中有多少个字符串是另一个字符串的子串或者类似的存在性判断。3.1 为什么直接双重循环会超时最直觉的写法是两层循环对每一对字符串判断包含关系。假设有 n 个字符串每个长度最多 m那么复杂度大约是 O(n² · m)在 n 达到几千的时候就会明显变慢。我第一次写这道题就是这么写的样例秒过但提交后在大数据上超时。问题的根源在于你把“判断包含”这个操作重复做了很多次而很多判断其实是可以提前排除的。比如长度更长的字符串不可能成为更短字符串的子串这个条件就能砍掉大量无效比较。3.2 用集合加速存在性判断更聪明的做法是把所有字符串放进一个集合里然后对每个字符串枚举它的所有子串看子串是否在集合中。这样复杂度变成 O(n · m²) 的枚举加上集合查询的对数开销实际跑起来快很多。setstring all(strs.begin(), strs.end()); int ans 0; for (const string s : strs) { bool found false; for (int i 0; i s.size() !found; i) { for (int j i 1; j s.size(); j) { string sub s.substr(i, j - i); if (sub ! s all.count(sub)) { found true; break; } } } if (found) ans; }这里有个细节要注意枚举子串时要把字符串自身排除掉否则每个字符串都会把自己算成自己的子串答案就错了。我见过有人在这里栽跟头样例刚好没覆盖这种情况结果隐藏测试点全挂。3.3 枚举顺序与提前终止上面代码里我用了!found作为循环条件这是提前终止的写法一旦找到一个合法子串后面的枚举就没必要继续了。这个优化在数据量大时效果很明显因为大部分字符串可能在前几个子串就命中了。还有一个容易被忽略的点是枚举顺序。从短子串开始枚举还是从长子串开始枚举命中率不一样。实测下来从短到长枚举通常更快命中因为短子串出现在集合里的概率更高。这个经验不是绝对的但值得你在写的时候留意一下。注意用substr会产生字符串拷贝如果字符串很长、枚举次数很多拷贝开销会累积。数据规模特别大的时候可以考虑用双指针加哈希的方式避免拷贝但对这套题的规模来说substr完全够用不必过度优化。这道题我建议的限时是二十分钟。如果你能在十五分钟内写出并通过说明你对字符串枚举和集合操作已经比较熟练了。4. 第五题构造题的思维训练第五题是一道构造题这类题的特点是答案不唯一你只需要构造出任意一个合法解。很多人第一次遇到构造题会懵因为平时做的题都是“求唯一答案”突然变成“随便给一个合法答案”反而不知道从哪下手。4.1 构造题的一般套路我总结构造题有三个常用切入点对称构造、贪心构造、以及从简单情况推广。这道题我采用的是对称构造的思路因为题目本身带有明显的对称性质。具体做法是先观察题目要求的条件找出哪些位置是“对称”的然后让对称位置取相同的值或者互补的值这样能自动满足一部分约束。比如题目要求某种配对关系那你就可以把配对的两个位置设成一样的剩下的位置再单独处理。// 对称构造示意 for (int i 0; i n / 2; i) { // 对称位置赋相同或互补的值 a[i] a[n - 1 - i] someValue; } // 处理中间位置n 为奇数时 if (n % 2 1) { a[n / 2] midValue; }4.2 构造合法性怎么验证构造题最怕的是“自己觉得对其实不合法”。我的习惯是写完构造逻辑后单独写一个验证函数把构造出来的结果代回题目条件逐条检查。这个验证函数不用提交只在本地测试时用但能帮你快速发现逻辑漏洞。bool check(vectorint a) { // 按题目条件逐条验证 // 返回是否合法 }本地跑几组小数据用暴力枚举所有可能答案对比如果构造结果始终合法那基本就稳了。这个“构造验证”的组合拳是我做构造题的标准流程强烈建议你也养成习惯。4.3 构造题的常见翻车点构造题翻车通常有两个原因一是边界情况没考虑比如 n 很小或者 n 为奇数时构造逻辑失效二是构造出的值超出题目范围比如题目要求值在某个区间内你构造时没注意。我的经验是先把 n1、n2、n3 这几个最小情况手动构造一遍确认逻辑成立再推广到一般情况。小情况能过大情况通常不会有大问题。提示构造题不要一上来就想着写通用代码先用纸笔把 n1 到 n5 的答案手动写出来观察规律规律出来了代码自然就出来了。这道题限时二十五分钟比较合理。构造题花时间是正常的但如果你超过四十分钟还没思路建议先跳过把后面的题看完再回来有时候后面的题会给你灵感。5. 第六题计数思想与前缀技巧第六题开始进入这套题的中上难度典型形式是给你一个序列问满足某种条件的三元组或二元组有多少个。这类题的核心是把 O(n³) 或 O(n²) 的暴力枚举优化到 O(n) 或 O(n log n)。5.1 从暴力到计数的思维转变暴力写法是三层循环枚举所有组合然后判断条件。n 小的时候能过n 大了必超时。优化的关键在于很多组合的判断条件是重复的可以提前统计。比如题目要求“三个数满足 abc 或者类似关系”那你就可以枚举其中两个第三个用哈希表或者计数数组直接查把一层循环变成 O(1) 查询。这个思路是计数类题目的通用套路。// 计数数组加速查询示意 vectorlong long cnt(maxVal 1, 0); for (int x : arr) cnt[x]; long long ans 0; for (int i 0; i n; i) { for (int j i 1; j n; j) { int need target - arr[i] - arr[j]; if (need 0 need maxVal) { ans cnt[need]; } } }5.2 下标映射的坑这道题我踩过的一个坑是下标映射错误。题目给的值可能很大但实际不同的值只有很少几种这时候需要做离散化或者用哈希表映射。映射的时候如果下标算错答案就会莫名其妙地偏大或偏小。我的排查方法是先用小数据手动算一遍正确答案然后打印中间统计结果逐个对比。如果统计结果和手动算的对不上问题一定出在映射或者计数逻辑上。5.3 复杂度估算的实际操作写计数题之前我习惯先估算一下复杂度。假设 n 是 2×10⁵那 O(n²) 就是 4×10¹⁰肯定超时O(n log n) 大约是 3×10⁶完全没问题。这个估算能帮你在写代码之前就确定方向避免写完才发现复杂度不对。n 的规模可接受的复杂度说明n ≤ 100O(n³)暴力可过n ≤ 5000O(n²)双重循环n ≤ 2×10⁵O(n log n)排序、二分、计数n ≤ 10⁶O(n)线性扫描这张表是我平时估算用的经验值实际会有浮动但大方向不会错。养成写代码前先看 n 的范围、再定复杂度的习惯能省下大量返工时间。这道题限时三十分钟。如果你能在二十五分钟内写出并通过说明你的计数思想已经比较扎实了。6. 第七题压轴题的枚举优化与数学推导最后一题是整套里最难的通常需要结合枚举和数学推导把看似需要高复杂度的解法优化到可接受范围。这道题我做了比较久中间走了弯路正好把踩坑过程完整讲一下。6.1 第一版思路为什么不行我第一版的思路是直接枚举所有可能的答案组合然后判断合法性。写完一算复杂度O(n²) 甚至 O(n³)n 稍微大一点就超时。这个思路的问题在于我把“枚举答案”和“验证答案”两个步骤都做成了暴力而实际上验证步骤可以用数学关系直接算出来不需要枚举。6.2 数学推导简化枚举重新分析题目条件后我发现可以把其中一个变量用另外两个变量表示出来这样枚举就从三维降到二维再配合一些单调性或者前缀统计就能把复杂度压到 O(n log n) 甚至 O(n)。推导过程是这样的假设题目要求满足某个等式 f(a, b, c) 0那我可以解出 c g(a, b)然后枚举 a 和 bc 直接算出来再判断 c 是否在合法范围内、是否满足额外约束。这一步把“枚举三个变量”变成了“枚举两个变量加一次计算”复杂度直接降一个量级。long long ans 0; for (int a 1; a n; a) { for (int b 1; b n; b) { long long c computeC(a, b); // 由等式推导出的 c if (c 1 c n valid(a, b, c)) { ans; } } }6.3 进一步优化利用单调性上面这个 O(n²) 的写法在 n 达到 10⁵ 时还是会超时。进一步优化的思路是观察 c 随 b 的变化是否单调如果单调就可以用双指针或者二分把内层循环优化掉。我实测下来这道题的 c 确实随 b 单调变化所以用双指针可以把复杂度降到 O(n)。这个优化不是一眼能看出来的需要你先写出 O(n²) 的版本观察数据规律再决定怎么优化。注意压轴题的优化往往是“先写出能过的暴力再观察规律优化”不要一上来就追求最优解。先把 O(n²) 写对拿到部分分再想办法优化这是比赛时的稳妥策略。6.4 压轴题的时间分配建议这道题我建议留四十分钟以上。如果前面六道题你用了七十分钟那还剩五十分钟给这道题时间是比较充裕的。如果前面超时了那这道题就先拿部分分把 O(n²) 的暴力写对提交能过多少测试点算多少不要死磕最优解。整套题的时间分配我整理成了一张表你可以参考题号建议用时累计用时A3 分钟3 分钟B5 分钟8 分钟C7 分钟15 分钟D20 分钟35 分钟E25 分钟60 分钟F30 分钟90 分钟G40 分钟130 分钟总共约两小时十分钟留出五十分钟缓冲和检查四小时的训练时间是够用的。这个分配不是死的你可以根据自己的强弱项调整但大原则是签到题快、中档题稳、压轴题留足时间。7. 赛后复盘比做题更重要的环节整套题做完真正的学习才刚刚开始。我带人训练时复盘花的时间往往比做题还多因为做题暴露问题复盘解决问题。7.1 复盘的正确姿势复盘不是把答案抄一遍而是回答三个问题第一这道题我卡在哪一步是读题、思路还是实现第二卡住的原因是什么是知识点缺失还是思维习惯问题第三下次遇到同类题我应该怎么调整。我自己的复盘习惯是给每道题打两个分思路分和实现分。思路分看的是“我有没有想出正确方向”实现分看的是“我想出来了但写对了吗”。两个分都低说明知识点要补思路分高实现分低说明代码熟练度不够思路分低实现分高说明思维训练要加强。7.2 常见问题速查表我把这套题训练中朋友们最常遇到的问题整理成了速查表你可以对照看看自己有没有中招现象可能原因解决方向样例过但提交挂边界条件没考虑补测 n1、n2、极值超时复杂度估算错误重新分析 n 的范围答案偏大重复计数检查去重逻辑答案偏小漏算情况检查条件完整性构造题不会没找对称性手动构造小情况找规律字符串题挂前导零/空串单独处理特殊输入这张表是我从多次训练中总结出来的覆盖了入门阶段百分之八十的翻车场景。你做完题对照一遍基本能定位自己的问题所在。7.3 我个人的几条实操心得第一条心得是限时训练一定要真的限时。很多人说限时实际上卡住了就去看题解这样训练效果大打折扣。我的做法是设一个闹钟时间到了不管做没做完都停然后按真实成绩复盘这样才能暴露真实水平。第二条心得是同一套题隔一周再做一遍。第一遍可能靠记忆做出来第二遍才是真实能力。如果第二遍还是卡在同一道题说明那个知识点你根本没掌握需要专门补。第三条心得是把每道题的复杂度写在代码注释里。这个习惯看起来多余但能逼你在写代码前就想清楚复杂度避免写完才发现超时。我坚持这个习惯之后超时的次数明显减少。提示复盘时不要只看自己做错的题做对的题也要看。有时候你做对了但用的是笨办法题解里有更优雅的写法这种“做对了但可以更好”的情况恰恰是提升的关键。这套题单我前后带人做了好几轮每一轮都能发现新的问题。入门阶段最怕的不是不会做而是不知道自己哪里不会。通过整套限时训练加系统复盘你能快速定位自己的短板然后有针对性地补。这比漫无目的地刷题效率高得多。