☰
算法竞赛周赛复盘:从读题到数据结构选型的实战策略
2026/10/16 9:44:33 网站建设 项目流程

周六晚上,我把AWC 0017 Beta这轮周赛打完了,五道题最后AC了四道,E题连提交都没交上。说实话,这个成绩放在平时算不上好,但复盘之后我反而觉得这一场比完整AC五道还要值:A题吃过一次WA,D题中途推翻重写了一版,E题连模型都没来得及验证。踩过的坑越多,复盘笔记就越有参考价值。这篇Blog就写给同水平段的选手:我不会去复述官方题解的每一步,而是把A到E这五档难度对应的常见考点、每道题真正在考察什么能力、以及卡题时我实际会做的决策,尽我所能讲清楚。适合刚接触算法竞赛、每周打周赛想稳定AC三四题、但又经常死在读题不细和边界条件上的朋友。

1. 开赛之后的前十分钟:先通读题面,再决定进攻路线

我见过太多人,包括我自己前两年的状态,拿到题目立刻从A开始敲,敲完A看B,敲完B看C,一路按顺序推下去。这个习惯在题目难度严格递增的场次里没有大问题,但它浪费了比赛初期最宝贵的一段时间:你没法判断后面几题里有谁是白送分、谁是地狱难度。一旦在B或者C卡住,你的注意力会被拖死在已经投入的一道题上,完全不知道这道题到底值不值得继续。

现在我开赛的做法变了:先花五到八分钟把A到E的题面全部扫一遍,不要求看懂所有细节,只看三件事——输入数据范围、操作类型、以及题面里的关键词。所谓关键词,就是那些直接暗示算法的词:排序、前缀、最大最小、路径、连通块、方案数、取模。这些词能在动手前帮我给五道题各自的分量快速排个序,而不是懵着头写。

1.1 我以前的错误做法:从A到E按顺序硬推

这里具体说说顺序打法是怎么坑我的。有一场类似的周赛,我按顺序打到C题,结果它是个需要绕一下的图论建模题,我卡了四十分钟,心态直接崩了,后面D、E连题面都没好好看。赛后一复盘才发现,D题只是个排序加双指针的贪心,E题也谈不上做不出。如果我一开始就知道D题是弱题,大可以先绕过C,把能拿的分拿稳再回头啃C。顺序模式最麻烦的地方,是让你被前一道题的心理消耗拖着走,而不是根据全局难度合理分配精力。算法竞赛里时间比黄金贵,第一步当然是获取全局信息。

1.2 现在我在用的读题策略与时间分配表

扫完全部题面后,我会在草稿纸上按照自己的判断给五道题打一个优先级标记。这个优先级不是按A到E排,而是按“性价比”排:估计自己能不能做出来、大约需要多久、占用多少时间。以下是我给AWC 0017这种五题场次定的默认时间表,实测下来节奏很稳:

阶段目标耗时控制心态关键词
通读分类A-E全部扫一遍5-8分钟只标记,不深入
第一波A + B20分钟以内快速通过,错一发可接受
第二波C30-45分钟想清楚再写,避免重构
第三波D45-70分钟选好数据结构再动手
最后E剩余时间只求思路,不纠结AC

当然这只是参考,Beta场有时前两题偏简单,有时C题会成为全场毒瘤。核心思想是一旦预计某道题超过50分钟还没有稳定思路,就标记“暂停”,先去做后面的题,把这道题的逻辑放在大脑后台自己转。我做D题时经常遇到这种情况:强行写半天调不通,放一放再看,反而一眼看出关键条件。这个习惯看着很小,但救过我无数次。

2. A题和B题:签到层级的题目是怎么把人绊倒的

如果说整场比赛有哪两道的分最应该拿稳,那必然是A和B。算法难度上这两道几乎没有门槛,考的就是读题仔细度和代码基本功。但正因为简单,选手特别容易在“顺手就写完”的放松状态里翻车,WA一发还反应不过来为什么。

2.1 A题标配是简单数学与模拟,但输出格式处处是坑

A题通常就是给定一组整数或一个字符串,做一次简单运算或判断,比如最大最小值、累加和、奇偶性、字符出现次数。算法上没有任何唬人之处,真正的陷阱常藏在输出格式里。我翻过太多次车:“每个答案占一行”和“答案之间用空格分隔”就差一个词,结果完全不同。还有一次我在循环末尾输出了多余空格,本地样例全部通过,提交上去照样WA。

对于A题我的经验是:先把样例的输入输出格式看死在眼里,再谈算法思路。提交前用肉眼检查一遍输出语句,尤其是循环里换行和空格的写法。另外一个容易被忽略的点是A题也可能给到10^18量级的数据,我现在把所有参与计算的变量一律定义成long long,绝对不赌“这道题数值小”的运气。这种看似多余的谨慎,在正式比赛里救了我很多次。

