☰
天梯赛L2备赛复盘:从翻车题到稳定输出的实战策略
2026/10/3 3:13:46 网站建设 项目流程

3月3号是周日,我们队的固定训练日。我打开PTA平台翻到天梯赛题集的时候,页面上的倒计时提醒我,距离正式比赛已经只剩一个多月的窗口期。队里不少队友还在刷L1保底,但我心里清楚,天梯赛这种十人团队赛,L1属于人人能拿、拿了也拉不开差距的基础分,真正决定队伍排名的,是L2那一组进阶级题目能稳定做出几道。所以我给自己定的主题很直接:那一天先不管L1,用整个下午集中刷L2,把这个训练日当成正式比赛前的摸底考。

这篇文章就是当天训练的完整记录,包括我的选题思路、两道翻车题的详细复盘、训练中暴露出来的三个隐藏问题,以及后续一个月我打算怎么调整节奏。如果你也在备赛天梯赛,刷L2刷得有点茫然,或者已经过了L1但觉得L2每一题都像新题,那这篇应该有点参考价值。

1. 3月3日的训练配置与选题思路

1.1 为什么在3月这个节点死磕L2

天梯赛的题目要分三档看待:L1是入场券,考的是程序能不能编译、循环写得顺不顺、简单字符串处理会不会漏边界,这些题只要基础语法过关,练熟之后基本是肌肉记忆,不会给团队总分带来额外惊喜。L2才是真正的分水岭,它不考特别深的算法,但考得特别杂——链表的增删改查、二叉树的各种遍历、并查集合并、简单图论最短路、复杂模拟题的阅读能力,全都会出现在L2里。L3则是另一个层级的竞赛题,多数队伍能稳定AC一道就已经算是超额完成任务。

对我们这种中游队伍来说,比赛策略其实非常透明:L1争取全拿,L2尽量多拿,L3靠运气。由于L1缺少区分度、L3性价比太低,唯一值得下重注的就是L2。练L2等于同时训练读题速度、代码实现速度和常见数据结构的熟练度,投入产出比最高。3月3日那一天,我给自己定的及格线是五道L2,这个数字不是拍脑袋定的——我们队的校内积分赛就是按这个标准排位的,个人稳定做对五道左右,贡献就能排到中上;做三道以下基本属于拖后腿。

1.2 当天的做题清单与最终战果

把当天实际刷过的题和结果列成表格,复盘的时候会看得非常清楚。这个表格是我当时记在笔记本上的原始版本,包含题号、题型方向、当日结果和卡住的核心原因:

题号题型方向当日结果卡住的核心原因
L2-001 紧急救援图论最短路+路径还原未完成Dijkstra上叠加路径计数与最大点权和,逻辑容易乱
L2-002 链表去重链表模拟一次AC输出格式用%05d,之前踩过坑所以比较顺
L2-006 树的遍历递归建树+层序输出第二遍AC递归区间切分时左右子树边界差一位
L2-022 重排链表链表模拟一次AC链表长度奇偶判断容易漏,写之前先画了图
L2-029 小字辈树/记忆化搜索改了一版过直接递归会重复计算,需要记忆化
L2-034 口罩发放复杂模拟卡了一小时两个输出列表混在一起,排序规则搞反

这张表比当天AC了几道题更有价值。两个链表题都一次过了,说明之前专门练链表模拟没有白费;而树的遍历、小字辈、口罩发放这三道题都出现了反复,说明我的主要短板不在“会不会写”,而在边界处理、输出组织和复杂规则提取上。

1.3 限时模拟的环境设置

训练环境建议按比赛标准来搭,不然练出来的感觉是失真的。我的做法是:开一个两小时的倒计时,整个过程中间不切网页、不刷论坛、不看题解;编译器用自己最熟的C++17,不开网络搜模板;旁边放一张草稿纸,只记录三样东西——卡住的题号、卡住的原因、预估剩余时间。这样最后复盘的时候,能直接看出每道题的时间黑洞到底在哪里。

那天的限时跑下来,我的时间分配大致是:前40分钟做了L2-002和L2-022两道链表题,中间50分钟全部耗在L2-034上,剩下30分钟被迫在L2-006和L2-029之间抢救,最后L2-029勉强改出来。这个结构非常不健康。一道复杂模拟题最多只该占20分钟,一旦超过,就应该先跳过做后面的题,等第一轮扫完了再回来啃,我那时候明显缺少这种“止损”意识。

