如果你的洛谷主页收藏夹里躺着十几份题单,真正AC过的题目却一只手数得过来,那这篇内容就是为你准备的。我刷洛谷断断续续快六年了,从入门的红色题单一路啃到省选难度,中间踩过不少坑,也把几十份题单翻来覆去地筛过。今天想聊的“洛谷题单完整解析”,不是要宣称我能把题单里几百道题逐题讲一遍——题单本身在不断更新,任何人的“完整”都追不上版本变化——而是要把题单背后的知识体系、选题逻辑和刷题方法彻底拆开,让你拿到任何一份题单,都知道该怎么刷、刷什么、刷到什么程度算是真正吃透了。
1. 洛谷题单体系的完整地图:从红色到黑色的意义
1.1 官方题单和私人题单到底有什么区别
洛谷上的题单,本质上可以分为三大类。第一类是官方题单,由平台管理员维护,特点是成体系、按知识点和难度分层,比如“入门1顺序结构”“入门2分支结构”这种面向新手的系列,再往后是模拟、贪心、搜索、动态规划等专题模块。官方题单的好处是路线靠谱,不会东一榔头西一棒子,但缺点是有些题目年代比较老,数据或题面的小毛病偶尔会让人心态爆炸。
第二类是用户自建题单,来源五花八门:有省队选手整理的“XX专题四十题”,有考研党总结的数据结构刷题路线,也有刚入门的同学随手收藏的“我觉得这些题好难”合集。这类题单的优点是针对性极强,往往能从一个很小的切口带你深入某个算法;缺点则是质量参差不齐,作者可能只是把某次比赛的题按编号丢进去,没有任何递进关系。
第三类是题单广场里的“热单”和“精选单”,相当于社区投票出来的结果。热单通常因为题量适中、难度曲线合理、配套说明详细而受欢迎,对于不知道该从哪下手的新手来说,先挑一份评论数多、创建时间不太久远的题单作为主线,是比较稳妥的起步方式。
我的建议很简单:如果你刚开始刷题,别自己乱搭题单,也别一口气收藏五十份,选一份官方题单当骨架,再找一份讨论区口碑好的私人题单做补充,就足够了。贪多嚼不烂,题单是给你指路的,不是给你展览用的。
1.2 难度颜色、通过率和AC人数背后的信息
洛谷的题目有一套非常直观的难度标签系统,大概从红到黑一路加深。红色是入门题,考语法和模拟;橙色是普及-,开始涉及基础算法;黄色对应普及/提高-,是大多数新手的“舒适区上限”;绿色代表普及+/提高,需要一定的综合能力;蓝色是提高+/省选-,开始考验模型识别和思维深度;紫色就是省选/NOI档位;黑色则是NOI+/CTSC级别的硬骨头。
单独看颜色还远远不够,真正会看题的人还会关注三个数据:通过率、AC人数、题解数量。通过率低并不一定代表题目难,有时候是题目描述有歧义,或者是空数据、极限数据卡掉了大多数人。AC人数多说明题目经典、值得做,哪怕它是橙色黄色,也值得你花时间把细节抠干净。至于题解数量,基本代表了前人在你之前踩过多少坑,题解越多的题,你卡住时能参考的“前辈经验”就越丰富。
你可以把这三个指标当作一个“雷达图”:难度颜色决定这道题值不值得现在碰,AC人数决定它是不是经典,题解数量决定你卡住时有没有退路。三者都好的题,就该是题单里的核心题;三者都不好的,多半是冷门偏题,跳过也不心疼。
1.3 怎么判断一份题单值不值得刷
判断一份题单质量,我有一套自己的土办法。首先看目录结构:好的题单一定会细分专题,而不是把一百道题像倒豆子一样堆在一起。比如动态规划这个大类,至少要分出背包、线性DP、区间DP、树形DP、状态压缩、优化技巧等小节,每小节三五道题,由易到难排列。如果一份题单把背包题和树形DP题混在一个文件夹里,说明作者自己对知识结构都不清晰,跟着刷多半会乱套。
其次看题目之间的递进关系。优质题单的前几道题一定是你垫垫脚能够到的,中间有几道要卡一卡的题,最后才放一道压轴题挑战。这个设计的本质是给人“爬坡”的体验,而不是一开始就让你撞墙。如果你发现题单第三题就需要查题解才能过,大概率是难度曲线没做好,或者你的基础还没到位,换个题单会比死磕更高效。
最后看讨论区的反馈。题单页面的评论和讨论往往藏着宝藏:有人会指出某道题数据太弱建议忽略,有人会推荐某道题的替代题目,还有人会把整个题单的学习顺序重新整理一遍。花十分钟读评论,比闷头刷十道题更有价值。
2. 刷题单最大的坑:为什么收藏了300题还是没长进
2.1 只看题解不写代码的“假性勤奋”
我见过太多人刷题单的姿势是这样的:打开题单,点进一道题,读题三分钟,没有思路,于是点开题解区,从头到尾读一遍,觉得“哦原来这么简单”,然后心满意足地切到下一道题。几个小时下来刷了十道题,感觉很充实,但AC记录里依然只有那可怜的几个绿勾。
这种假性勤奋的根源在于把“看懂”当成了“会做”。看题解时你的大脑处于被动接收状态,不会真正经历状态定义时的纠结、转移方程推导时的试错、以及实现细节上的那些坑。等上了考场或者打比赛,你需要在没有题解的情况下独立完成整个思考链,平时没有练过的东西根本调用不出来。
我的建议是给每道题设置一个“独立思考底线”:至少给自己二十分钟,实在没有思路再允许看题解。看完题解之后不要立刻关掉页面,合上题解自己重写一遍代码,写不出来就再看,然后重写,直到能独立AC为止。第二天再做一遍这道题,看自己还能不能写出核心的状态转移方程。看起来很费时间,但效率远超“一小时十道题”式的浏览。
2.2 刷题顺序错了:畏难和舒适区
另一种常见的刷题单翻车姿势是难度取向走极端。有人只挑红题和橙题刷,AC率好看,但刷了三百题全是模拟和简单贪心,算法能力完全没进步,相当于天天在健身房里举轻哑铃,练不出肌肉。还有人一上来就挑战黑题,卡在某一题上两三天,挫败感拉满,最后连打开题单的勇气都没有了。
正确的做法是按难度梯度推进,每个专题保证黄、绿级别的题占一半以上。如果你连黄题都要想半天,那就老老实实把橙色题再刷一阵子;如果你绿题能稳定在一小时内独立AC,那蓝题就该进入你的常规训练了。关键原则是“最近发展区”——刷的题要比你当前水平高一层,但只高一层,而不是高一座山。
如果遇到一道题卡了两个小时还没有任何头绪,我的建议是先标记起来,跳到下一道,而不是死磕到底。很多题卡住的原因不是你笨,而是它用到的前置知识点你还没学过,或者题目本身设计得不够友好。把标记的题放到两三个星期后再回来刷,往往会有“当初怎么没想到”的顿悟感。
2.3 缺少限时训练:考场上崩盘的根源
刷题单是训练,不是阅读。如果你平时刷题从来不开计时器,一道题想多久都能想,那到考场上一定会面临时间管理灾难。算法竞赛最残酷的一点是:你明明会做这道题,但因为在某一步纠结太久,导致后面的题全都没时间看,最终得分凄惨。
我在刷题单时会给习题设两档时限:简单题四十分钟,中等题九十分钟,压轴题可以放宽到两个小时。如果到了时限还没AC,就直接翻题解复盘,绝不恋战。这种做法逼着你在平时就养成“先易后难、果断取舍”的习惯,到了考场上脑子里会有一种本能的节奏感:这道题十分钟没思路就跳,那道题写完暴力再回头优化。
考前两周我会强制自己每个周末做一场完整的模拟赛,把历年真题或者题单里的综合题拼成一套卷子,卡两个小时整,全程不碰题解、不中断、不与人讨论。模拟赛的成绩不重要,重要的是模拟真实考场的压力环境,让你在紧张状态下还能保持正常的做题节奏。
3. 动态规划题单的递进式吃法:以GT考试这道经典题为例
3.1 DP题单的正确学习路径:背包→线性→区间→树形→状压→优化
动态规划可能是洛谷题单里最庞大的一个板块,也是从普及组到省选都绕不开的核心知识点。很多新手一听到DP就头疼,本质原因是跳过了学习路径的递进顺序,直接去啃了最难的题。我梳理一下我自己认为比较合理的DP专题学习顺序,供大家参考。
先从背包问题起步,代表题是P1048采药和P1616疯狂的采药。背包题的价值在于让你理解DP最重要的两个东西:状态定义和枚举顺序。为什么01背包要倒序枚举容量,而完全背包要正序枚举?因为倒序能避免同一个物品被重复选取,正序则利用了“可以重复选”的特性。把这两个“为什么”用自己的话讲明白,你的背包就算入门了。
接下来是线性DP,代表题是P1216数字三角形和P1020导弹拦截。数字三角形看起来只是“从下往上取最大值”,但它其实是DAG上的最长路模型,是很多复杂DP的底子。导弹拦截则涉及最长不下降子序列和贪心,能帮你把“以某个位置为结尾”的状态定义方式彻底吃透。
然后是区间DP,代表题是P1880石子合并。区间DP的核心是“枚举区间长度,再枚举分割点”,这个套路非常固定,一旦掌握就能套用到大量类似题目上。树形DP的代表题是P1352没有上司的舞会,状态围绕“选根还是不选根”展开,关键是用DFS回溯来合并子树信息。状态压缩的代表题是P2704炮兵阵地,用二进制位表示一行的状态,提前预处理合法状态,再逐行转移。数位DP的代表题是P2657windy数,记忆化搜索框架加上limit和lead两个标志位,是一类非常独立的题型。
当这些基础题型都刷过一遍后,才轮到优化专题:前缀和优化、滚动数组、单调队列、矩阵快速幂。优化不是第一遍刷题时该学的东西,而是你已经被基础DP折磨过、对状态和转移足够熟悉之后,再来思考“如何让程序跑得更快”的进阶需求。
3.2 P3193 [HNOI2008]GT考试:KMP自动机与矩阵快速幂
P3193这道题,在我眼里是动态规划题单里最有代表性的“状态机计数”题目。题目让你求长度为n的数字字符串中,不包含某个指定长度为m的字符串的方案数,n可以给到很大的量级,m则相对小。如果你只学了基础DP,第一反应可能是容斥:用总方案数减去包含该串的方案数。但这个思路很快会遇到麻烦,因为“包含”这个事件会发生重叠,直接容斥非常痛苦。
更优雅的做法是把KMP的next数组升级成一个自动机。定义状态dp[i][j]表示已经构造了长度为i的字符串,当前字符串的后缀与禁止串前缀的匹配长度为j,且j小于m的方案数。每往末尾添加一个数字,当前匹配长度都会按照KMP的失配指针跳到一个新的位置;如果跳到了长度m,意味着构造出了一个包含禁止串的非法串。因为每个状态只和上一个状态发生线性转移,整个问题就变成了一个有限状态自动机上的计数问题,天然适配矩阵快速幂优化。
实现的时候有一个关键坑:不要把所有可能性放在同一个DP数组里硬推,而是先用KMP预处理出一个“状态转移表”。举个例子,假设当前匹配长度是j,你枚举下一个数字c(0到9),通过KMP的while循环不断回退失配指针,直到找到s[k]等于c或者k归零,那么新匹配长度就是k加一。把这套逻辑写成代码,预计算一个m乘10的go表,后面所有状态转移都是查表操作,速度和正确性都会非常稳。
预处理完转移表后,把dp数组每一次的线性转移构造成一个m乘m的矩阵,然后对这个矩阵做快速幂,把n压缩到log n的复杂度。m最大也就是二十几,矩阵乘法的常数开销很小,整体跑起来毫无压力。很多第一次接触这道题的人会被“KMP还能这么用”震撼到,其实本质就是把字符串匹配问题抽象成状态机问题,这在AC自动机、后缀自动机等进阶内容里还会反复出现。
3.3 从一道题反推整个专题:刷一道题要刷到什么程度
一道省选难度的DP题,可以同时拆出字符串匹配、状态机建模、矩阵快速幂三个知识点。题单的价值不在于让你背题,而在于每道题都教一种状态建模的方式。因此我在刷DP专题时有一个强制要求:每AC一道题,写一段不超过五十字的“状态定义一句话”,哪怕不写完整题解,也要把这道题的核心建模思路用自己的话记录下来。
我把这些“一句话”按类型归档,最后发现DP题单其实是一棵知识树:一类是线性递推,一类是区间合并,一类是树形依赖,一类是状态压缩枚举子集,还有一类是自动机构建转移。当你看到一个陌生题时,先问自己是哪棵树上的果实,再考虑套哪套模板、用哪种优化手段,思路就会清晰很多。
很多人刷题只追求AC,AC完就删掉临时文件,这是最大的浪费。一道题你花了两小时搞懂,只留下一行绿勾,那两小时就真的被浪费了;你花十分钟写一段心得,把模型、关键点、易错点记下来,这道题才真正进入了你的能力圈。
4. 普及组真题的三种解法对比:以直播获奖为例
4.1 值域桶加扫描:比赛中最稳的写法
P7072直播获奖是近年普及组一道很经典的真题:n位选手的成绩出场后,每位选手公布后都要输出当前获奖分数线的分数。成绩范围只有0到600,这给了我们一个非常暴力的机会——维护一个长度为601的计数数组,每个人分数进来就在对应桶里加一,然后从600向下累加,累加值第一次超过当前获奖人数时,那个分数就是当时的分数线。
这个写法的复杂度是O(600n),n最大值十万,算下来也就是六千万次左右的简单操作,C++完全不虚。代码长度只有十几行,没有堆、没有树状数组,比赛时最不容易写错。有人会觉得这个方法“不高大上”,但竞赛考的是稳定拿分,不是题解加分。数据范围允许时,越暴力的方案越值得优先考虑。
需要注意的细节是获奖人数w的计算:第i个人出现后,w等于max(1, i乘p除以100的整数部分)。这里p乘i做除法前最好先转成更大的整数类型,防止意外溢出。分数从600往下扫描时,注意循环结束条件——left减到小于等于0就break,输出当前s。
4.2 对顶堆方案:动态维护第k大的通用套路
当值域变大,比如分数从0到1e9,桶扫描就不再适用了,这时候需要更通用的数据结构方案。对顶堆是我很推荐的一种思路:维护两个堆,大根堆放“分数线以下”的数,小根堆放“分数线及以上”的数,并且让获奖人数恰好等于小根堆的size。
每来一个新分数x,先看小根堆是否满了:没满就直接塞进去;满了就比较x和小根堆堆顶,如果x比堆顶大,说明x应该进入获奖集合,那就把堆顶挤到大根堆,再把x放进小根堆;否则x进大根堆。当获奖人数w因为人数增长而变大时,从小根堆不够,就从大根堆把堆顶元素挪回来,直到小根堆size等于新的w。这样每次的分数线就是小根堆堆顶。
对顶堆的复杂度是O(n log n),代码量比桶扫描大一些,但思路非常通用,尤其适合那些需要动态求第k大的问题。这也解释了为什么同一个题单里的同一道题值得用不同方法反复做:代码本身不是目的,思维方式才是。
4.3 不同写法的适用场景与比赛取舍
我觉得把这三种方案放在一起对比特别有意思:值域桶+扫描适合值域可控的小数据,树状数组+二分适合需要频繁查询的场景,对顶堆适合值域无界且需要在线实时的动态第k大问题。很多人只记一种解法就上考场,其实这三种写法的选择标准很简单,就是看数据范围。
我个人的经验是:普及组阶段先把桶扫描写好,因为代码短、错误率低,足够应对大多数题;到了提高组阶段再练对顶堆,因为它能帮助建立“用堆维护序信息”的直觉,这在很多排序相关题目里都能用到。考场上的最优策略永远是“用你最熟练、最不容易出错的方案”,而不是“用复杂度看起来最优雅的方案”。
5. 老题和新题的取舍:P编号与B系列题单的刷法差异
5.1 P编号老题的价值:题解多、专题明确、稳健沉淀
洛谷的题目编号里,P开头是最常见的,题号越小,往往意味着入库时间越早。像1843这种老编号的题,你在官方题单里经常会遇到。老题的最大优势是经过多年沉淀,题解区里能找到好几个年代、好几种思路的版本:有暴力模拟的答案,有刚学算法的学生写的易懂版本,还有神犇用炫技优化写出的高难版本。这种“分层题解”是最佳学习材料,你能看到同一个问题不同技术水平的人分别怎么想。
老题因为命题年代早,很多是“裸算法”题:题目不会拐太多弯,主要考察某个单一知识点。刷这种题最大的价值就是帮你把某个算法的骨架彻底搞明白。做完一道老题后再去做近年的新题,你会发现那些综合题的复杂外表下,装着的还是这些老算法骨架。
不过我不建议把全部时间都献给老题。有些老题的数据和题面确实存在一些历史问题,比如歧义描述、边界数据缺失等。刷到这类题如果发现AC很费劲,先看看题解区讨论是不是题目本身有坑,别把锅全扣在自己头上。
5.2 B系列新题的价值与风险
近几年洛谷题库里出现了越来越多的B编号题目。它们大多来自新的比赛或用户投稿,题目风格更贴近当下命题趋势,综合性强,经常把两个甚至三个知识点揉在一道题里。像B3930这种新题,优点是很“新鲜”,能够逼你快速识别题目背后的模型,而不是靠背诵模板做题。
但新题的风险在于题解沉淀不足。一道刚上线没多久的题,可能只有零星几篇题解,质量也参差不齐,卡住时能参考的资料少。我的策略是:遇到这类题先收藏,正常尝试一轮,如果四十分钟还没有任何突破口,就标记“待回炉”,过一两个星期再回来看,那时题解区大概率已经有足够多的思路可供参考了。
学习算法的核心阶段需要的是确定性,明确的知识点配明确的例题,才能把基础夯实。新题更适合作为“阶段性检验”而不是“入门教材”,把新题穿插在题单的中后期,用来检查你从前面的旧题里提炼出来的模型是否真的能用,效果会好很多。
5.3 我的题单刷新策略:新旧结合
我现在刷题单的精力分配大概是七三开:七成刷经典老题,夯实算法骨架;三成刷新题和综合题,训练模型识别和应变能力。每周固定一个专题,从官方题单里选十五到二十题作为主干,再穿插两到三道B系列新题作为调味。
这样做的好处是,你既有稳定的知识增量,又有新鲜感的刺激,不会刷题刷到麻木,也不会被新题打击到自闭。如果你还在纠结“同一道题要不要刷第二遍”,我的建议是:经典题值得隔段时间重刷一遍,新题则只刷一遍,重点关注它有没有给你带来新的思维角度。
6. 把题单变成自己的知识库:错题本、模板与复盘方法
6.1 错题本不是抄题解,而是记录思维断点
我见过不少人的错题本,厚厚一摞,全是把题解抄了一遍,没有任何个人思考在里面。真正的错题本应该记录的是“思维断点”,也就是你在独立解题时,具体在哪一步卡住了。是被状态定义难住了,是转移方程写错了,还是某个边界条件漏判了?这些断点才是你刷题单最需要攻克的东西。
我建议的错题本格式很简单:一个表格,五列——日期、题号、算法标签、错误类型、一句话心得。每道题AC之后,花两分钟填一下,特别是“错误类型”那一列,一定要写具体,比如“KMP失配指针回退时越界”“区间DP忘了枚举长度”这种,而不是笼统地写“不会做”。
6.2 一题多解的价值:别人的题解不看等于白刷
同一道直播获奖题,桶扫描、树状数组、对顶堆是三套完全不同的思维方式。如果你只看了题解区第一篇帖子就关掉页面,你只收获了一个答案;如果你再往下翻两楼,看到一个和你思路完全不同但同样AC的方案,你的认知才会被打开。
我刷每道经典题都会至少看两三篇题解,重点不是比较谁写法更短,而是理解作者的建模方式和优化动机。看完之后我会自己写一段总结,写清楚哪种写法在什么场景下最优。这样做一次,比稀里糊涂地刷十道题都更值。
6.3 复刷机制:两周原则和限时重做
最后分享一个让题单真正内化的机制:两周复刷。每道在错题本里记录过的题,两周之后重做一遍,要求一次AC,否则继续记录、继续复刷。间隔太短会变成背答案,没有测试价值;间隔太久会变成重新学一遍,效率太低。两周是我测试下来比较舒服的遗忘曲线节点。
每次复刷时我会打开计时器,按考试规格限时,一道过不去的原题如果二十分钟内还是没有思路,就说明当初的“学会”并没有真正进入长期记忆,需要再做一遍。考试前一周,我只复刷错题本里的题和模板代码,不再碰新题,保持手感的同时也给自己一个稳定的心理预期。
刷题单这条路,说到底不是把别人铺好的路走到底,而是在走的过程中留下自己的脚印。现在我的习惯已经变了,不再追求“把题单清完”,而是追求“每道题都变成自己的东西”:状态定义能用一句话讲清楚,转移方程能默写出来,易错点能提前预判。做到这种程度,你手里拿的就不再是一份题单,而是一套属于自己的解题反射。