2.2 B题最常见的两类失误:字符计数与区间边界

B题经常落在字符串或简单计数问题:统计某种字符出现次数、判断子串是否满足某个性质、或者在分类计数上做简单推导。常见翻车点有两个。第一个是字符计数数组开小了,只考虑了字母’a’-‘z’,没有加上大写字母或数字,某组样例直接数组越界。第二个是区间端点处理出错,写成左闭右闭或左闭右开时差一个索引,统计结果要么多算要么少算。

我的对策是固定两套约定:所有区间统一写成左闭右开[l, r),所有计数容器尽量用字典而不是定长数组。Python写dict,C++用map或者unordered_map,这样就算字符集超出预期也不会越界。约定固定之后,B题这类问题我很少再犯端点错误,因为只需要记住一种写法,不需要每道题现场重新推理边界。

2.3 一道典型签到题的完整调试思路(示例)

不搬原题面,这里用A/B档最常见的模型演示我的排查流程。假设题目是“给定字符串s,求出现次数最多的字符编号,如果次数相同取编号更小的”,看起来一句话写完,但仍有四个细节值得核对:第一,输入是否可能为空串;第二,大小写是否参与排序;第三,输出的是字符本身还是ASCII码;第四,统计循环有没有漏掉最后一个字符。

我的排查顺序是:先构造几个小边界样例,空串、单字符、全部相同、长度极大;再检查变量类型是否能够覆盖字符的ASCII范围;最后逐字对照输出要求。这个“签到题反而不急着写,先列一遍边界清单”的习惯,真的值得养成。它并不会让你多花两分钟,但能省下因为WA而浪费的十几分钟。

3. C题:在暴力能过与必须优化之间做判断

C题是周赛里真正开始有区分度的一道,但它的区分度通常不在算法多冷门,而在于你能否快速判断当前数据范围允许什么算法。很多人被C题卡住,不是因为题目难,而是因为他用了复杂度正确但常数极大的写法;或者反过来,拿着一个暴力就能过的题非要上高级优化,结果把自己绕晕。

3.1 先用数据范围估算复杂度,决定朴素做法可不可行

看到一道C题时,我第一件事永远是看数据上限。这里提供一个我常年使用的估算标准:1秒时限下,大约能稳定跑2×10^7次简单运算;1e6量级的数据可以用O(N),偶尔O(N log N);1e5量级用O(N log N)很稳,O(N^2)就有风险;2000量级允许O(N^2);如果只有20到30这个量级,大概率是O(2^N)枚举或状态压缩。拿这个标准一框,C题该用什么复杂度一目了然。

AWC 0017这场我其实犯过一个小错误:某道C题看到上限是2000,我下意识觉得肯定要优化,结果绕了一大圈才发现O(N^2)加少量剪枝就是正解,白白浪费二十分钟。后来我每次都会强迫自己先算复杂度再决定优化方案,绝不反向操作。这个原则看着简单,但在比赛高压下很容易被忽略。

3.2 连通块搜索模板:多数网格类C题的通用解法

C题最常见的载体是二维网格:判断连通块数量、最大连通块面积、最短步数。这类题的解法高度模板化,BFS和DFS都可行,我偏好BFS,因为迭代写法不容易爆栈,而且求“从起点到目标点最少几步”天然就是BFS的强项。这是我每次都会从模板库调出的网格BFS骨架:

from collections import deque def bfs_grid(grid, start): h, w = len(grid), len(grid[0]) seen = [[False] * w for _ in range(h)] q = deque([start]) seen[start[0]][start[1]] = True cnt = 0 while q: x, y = q.popleft() cnt += 1 for dx, dy in ((1,0), (-1,0), (0,1), (0,-1)): nx, ny = x + dx, y + dy if 0 <= nx < h and 0 <= ny < w and not seen[nx][ny] and grid[nx][ny] == '#': seen[nx][ny] = True q.append((nx, ny)) return cnt

很多人栽在同一个地方:忘记入队时就标记seen,导致同一个点被重复入队,轻则计数偏大,重则无限循环。BFS里有一条铁律——入队即标记,不要在出队时才标记。那个时刻已经晚了,因为别的方向可能已经把队里同一个点又加了一遍。这个模板只要改改格子条件和结束条件,就能覆盖大多数网格搜索类C题,非常值得背下来。

3.3 什么时候记忆化搜索比递推DP更省事

如果C题上升到动态规划,我发现不少选手会在状态定义上卡很久。这里有个取巧的方案:先用记忆化搜索把状态转移方程写出来,暂时不需要纠结遍历顺序。尤其是转移方向比较复杂、或者状态维度较多的时候,记忆化搜索在编写上的心智负担要小很多。Python里直接用缓存装饰器,C++用一个初始化为-1的二维数组就能实现同样的效果。