2. 翻车复盘一:L2-034口罩发放,排序线索是两条

2.1 这道题表面考模拟,实际考信息拆分

L2-034是往年天梯赛的真题,题面很长,信息量很大。表面上看它考的是模拟,实际上同时考了三个点:一是能不能从长题面里提取出真正的规则;二是能不能分清题目最后要求输出的两类名单;三是会不会处理身份证号相关的排序和比较。很多人一看到长题面就开始慌,其实拆开就一句话:有一批人反复提交申请,按某条规则判断谁有资格领取口罩,最后按输入顺序输出有资格的人;同时把你收到的所有发热人员单独记下来,最后按身份证号字典序输出。

难就难在“同时”。这两个输出有各自独立的顺序规则,但来自同一批输入。我当时的第一版代码,把符合领取条件的人和发热人员全部塞进了同一个结构里,然后在输出阶段才想着用条件过滤。结果就是顺序乱成一团:有资格名单要求“按提交顺序”,发热名单要求“按身份证号排序”,同一个容器里根本没法同时满足这两种规则,每次排序都会把另一边打乱。

2.2 一个vector装两条逻辑线的惨案

我第一版逻辑看起来每一个判断都有:每条记录输入之后,先判断是否发热,发热就打算塞进一个列表;再判断此人是否有资格购买,有资格也塞进同一个列表,最后统一排序输出。听起来好像都考虑了,问题出在哪里呢?出在排序规则互相污染:有资格购买的名单要求按提交顺序输出,发热名单要求按身份证号排序。如果你把两组人放在同一个vector里,不管你用什么排序键,都会破坏另一组的顺序。

而且还有一个更隐蔽的坑:同一个人可能在多条记录里反复出现。发热名单要不要去重、有资格购买的人能不能重复领取,都必须严格按题面来。我当时自作主张去重,结果反而少输出了一条,越改越懵,最后发现自己根本是在跟一个错误的数据结构较劲。

2.3 正确结构:为每个输出单独开一条管道

修通之后我发现,正确做法其实特别简单,关键是思路要先转过来:为两种输出分别建立独立容器,互不干扰。有资格购买的人用一个vector保存,记录顺序就是输入顺序,所以根本不需要排序;发热人员另用一个容器保存,最后单独按身份证号排序。如果确定身份证号不会重复,直接用map以身份证号为key存,输出时遍历map就是天然字典序;如果可能重复,就输出前打标记去重。

关键代码骨架大概是这个样子:

vector<Node> okList; // 有资格购买的人,保持输入顺序 set<string> hasGet; // 标记已经领取过的人 map<string, Node> feverMap; // 发热人员,按身份证号排序 // 按输入顺序逐条处理: // 1. 如果发热,写入 feverMap // 2. 如果提交时间在有效窗口内 且 未领取过,则 push 进 okList,同时在 hasGet 标记

这里的教训不是“题难”,而是我一开始没把输出当成独立的管道。比赛里这种结构非常常见:同一个输入,按不同规则生成多个输出,每个输出有自己的顺序。只要脑子里先想清楚“最终要产出几份名单、每份名单用什么顺序”,代码结构自然就清楚了。

2.4 辨识“双输出”题的通用信号

复盘时我总结出一个很实用的识别方法:看题目的末尾部分,如果它用“同时”“另外”“分别”这类词给出了两个或以上的输出要求,那在设计数据结构时就必须从一开始为每个输出单独准备容器。顺序型输出就用vector保持原序;字典序输出就用map或者最后统一排序。一边读数据一边塞进对应容器,最后各走各的输出逻辑,基本不会乱。

这个经验不只适用于口罩发放这一道题。天梯赛很多L2模拟题都爱在最后一两句安排多个输出要求,本质考的就是代码组织能力。我后来再遇到这种题,第一件事不是急着写输入输出框架,而是先数清楚题目要求输出几份东西。

3. 翻车复盘二:L2-006树的遍历,递归边界差一步错一路

3.1 中序+后序建树的本质:根在后序末尾,位置由中序决定

