2026年3月10号到12号,我在刷题平台上留下了一份记录。ID是hetao1733837,三天时间,13道题,11次通过,覆盖二分、双指针、动态规划、图论和几道模拟题。单独看这份记录,成绩不算特别亮眼,但把做题顺序和错题复盘拉出来,里面的信息量还挺大。这三天并不是临时起意刷着玩,而是一场有明确目标的集中训练:把几个一直模糊的知识点彻底搞清楚。如果你也在刷题、笔试前突击,或者准备面试中的算法环节,这份记录里提到的思路、模板和踩坑方式,可以拿来直接用。尤其是边界问题和状态定义这两个点,几乎每三道题就会遇上一次。下面按我的复盘顺序来写,不会按日期流水账,而是按知识点和问题来整理。这样比单纯贴AC记录更有参考价值。1. 三天刷题计划是怎么排出来的1.1 为什么是三天而不是一个晚上算法学习有一个“输入—练习—复盘”的循环。一个晚上刷题,往往刚进入状态就结束了,根本没时间重复。刷题一段时间的提升不是靠“做新题”,而是靠“隔一段时间回头再看错题”。三天正好能让这个循环发生两次:第一天学的,第二天晚上巩固,第三天上午再复习一遍。一天或两天的安排很难覆盖超过两个知识点。三天时间我可以安排三块不同的内容,同时在每个块内预留容错空间:某一类题当天没弄懂,晚上可以再找一个同类题强化。如果是临时突击,这种容错很重要。一旦卡住,不会把整个计划拖垮。很多刚开始刷题的人会给自己排“每天30题”的计划。实际上,高强度刷题之后,第二天脑子是蒙的,前一天的思路记不清,更别说总结。三天里我定的目标是每天4到5道有质量的题,外加一道随机巩固题。这样总量大概13到16道,属于中等偏上强度,但每道题都有时间思考、写代码和对着出错记录总结。1.2 按题型分类而不是按难度刷一个经常出现的误区:按平台的难度标签从简单到难挨个做。这种做法不是不行,但见效慢。原因是不同知识点混在一起,大脑还没来得及形成“这类题的套路”,又被下一个知识点打断。集中刷同类题型,可以在短时间内积累模式识别。这次的安排按知识块分:第一天是二分和双指针,第二天是动态规划,第三天是图论加复盘。每个知识块内部再按难度分层,先做基础模板题,再做需要变形的题目。比如二分,先复习标准的二分查找,接着做二分答案的题,最后做和双指针结合的题目。同一个主题下的不同场景反复出现,解题模板才能真正进入长期记忆,而不是看完解析之后“好像懂了”。选这三个主题是有原因的。二分和双指针考察的是区间思维;DP考察状态设计;图论考察建图能力和遍历策略。这三个方向覆盖了很多常见笔试中的高频点:前缀和、最值、连通性、最短路径。把它们放在三天里,每天都有新鲜感,又不会太散。1.3 每日节奏和时间块安排三天时间不是一个整天扎在电脑前。我按照工作时间表来模拟:上午9点到11点,第一轮刷题;下午2点到4点,第二轮刷题或补题;晚上8点到9点半,只做复盘和整理模板。每轮约40到50分钟,中间一定休息10分钟。长时间盯着屏幕容易让思路僵住,我试过连续两小时硬刚一道题,最后发现是题目理解错了,白白浪费一小时。这样安排还有一个原因:笔试或实际工作里,很少有人让你无限时思考。固定的时间块能训练做题时的紧迫感。如果一道题超过30分钟没有进展,我的处理方式是先写下当前思路和疑点,标记为“回头再看”,换一道题。这不是放弃,而是防止情绪消耗。回头再看的时候,经常一两分钟就能发现卡点。三天的记录里,有两道题都是第一天没AC,第二天补上的;它们并不是不会,而是当时进入了一个死胡同。2. 这三天重点啃的数据结构与模板2.1 二分答案:边界问题永远是第一道坎二分这个知识点,几乎所有刷题的人都能写出基本的while循环,但一遇到“最小化最大值”“最大化最小值”就蒙。原因在于,普通二分是对一个排好序的数组找一个目标值,而二分答案是对“答案可能的取值范围”做二分,再用一个check函数去判断取某个值时是否可行。我这次把二分答案的模板钉死成了下面这样:def check(mid): # 判断当前 mid 作为答案是否可行 # 可行返回 True,不可行返回 False return ... l, r = low, high while l < r: mid = (l + r + 1) // 2 if check(mid): l = mid # 当前 mid 可行,尝试更大 else: r = mid - 1 print(l)这个模板针对的是“可行域在右侧,要求最大值”的情况,所以mid取上中位数(l + r + 1) // 2。如果题目要求最小值,通常是这样:while l < r: mid = (l + r) // 2 if check(mid): r = mid else: l = mid + 1这两个模板看起来只差一个+1,但用错就会死循环。为什么会死循环?当l + 1 == r时,如果mid = (l + r) // 2,mid会等于l,某次check成功时执行l = mid,l根本没变,循环卡死。反过来,如果求最小值却用上中位数,可能让r偏移过大,结果错。所以每次写二分,第一件事就是确认:自己是在找左侧边界还是右侧边界,然后选择对应的mid取法。光背模板不够。二分答案的核心是check函数。做二分答案题,check往往比二分本身更难写:它决定了“可行”的判定复杂度。如果check是O(n),那整体复杂度是O(n log L),L是答案范围,通常可以接受。我第一次做某些题时,总想直接构造最优解,结果绕进贪心;其实先写一个暴力的check,再套二分,往往更稳。2.2 动态规划:状态定义比转移方程更值钱第二天的主要精力放在DP。总有一个问题会反复出现:转移方程到底怎么来的。实际上,如果状态定义得足够清楚,转移方程是可以推出来的。状态定义本身就是解题思路的核心。以01背包为例,代码很多人都会写:dp = [0] * (capacity + 1) for i in range(n): w, v = items[i] for c in range(capacity, w - 1, -1): dp[c] = max(dp[c], dp[c - w] + v)这里从大到小遍历容量,是为了保证每个物品只被选一次。如果从小到大遍历,dp[c - w]可能已经包含了当前物品,那就会变成“完全背包”。这个细节我第一天没理解透,第二天把两种背包放在一起对比,才彻底搞明白:空间压缩后,循环方向决定状态更新的“先后顺序”,于是决定了物品能否被重复使用。还有一个很容易忽略的点是初始化。求“恰好装满”和求“不超过容量”两种问法,初始化完全不同。求恰好装满时,一般把dp[0]设为0,其余设为负无穷;求不超过容量时,全初始化为0。我第一次做这类题时,题目要求“求刚好达到目标的最少物品数”,我把dp都初始化成了一个大数,漏掉了dp[0] = 0,结果一发WA,半天才发现。所以读题时先判断目标到底是在约束条件的边界上,还是只要在范围内即可。2.3 图论:建图方式决定了复杂度第三天是图论。图的存储方式,直接决定一道题能不能在时限内跑完。邻接矩阵适合顶点数很少的稠密图,O(N^2)的空间,适合N在1000以下的场景;邻接表适合大多数稀疏图,空间O(N+M)。我这次有一道题目N可以到10万,用邻接矩阵直接内存超限,换成邻接表就过了。所以不要急着想最短路径算法,先确认图的存储是否合理。BFS的模板可以用队列来实现,每个节点入队时标记vis,防止重复入队;Dijkstra用优先队列,核心是每次取出当前距离最小的点更新邻居。写Dijkstra时有一个很常见的坑:一个节点可能多次入队,如果只在出队时不判断,就会用旧的距离重复更新,导致复杂度退化。正确做法是在出队时判断if cur_dist != dist[v]: continue。还有一个容易被忽略的题目变形:把BFS的“步数”当成边权为1的最短路。很多迷宫题看着是BFS,其实考的是“离终点的最短步数”,那就把队里存坐标和一个步数字段就行。如果还能上下左右移动、遇到障碍物,也可以套BFS,不用真的去写Dijkstra。3. 实操记录与核心环节复盘3.1 Day 1:二分与双指针的踩坑实录第一天有4道题。上午第一道是经典的“在有序数组里找目标值”,本来以为很稳,结果提交后处理数组重复元素时出错。那道题要求返回第一个等于target的下标,用标准二分模板会找到任意一个等于target的位置,不一定是第一个。解决办法:找到target后,继续向左收缩右边界。这让我意识到,模板只能解决“是否存在/是否满足”,一旦要求“第一个”或“最后一个”,必须对模板做细微调整。下午做了一道“求最长连续子数组,使其和不超过K”的题。一开始我用了两重循环,复杂度O(N^2),N是10万,显然超时。换双指针之后,右指针不断右移,左指针就在和不满足时右移。这里的关键是,数组元素都是正数。如果数组里有负数,双指针就不成立了,因为和中途可能变小。这个“为什么双指针能做”的前提,比代码本身重要。如果题目没保证正数,就得上前缀和加二分或者线段树。晚上补了一道“合并区间”类型的题。这类题第一步永远是排序,按区间左端点排序。不排序,后面所有判断都是乱的。排序之后,扫描每个区间,维护当前合并的start和end:如果新区间左端点大于当前end,说明合并结束;否则更新end为两者较大值。代码很简单,但我之前总在边界判断上犹豫:是>=还是>。其实只要想清楚“在区间端点相接时是否算重叠”,根据题意选择即可。大多数题里[1, 2]和[2, 3]是重叠还是相接,题目会明说;不说的话按惯例可以合并。3.2 Day 2:DP状态压缩的一题两解第二天主攻DP,遇到一道很典型的“编辑距离”题:给定两个字符串,求最少操作次数,使它们相等,操作包括插入、删除、替换。一上来我写了二维DP:dp = [[0] * (m + 1) for _ in range(n + 1)] for i in range(n + 1): dp[i][0] = i for j in range(m + 1): dp[0][j] = j for i in range(1, n + 1): for j in range(1, m + 1): if s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] else: dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1状态含义是“s1前i个字符变成s2前j个字符的最小操作次数”。初始化好以后,转移方程几乎可以读出来:如果当前字符相等,直接继承斜对角的结果;如果不相等,从删除、插入、替换三种情况里取最小。这个题真正卡住的地方是怎么把空间从二维压到一维。因为转移时只用到了上一行的dp[i-1][j]和dp[i-1][j-1],以及当前行的dp[i][j-1]。用一个一维数组时,需要用一个变量提前保存dp[i-1][j-1],否则它会被覆盖。我第一版空间压缩代码没保存旧值,结果等于替换计算错了。这个坑让我印象深刻:空间优化不是单纯把二维降成一维,而是要分析每一轮更新时哪些“旧值”仍然需要。3.3 Day 3:图论补漏和模板稳定化第三天安排了5道题,重点把第二天没弄懂的BFS最短步数问题彻底过了一遍。一道迷宫题在N*M格子里找从起点到终点的最短路径,墙不能走。写BFS时,最开始的代码用了visited = [[False]*m for _ in range(n)],入队时标记。这个是对的,但我在第一版里写成了“出队时标记”,结果同一个节点被加入队列很多次,虽然结果没错,但队列大小增长很快,差点超时。还有一道求连通块数量的题。这道题其实只要对每个未访问节点调用DFS/BFS即可。DFS的递归写法看起来方便,但N很大时有递归深度风险。我这次特意用迭代栈写,避免爆栈。深度优先不一定只能用递归,用stack = [start]循环pop,效果完全一样。第三天的统计表格如下:日期主要类型题数AC数核心问题3月10日二分、双指针43二分的左边界处理3月11日动态规划43DP空间压缩时的状态覆盖3月12日图论、补漏55BFS入队标记时机这个表格不是拿来晒成绩,而是让复盘有依据:第二天少AC的那一题,第三天其实变成了图论里“状态标记”的提醒。不同知识点之间是有关联的。4. 刷题过程的常见问题与排查技巧实录4.1 样例过但提交全 WA 的排查顺序样例过了提交全错,是刷题最挫败的经历。遇到这种情况,我有一个固定的排查顺序。第一,看题目给的输入范围,测一个最小规模的数据,比如n=0或n=1。很多边界条件在样例里不会出现,一旦n=0,数组访问就越界或循环不执行,结果是0,但正确答案可能是特殊值。第二,检查重复元素,二分和排序相关的题尤其容易在这里翻车。第三,检查整数溢出,C++里用int还是long long,Python没有这个问题,但Java需要小心。第四,重新读题,看自己是不是把“升序”“降序”“可以重复”“最小”这些限定词看漏了。我有一次WA到怀疑人生,后来发现题目要求输出排序后的下标,而不是值。这种“输出目标理解错了”的失误,样例甚至会误导你,因为样例恰好没区分。所以刷题记录里,我习惯在每道题旁边写一句“题目的输出要求”,比只贴代码有用。4.2 TLE 的定位方法提交超时,先别急着优化细节。正确的第一步是估算复杂度。如果N是10万,O(N^2)一般就危险了,O(N log N)通常可以。如果算法复杂度本身没问题,再去看有没有多余操作,比如在循环里调用了一个本身是O(N)的函数,导致整体变O(N^2)却不自知。我曾经在一个循环里写if x in list,那个list长度是10万,循环也是10万,实测跑起来非常慢。换成set之后,同样的逻辑瞬间通过。这个“隐性复杂度”比显式的两重循环更难发现,因为代码看起来很整齐。排查TLE时的做法是,给几个大数据规模的测试,分段打印耗时,缩小到具体是哪个函数慢;线上笔试虽然没有本机测试数据,但在本地可以自己构造最大数据,先跑一遍。输入输出加速也很重要,尤其是在行数很多的题目里。Python的sys.stdin.read()比input()快很多,C++可以用ios::sync_with_stdio(false)。这些不算算法优化,但关键时刻确实能救命。4.3 三天刷题的心态管理三天连刷,最难的不是题目,而是第三天的疲倦感。第一天还觉得新鲜,第二天开始怀疑自己的脑子,第三天则是既想结束又怕没收获。我这次特意在第三天安排了图论题,因为图论题建模过程有趣,能调动积极性。如果发现连续两道题都没思路,那就停下来,不再开新题。去整理之前写过的模板,哪怕是抄一遍到笔记里,也比硬着头皮看好评解析有效。整理模板本身就是复习。另一个方法是换一道类型完全不同的简单题,找回“我能过题”的掌控感,再回到难题上。刷题记录里的数据不用太在意,错的题才是重点;三天结束之后,我复盘最多的是那两道没AC的题,它们比9道AC题提供了更多信息。注意:如果一道题超过30分钟没有任何进展,先把思路和疑点记下来,然后换题。情绪消耗比脑力消耗更容易让你提前放弃。三天刷题的记录到这里基本复盘完了。我个人实际操作中的体会是,刷题到一定数量之后,拼的已经不是代码量,而是对模板的理解深度和排除bug的熟练度。这次三天13道题,AC11道,看似只是一个普通记录,但真正有价值的,是每一道错题对应的那个具体细节:二分边界、DP初始化、BFS入队标记、空间压缩时的旧值保存。如果你也想试试,建议也按“知识点分组+固定时间块+当天复盘”的方式安排,哪怕只做三天,回头再看的时候,你会发现自己对算法的理解比题数更扎实。最后再分享一个小技巧:每天结束后,把当天所有出错点写成一句话,放到手机备忘录里,隔两天翻一遍,比考前突击笔记有用得多。
2026年3月10号到12号,我在刷题平台上留下了一份记录。ID是hetao1733837,三天时间,13道题,11次通过,覆盖二分、双指针、动态规划、图论和几道模拟题。单独看这份记录,成绩不算特别亮眼,但把做题顺序和错题复盘拉出来,里面的信息量还挺大。这三天并不是临时起意刷着玩,而是一场有明确目标的集中训练:把几个一直模糊的知识点彻底搞清楚。如果你也在刷题、笔试前突击,或者准备面试中的算法环节,这份记录里提到的思路、模板和踩坑方式,可以拿来直接用。尤其是边界问题和状态定义这两个点,几乎每三道题就会遇上一次。下面按我的复盘顺序来写,不会按日期流水账,而是按知识点和问题来整理。这样比单纯贴AC记录更有参考价值。1. 三天刷题计划是怎么排出来的1.1 为什么是三天而不是一个晚上算法学习有一个“输入—练习—复盘”的循环。一个晚上刷题,往往刚进入状态就结束了,根本没时间重复。刷题一段时间的提升不是靠“做新题”,而是靠“隔一段时间回头再看错题”。三天正好能让这个循环发生两次:第一天学的,第二天晚上巩固,第三天上午再复习一遍。一天或两天的安排很难覆盖超过两个知识点。三天时间我可以安排三块不同的内容,同时在每个块内预留容错空间:某一类题当天没弄懂,晚上可以再找一个同类题强化。如果是临时突击,这种容错很重要。一旦卡住,不会把整个计划拖垮。很多刚开始刷题的人会给自己排“每天30题”的计划。实际上,高强度刷题之后,第二天脑子是蒙的,前一天的思路记不清,更别说总结。三天里我定的目标是每天4到5道有质量的题,外加一道随机巩固题。这样总量大概13到16道,属于中等偏上强度,但每道题都有时间思考、写代码和对着出错记录总结。1.2 按题型分类而不是按难度刷一个经常出现的误区:按平台的难度标签从简单到难挨个做。这种做法不是不行,但见效慢。原因是不同知识点混在一起,大脑还没来得及形成“这类题的套路”,又被下一个知识点打断。集中刷同类题型,可以在短时间内积累模式识别。这次的安排按知识块分:第一天是二分和双指针,第二天是动态规划,第三天是图论加复盘。每个知识块内部再按难度分层,先做基础模板题,再做需要变形的题目。比如二分,先复习标准的二分查找,接着做二分答案的题,最后做和双指针结合的题目。同一个主题下的不同场景反复出现,解题模板才能真正进入长期记忆,而不是看完解析之后“好像懂了”。选这三个主题是有原因的。二分和双指针考察的是区间思维;DP考察状态设计;图论考察建图能力和遍历策略。这三个方向覆盖了很多常见笔试中的高频点:前缀和、最值、连通性、最短路径。把它们放在三天里,每天都有新鲜感,又不会太散。1.3 每日节奏和时间块安排三天时间不是一个整天扎在电脑前。我按照工作时间表来模拟:上午9点到11点,第一轮刷题;下午2点到4点,第二轮刷题或补题;晚上8点到9点半,只做复盘和整理模板。每轮约40到50分钟,中间一定休息10分钟。长时间盯着屏幕容易让思路僵住,我试过连续两小时硬刚一道题,最后发现是题目理解错了,白白浪费一小时。这样安排还有一个原因:笔试或实际工作里,很少有人让你无限时思考。固定的时间块能训练做题时的紧迫感。如果一道题超过30分钟没有进展,我的处理方式是先写下当前思路和疑点,标记为“回头再看”,换一道题。这不是放弃,而是防止情绪消耗。回头再看的时候,经常一两分钟就能发现卡点。三天的记录里,有两道题都是第一天没AC,第二天补上的;它们并不是不会,而是当时进入了一个死胡同。2. 这三天重点啃的数据结构与模板2.1 二分答案:边界问题永远是第一道坎二分这个知识点,几乎所有刷题的人都能写出基本的while循环,但一遇到“最小化最大值”“最大化最小值”就蒙。原因在于,普通二分是对一个排好序的数组找一个目标值,而二分答案是对“答案可能的取值范围”做二分,再用一个check函数去判断取某个值时是否可行。我这次把二分答案的模板钉死成了下面这样:def check(mid): # 判断当前 mid 作为答案是否可行 # 可行返回 True,不可行返回 False return ... l, r = low, high while l < r: mid = (l + r + 1) // 2 if check(mid): l = mid # 当前 mid 可行,尝试更大 else: r = mid - 1 print(l)这个模板针对的是“可行域在右侧,要求最大值”的情况,所以mid取上中位数(l + r + 1) // 2。如果题目要求最小值,通常是这样:while l < r: mid = (l + r) // 2 if check(mid): r = mid else: l = mid + 1这两个模板看起来只差一个+1,但用错就会死循环。为什么会死循环?当l + 1 == r时,如果mid = (l + r) // 2,mid会等于l,某次check成功时执行l = mid,l根本没变,循环卡死。反过来,如果求最小值却用上中位数,可能让r偏移过大,结果错。所以每次写二分,第一件事就是确认:自己是在找左侧边界还是右侧边界,然后选择对应的mid取法。光背模板不够。二分答案的核心是check函数。做二分答案题,check往往比二分本身更难写:它决定了“可行”的判定复杂度。如果check是O(n),那整体复杂度是O(n log L),L是答案范围,通常可以接受。我第一次做某些题时,总想直接构造最优解,结果绕进贪心;其实先写一个暴力的check,再套二分,往往更稳。2.2 动态规划:状态定义比转移方程更值钱第二天的主要精力放在DP。总有一个问题会反复出现:转移方程到底怎么来的。实际上,如果状态定义得足够清楚,转移方程是可以推出来的。状态定义本身就是解题思路的核心。以01背包为例,代码很多人都会写:dp = [0] * (capacity + 1) for i in range(n): w, v = items[i] for c in range(capacity, w - 1, -1): dp[c] = max(dp[c], dp[c - w] + v)这里从大到小遍历容量,是为了保证每个物品只被选一次。如果从小到大遍历,dp[c - w]可能已经包含了当前物品,那就会变成“完全背包”。这个细节我第一天没理解透,第二天把两种背包放在一起对比,才彻底搞明白:空间压缩后,循环方向决定状态更新的“先后顺序”,于是决定了物品能否被重复使用。还有一个很容易忽略的点是初始化。求“恰好装满”和求“不超过容量”两种问法,初始化完全不同。求恰好装满时,一般把dp[0]设为0,其余设为负无穷;求不超过容量时,全初始化为0。我第一次做这类题时,题目要求“求刚好达到目标的最少物品数”,我把dp都初始化成了一个大数,漏掉了dp[0] = 0,结果一发WA,半天才发现。所以读题时先判断目标到底是在约束条件的边界上,还是只要在范围内即可。2.3 图论:建图方式决定了复杂度第三天是图论。图的存储方式,直接决定一道题能不能在时限内跑完。邻接矩阵适合顶点数很少的稠密图,O(N^2)的空间,适合N在1000以下的场景;邻接表适合大多数稀疏图,空间O(N+M)。我这次有一道题目N可以到10万,用邻接矩阵直接内存超限,换成邻接表就过了。所以不要急着想最短路径算法,先确认图的存储是否合理。BFS的模板可以用队列来实现,每个节点入队时标记vis,防止重复入队;Dijkstra用优先队列,核心是每次取出当前距离最小的点更新邻居。写Dijkstra时有一个很常见的坑:一个节点可能多次入队,如果只在出队时不判断,就会用旧的距离重复更新,导致复杂度退化。正确做法是在出队时判断if cur_dist != dist[v]: continue。还有一个容易被忽略的题目变形:把BFS的“步数”当成边权为1的最短路。很多迷宫题看着是BFS,其实考的是“离终点的最短步数”,那就把队里存坐标和一个步数字段就行。如果还能上下左右移动、遇到障碍物,也可以套BFS,不用真的去写Dijkstra。3. 实操记录与核心环节复盘3.1 Day 1:二分与双指针的踩坑实录第一天有4道题。上午第一道是经典的“在有序数组里找目标值”,本来以为很稳,结果提交后处理数组重复元素时出错。那道题要求返回第一个等于target的下标,用标准二分模板会找到任意一个等于target的位置,不一定是第一个。解决办法:找到target后,继续向左收缩右边界。这让我意识到,模板只能解决“是否存在/是否满足”,一旦要求“第一个”或“最后一个”,必须对模板做细微调整。下午做了一道“求最长连续子数组,使其和不超过K”的题。一开始我用了两重循环,复杂度O(N^2),N是10万,显然超时。换双指针之后,右指针不断右移,左指针就在和不满足时右移。这里的关键是,数组元素都是正数。如果数组里有负数,双指针就不成立了,因为和中途可能变小。这个“为什么双指针能做”的前提,比代码本身重要。如果题目没保证正数,就得上前缀和加二分或者线段树。晚上补了一道“合并区间”类型的题。这类题第一步永远是排序,按区间左端点排序。不排序,后面所有判断都是乱的。排序之后,扫描每个区间,维护当前合并的start和end:如果新区间左端点大于当前end,说明合并结束;否则更新end为两者较大值。代码很简单,但我之前总在边界判断上犹豫:是>=还是>。其实只要想清楚“在区间端点相接时是否算重叠”,根据题意选择即可。大多数题里[1, 2]和[2, 3]是重叠还是相接,题目会明说;不说的话按惯例可以合并。3.2 Day 2:DP状态压缩的一题两解第二天主攻DP,遇到一道很典型的“编辑距离”题:给定两个字符串,求最少操作次数,使它们相等,操作包括插入、删除、替换。一上来我写了二维DP:dp = [[0] * (m + 1) for _ in range(n + 1)] for i in range(n + 1): dp[i][0] = i for j in range(m + 1): dp[0][j] = j for i in range(1, n + 1): for j in range(1, m + 1): if s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] else: dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1状态含义是“s1前i个字符变成s2前j个字符的最小操作次数”。初始化好以后,转移方程几乎可以读出来:如果当前字符相等,直接继承斜对角的结果;如果不相等,从删除、插入、替换三种情况里取最小。这个题真正卡住的地方是怎么把空间从二维压到一维。因为转移时只用到了上一行的dp[i-1][j]和dp[i-1][j-1],以及当前行的dp[i][j-1]。用一个一维数组时,需要用一个变量提前保存dp[i-1][j-1],否则它会被覆盖。我第一版空间压缩代码没保存旧值,结果等于替换计算错了。这个坑让我印象深刻:空间优化不是单纯把二维降成一维,而是要分析每一轮更新时哪些“旧值”仍然需要。3.3 Day 3:图论补漏和模板稳定化第三天安排了5道题,重点把第二天没弄懂的BFS最短步数问题彻底过了一遍。一道迷宫题在N*M格子里找从起点到终点的最短路径,墙不能走。写BFS时,最开始的代码用了visited = [[False]*m for _ in range(n)],入队时标记。这个是对的,但我在第一版里写成了“出队时标记”,结果同一个节点被加入队列很多次,虽然结果没错,但队列大小增长很快,差点超时。还有一道求连通块数量的题。这道题其实只要对每个未访问节点调用DFS/BFS即可。DFS的递归写法看起来方便,但N很大时有递归深度风险。我这次特意用迭代栈写,避免爆栈。深度优先不一定只能用递归,用stack = [start]循环pop,效果完全一样。第三天的统计表格如下:日期主要类型题数AC数核心问题3月10日二分、双指针43二分的左边界处理3月11日动态规划43DP空间压缩时的状态覆盖3月12日图论、补漏55BFS入队标记时机这个表格不是拿来晒成绩,而是让复盘有依据:第二天少AC的那一题,第三天其实变成了图论里“状态标记”的提醒。不同知识点之间是有关联的。4. 刷题过程的常见问题与排查技巧实录4.1 样例过但提交全 WA 的排查顺序样例过了提交全错,是刷题最挫败的经历。遇到这种情况,我有一个固定的排查顺序。第一,看题目给的输入范围,测一个最小规模的数据,比如n=0或n=1。很多边界条件在样例里不会出现,一旦n=0,数组访问就越界或循环不执行,结果是0,但正确答案可能是特殊值。第二,检查重复元素,二分和排序相关的题尤其容易在这里翻车。第三,检查整数溢出,C++里用int还是long long,Python没有这个问题,但Java需要小心。第四,重新读题,看自己是不是把“升序”“降序”“可以重复”“最小”这些限定词看漏了。我有一次WA到怀疑人生,后来发现题目要求输出排序后的下标,而不是值。这种“输出目标理解错了”的失误,样例甚至会误导你,因为样例恰好没区分。所以刷题记录里,我习惯在每道题旁边写一句“题目的输出要求”,比只贴代码有用。4.2 TLE 的定位方法提交超时,先别急着优化细节。正确的第一步是估算复杂度。如果N是10万,O(N^2)一般就危险了,O(N log N)通常可以。如果算法复杂度本身没问题,再去看有没有多余操作,比如在循环里调用了一个本身是O(N)的函数,导致整体变O(N^2)却不自知。我曾经在一个循环里写if x in list,那个list长度是10万,循环也是10万,实测跑起来非常慢。换成set之后,同样的逻辑瞬间通过。这个“隐性复杂度”比显式的两重循环更难发现,因为代码看起来很整齐。排查TLE时的做法是,给几个大数据规模的测试,分段打印耗时,缩小到具体是哪个函数慢;线上笔试虽然没有本机测试数据,但在本地可以自己构造最大数据,先跑一遍。输入输出加速也很重要,尤其是在行数很多的题目里。Python的sys.stdin.read()比input()快很多,C++可以用ios::sync_with_stdio(false)。这些不算算法优化,但关键时刻确实能救命。4.3 三天刷题的心态管理三天连刷,最难的不是题目,而是第三天的疲倦感。第一天还觉得新鲜,第二天开始怀疑自己的脑子,第三天则是既想结束又怕没收获。我这次特意在第三天安排了图论题,因为图论题建模过程有趣,能调动积极性。如果发现连续两道题都没思路,那就停下来,不再开新题。去整理之前写过的模板,哪怕是抄一遍到笔记里,也比硬着头皮看好评解析有效。整理模板本身就是复习。另一个方法是换一道类型完全不同的简单题,找回“我能过题”的掌控感,再回到难题上。刷题记录里的数据不用太在意,错的题才是重点;三天结束之后,我复盘最多的是那两道没AC的题,它们比9道AC题提供了更多信息。注意:如果一道题超过30分钟没有任何进展,先把思路和疑点记下来,然后换题。情绪消耗比脑力消耗更容易让你提前放弃。三天刷题的记录到这里基本复盘完了。我个人实际操作中的体会是,刷题到一定数量之后,拼的已经不是代码量,而是对模板的理解深度和排除bug的熟练度。这次三天13道题,AC11道,看似只是一个普通记录,但真正有价值的,是每一道错题对应的那个具体细节:二分边界、DP初始化、BFS入队标记、空间压缩时的旧值保存。如果你也想试试,建议也按“知识点分组+固定时间块+当天复盘”的方式安排,哪怕只做三天,回头再看的时候,你会发现自己对算法的理解比题数更扎实。最后再分享一个小技巧:每天结束后,把当天所有出错点写成一句话,放到手机备忘录里,隔两天翻一遍,比考前突击笔记有用得多。