我个人的判断标准是:一个递归函数,只要返回值表达的是“从某个状态出发的最优结果”,并且参数能唯一确定这个状态,那么对它做缓存就不会错。等代码跑通之后,如果性能不达标,再改成递推也不迟。在C题阶段,先保证思路正确、再优化常数,这个顺序永远不会错。反过来先想着优化,很容易写出一堆自己都看不懂的转移逻辑。

4. D题:数据结构选型才是真正的分水岭

到了D题,大部分选手不再卡“怎么写”,而是卡“用什么写”。选对数据结构,一道D题四十分钟能过;选错,两个小时都可能调不出来。D题真正考验的是你对数据特征和操作类型的敏感度。

4.1 D题最常见的四类模型

根据我长期补题的经验,D题反复出现的骨架基本就四类。第一是区间问题,包括区间和、区间最值、区间加减;第二是集合关系,包括连通性、合并、分组;第三是动态维护,比如插入删除后查最值或第K大;第四是图论基础,包括最短路、拓扑序、最小生成树。你可以把D题理解成一个“业务场景,要求你用某种数据结构支撑高频操作”的题目。所以第一步永远是拆解操作类型:是单点改区间查,还是区间改单点取,还是插入删除后动态求最值。这几类操作对应的数据结构几乎是固定的,认不出来才是最大问题。

4.2 在线与离线:决定用堆、树状数组还是线段树的关键

我判断数据结构时还有一个更前置的问题:这个题目要求即时回答,还是允许把所有查询收集起来再统一处理。如果允许离线,很多题根本不需要线段树,排序加双指针,或者树状数组按顺序插入,代码量和常数都小很多。如果需要在线回答,则多半要上堆、有序集合或者线段树。

举一个最常见的例子:动态维护数组里的第K大。在线做法是两个堆或平衡树;但如果所有查询都在最后给出,就可以把所有数字读进来排序,然后二分答案,用前缀和验证,思路瞬间清爽。离线思维是D题里性价比最高的能力之一。很多选手一看到“查询第K大”就条件反射去写平衡树,其实离线条件下有更短更稳的路线。

4.3 线段树与树状数组选型对比

很多同学一看到区间题就写线段树,其实很多时候树状数组更合适。我根据自己的使用体验,把选择标准列在这里:

需求推荐结构理由
区间求和 + 单点修改树状数组代码短,常数小,实现快
区间最值 + 单点修改线段树树状数组维护最值比较绕
区间加减 + 区间求和线段树(懒标记)或差分差分只能离线,线段树支持在线
区间覆盖 + 动态查询线段树懒标记覆盖语义比累加更好控制

我的原则是“能树状数组就不线段树”。线段树写着写着容易在区间边界上翻车,尤其是左右索引的写法和习惯一旦不统一,调一个多小时的人不在少数。树状数组能覆盖的场景往往定义短、边界少,反而更适合比赛快节奏。这不是说线段树不重要,而是说不要在低需求场景里过度使用重型武器。

4.4 卡题时的自救:先写暴力,再找优化点

D题卡住的时候,我的第一反应不是去查神仙算法,而是先把一个O(N^2)的暴力写出来。这样做有三个作用:验证对题意的理解;拿到一个正确的基线答案;通过暴力代码观察瓶颈到底在哪里。很多时候写着写着就会发现,瓶颈集中在某一步的重复扫描,那么只要针对这一步做数据结构优化就够了。

我记得有一道区间众数相关的题,暴力写法提交超时,但盯着代码琢磨了一阵,发现每次查询根本不需要重新统计全区间,只需要维护当前使用的计数数组和当前最大值,于是改成双指针扫描,轻松通过。暴力不只是用来拿部分分的,它其实是D题通向正解的最佳脚手架。这个习惯我每个赛季都会反复推荐,因为它真的能救命。

5. E题:从题面特征反推考点模型

E题在五题场次里基本决定了你能不能上分。它不一定比D题难多少,但往往更冷、更有迷惑性。花三十分钟看出模型,这道题就是白送的大分;看不出来,可能到比赛结束也只能空在那里。

5.1 E题常见的五大考点方向

我复盘过不少套题,E题的出题方向相对集中在五个方向。第一是数论,包括最大公约数、逆元、组合数取模;第二是图论进阶,包括树形DP、基环树、缩点后的处理;第三是状态压缩DP;第四是贪心构造题,经常用到奇偶性、鸽巢原理、极值论证;第五是二分答案配合某种贪心验证。这五个方向有一个共同点:动手写代码之前,必须先做一轮数学性质的观察。E题几乎不存在“读题直接套模板”的情况,它考的就是你能否把题面的约束转化成某个已知模型的约束。