当天最后三十分钟,我碰到L2-006。这道题给出一棵二叉树的中序序列和后序序列,要求输出层序遍历结果。核心思路其实一句话就能讲完:后序序列的最后一个节点一定是整棵树的根,拿着这个根去中序序列里找到它的位置,左边就是左子树的中序区间,右边就是右子树的中序区间,然后递归往下建树。

我第一版代码的问题出在区间切分上,具体是后序序列的右子树起点算错了。很多人第一次写都会踩这个坑:左子树区间和后序区间的关系总是差一个下标。正确的写法,是每个递归函数同时接收当前子树的中序区间和后序区间,先用后序的最后一个元素确定根,再去中序里数出左子树的长度,最后用这个长度去切后序区间,而不是凭感觉平移。

我当时最后使用的核心函数长这样:

int build(int inL, int inR, int poL, int poR) { if (inL > inR) return -1; int root = post[poR]; // 后序最后一个节点是根 int pos = -1; for (int i = inL; i <= inR; i++) { if (in[i] == root) { pos = i; break; } } int leftLen = pos - inL; // 左子树节点个数 lc[root] = build(inL, pos - 1, poL, poL + leftLen - 1); rc[root] = build(pos + 1, inR, poL + leftLen, poR - 1); return root; }

注意:后序右子树的起点写成poL + leftLen,不要写成poL + leftLen + 1。根虽然在 poR 位置,但右子树区间并不需要为根预留额外偏移,这是这个模板里最容易错位的地方。

3.2 三个边界细节,每一个都让我白交一发

第一,空区间判断用中序的inL > inR,不要用后序去判断。因为后序区间在递归中可能因为切分方式产生一些看起来合法的区间,但中序区间才是真正决定子树范围的依据。第二,找根在中序里的位置时,每一次都要在当前子树的中序区间内找,不是在整个原数组里找。我第一次写的时候用全局数组下标直接定位,第二次递归进入子树时,位置严重错位,建出来的树完全是歪的。第三,节点编号在题里通常就是1到N,所以用数组存左右孩子最方便,不用写指针版二叉树,层序输出时从根开始用队列BFS即可。

层序输出本身只占这道题最后10%的工作量:根节点入队,弹出时依次把左右孩子入队,直到队列为空。真正要注意的只有分隔符——天梯赛这类题特别喜欢让你最后一个数字后面不留空格。统一做法是“第一个节点输出前不打空格,后面每个节点输出前加一个空格”,这比输出完再想办法删末尾空格要稳得多。

3.3 数据结构题练到“肌肉记忆”才算会

当时同队的朋友看了一眼我的代码,很疑惑地说:这题你之前不是见过吗?确实,“中序加后序建树”这个套路我记得,但真正动手时,每个递归边界上都要犹豫几秒,犹豫多了,就在这种题上耗掉了超过二十分钟。考场上没有那么多时间给你回忆模板。

后来我给自己定了一条规矩:任何一道L2数据结构题,如果不能在草稿纸上用三分钟写出核心框架,说明熟练度不够,回头必须复现到能背下来的程度。中序加后序建树,本质上和链表反转、并查集合并一样,属于必须形成肌肉记忆的代码骨架。天梯赛L2的树题考的就是你有没有把这套东西练到不用动脑。

4. 一次训练暴露的三个隐性短板

4.1 读题慢:样例能过不等于题目读对

那天我粗略估算,平均每道题读题用了5到8分钟。L2题面普遍偏长,尤其是模拟题,样例只是最浅层的信息。我读题有个不好的习惯:先看样例,再用样例反推规则,推不动才回头读题面。这个习惯对短题面有效,对L2这种长题面很危险,因为样例往往只覆盖最顺利的情况,隐藏规则全在题面的角落里。

反击发放题里,我在样例上表现良好,但自己构造的测试数据一深,就发现“已经领取过的人不能重复领取”这个隐藏条件没处理。正确的读题姿势应该是:先完整读一遍题,把规则逐条列成清单,再对着样例把清单里每一条验证一遍。把读题从“看故事”变成“提取规则”之后,模拟题的准确率明显上升。

4.2 输出陷阱:格式错误比逻辑错误更冤

天梯赛判题和传统OJ一样严格,多一个空格、少一个换行,都算格式错误。常见陷阱包括:行尾不能有空格但行内必须有分隔符;多个整数用单个空格分开;输出字符串时大小写敏感;日期、编号、身份证号这类长数字必须用字符串类型读入,否则前导零会丢。这些坑单独拎出来都不难,但组合在一起会大量消耗罚时。

我的应对办法是把常用输出模式写成小工具函数,比如“输出一个列表、题目要求末尾无空格”这种重复操作,写好模板之后所有题直接复用。长数字ID统一用string读入,必要的时候再转long long。这种做法看起来浪费,实际上能避开天梯赛大量藏在输出细节里的罚时。

4.3 容器选择的心智模型

当天另一道题L2-029小字辈,第一版递归超时,改成记忆化才过。这暴露出我对“树的高度、代数”这类问题缺少快速反应:只要题目涉及从叶子往根找、找祖先、找层级,就应该想到记忆化或者路径压缩,而不是每次都重新递归跑一整条链。

复盘的时候我把容器选择的心智模型也梳理了一遍:需要查重时先想set;需要按key配对、并且天然按key排序输出时想map;只需要保持输入顺序、最后线性遍历时想vector;需要“来过一次就不再处理”时想bool数组或set。L2的题基本离不开这几个容器。链表题更是这样,用结构体数组加地址串接模拟,比真的写指针链表快得多,也稳得多。我第一次写链表去重时用的是真指针,删除逻辑绕来绕去容易出错;后来改成结构体数组,一次就过了。天梯赛时间紧,数据结构的选型标准应该是简单可靠,而不是性能最优。

5. 下阶段练法调整:把L2刷成稳定输出

5.1 优先级排序:先练树、链表、并查集

3月3日训练结束后,我把正式比赛前最后一段时间的训练重点定了下来,优先级是这样排的:

  • 链表模拟:高。几乎每年都有,练熟就是送分,重点练结构体数组、地址串接、奇偶长度判断。
  • 树的遍历与重构:高。中序加后序、前序加中序、层序变体,都是固定套路,必须形成肌肉记忆。
  • 并查集:高。模板只有十几行,考的是合并逻辑和统计集合数量的附加操作。
  • 栈和队列应用:中高。主要和二叉树层序、表达式处理、括号匹配绑定出现。
  • 图论基础:中。最短路和DFS、BFS会涉及,但L2的图论题大多不难,重点是把模板打熟。
  • 复杂模拟:中。不考算法,考读题和代码组织,每天练一道保持手感。

按这个优先级,我给自己定的每日方案是:每天两道L2,一道数据结构题加一道模拟题;周末做完整的限时赛;每次练习后必须复盘,写清错误类型是读错题、边界漏、语法错还是数据结构选错,连续记录一周就能看到自己的错误分布。

5.2 卡题超过20分钟就切题

那天最大的时间浪费,就是L2-034卡了四十分钟还不肯放手。之后训练我给自己立了规矩:限时模拟中途,一道题卡超过20分钟,先标记为“第一轮未完成”,立刻切下一题。等把能拿的题都拿完之后再回来处理,而且回来之后先重读题面,而不是对着写了一半的代码继续改。

这个规矩不只是时间管理,也是心态管理。连续卡题会带来很强的挫败感,后面的题本来能做,也容易因为心态崩而丢分。及时切换题目,既保住基础分,又给大脑一个缓冲。回头看那道题时,经常因为重新读了题面,发现自己先前的理解本身就有偏差。

5.3 复盘是训练的一半

很多队友练题的方式是AC一道、开心一下,马上刷下一道。我练题的方式刚好相反:一道题AC之后,会强迫自己调出错误提交记录,对比正确版本,找出导致错误的那个具体点,然后写一行注释记录原因。错误代码是比AC代码更值钱的东西,因为AC代码只有一种,坑却有无数种。

这一个月练下来,我的直观感受是:L2刷题数量远没有“每道题是否总结”重要。盲目刷一百道题不总结,错误类型会反复出现;认真复盘二十道题,常见坑基本能全部踩完一遍。天梯赛的L2本质上考的是熟练度和细致度,这两样东西只能靠有针对性的刻意练习积累,而刻意练习的核心,就是每次训练后都回答一个问题:这一题我凭什么能过,下一题我凭什么保证还过。当时我在笔记本最后一页写着,如果每个周末都能像3月3日这样,把翻车的题吃透到能给别人讲清楚的程度,比赛时至少能多拿一两道L2的分。

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

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

立即咨询