5.2 从数据范围和关键动词识别算法

识别E题模型时,我看三个信号。第一是数据范围:n不超过20的时候,基本往状态压缩枚举想;n在1e5左右并且题面出现“最大最小”,优先考虑二分答案;出现gcd、lcm、素数这些词,直接切数论思路。第二是操作类型:“每次可以翻转、选择、删除”往往指向DP或状态压缩;“求最少步数、能否达到”指向BFS或者构造;“请构造一组解”很多时候是贪心加归纳。第三是取模:只要有“对某个模数取模”出现,基本可以确定是一个组合计数DP,你需要立刻去找状态定义,而不是考虑模拟。

这套“题面特征到算法模型”的对应表,是我在每次复盘时都会专门记录的内容,越积越厚,识别速度也就越来越快。没有这个积累,光靠临场推理,E题很难在有限时间内拿下。

5.3 复盘E题时,我踩过的“想错方向”的坑

提到E题我想讲一个心态层面的教训。有次做一道E题,题面塞了一堆“最多”“最少”的条件,我直接奔着贪心去写,样例全过,结果提交WA了三次,最后还是放弃。补题看题解才发现那是一道二分答案,check函数里套了一层贪心。为什么我没想到?因为我在读题时把第一个念头当成了正确答案,后面所有推理都在为这个念头找理由,这就是典型的验证偏差。

现在我的习惯是:读完题先强迫自己列出至少两种候选思路,分别估算复杂度和可行性,再决定动手写哪个。这个小改变对E题正确率的提升非常明显。如果你也经常出现“样例能过但逻辑根本没有理顺”的情况,不妨也试试这个方法。多列一个候选方案,从来不会浪费太多时间。

6. 赛后复盘与题解笔记,才是涨分的主战场

打完比赛AC四道也好、只AC一道也好,真正决定下一次能进步多少的,是赛后接下来几个小时你在干什么。我见过太多人比赛一结束就把题目抛到脑后,下一场继续重复同样的错误。复盘这件事,应当被视为比赛的一部分,而不是可有可无的收尾工作。

6.1 赛后三个小时内要完成的三件事

比赛结束后,我会给自己开一个“三小时复盘窗口”,里面只做三件事。第一件,把每道题的最终状态和耗时列出来,包括WA了几次、TLE了几次,并且标出哪些是不该犯的低级错误。第二件,把卡住的题用自己的话重新复述一遍题意,不看原题面,看自己能不能写清核心约束。复述不出来的地方,就是理解漏洞所在。第三件,去查官方题解或其他选手的题解,但只看思路不看完整代码,然后自己重新写一遍AC。如果一道题看完题解还不能独立AC,说明问题比想象的大,需要回去补相关知识。

6.2 怎么把题解写成自己看得懂、能复用的笔记

很多人写题解就是把AC代码贴到博客里,说实话意义很小。我自己笔记的格式很固定:每道题记四行——考点标签、题意模型、关键结论或引理、我当时犯的错误思路。考点标签用来建立题目之间的联系,比如“二分答案+贪心验证”这道题和之前某道经典题共用同一个check函数,就在笔记里互相链接。关键结论单独写,因为那往往是别人一句话点醒你、但自己盯一天都想不到的地方。

我从来不照抄别人的代码。代码是逻辑的投影,只有自己重新写一遍,才真正把逻辑内部化。建立错题地图比追求题量更有用,这是我的核心看法。

6.3 我觉得值得长期维护的三类代码模板

最后说说模板库。我建议每个选手都维护三档模板:基础档,包括常用头文件、快速读入输出、整数二分、前缀和;结构档,包括BFS/DFS、并查集、树状数组、最短路径;进阶档,包括线段树懒标记、组合数取模预处理、状态压缩DP骨架。这三档不是用来背的,而是帮你在比赛里节省“从零开始敲”的时间。

更重要的是,每次补题如果发现某类模板的形态和当前解法不一致,要第一时间回头修改模板。我自己就是在反复修改模板的过程中,逐渐理解了为什么标准写法要那样写。主动维护模板,远比从网上下载一份“大全”更有用,因为只有经过自己验证的代码才在比赛时用得安心。

比赛打完,真正值得庆祝的不是AC数量,而是你手里又多了几张清楚的错题地图。AWC 0017 Beta这场我的AC率不算亮眼,但复盘之后得到的几个判断标准——先读全部题面、先算复杂度、先选数据结构、先列出候选思路——我会应用在接下来每一场周赛里。希望这份思路复盘也能帮你少走几段弯路,至少别再让A题和B题把状态搞崩了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询