资讯动态

算法竞赛刷历年真题:本地调试与对拍验证闭环

发布时间:2026/10/28 6:08:23 来源:尧图企业网站定制
1. 为什么刷历年真题一定要配一套本地流程每年固定举办、题目质量在圈内口碑一直很好的那项年度程序设计赛事它的历年真题是很多人算法路上绕不开的训练素材。题目风格稳、考点覆盖面广、难度梯度均匀从签到题到压轴题都齐全适合拿来练手。但真正决定你能不能把它刷出效果的其实取决于一套配套的本地流程。所谓本地流程就是从环境搭建、写代码、编译调试到用小数据对拍验证、用大数据测性能这一整套动作。只做题不验证等于把真题当阅读理解做看完题解觉得“懂了”赛场上照样写不出来或者写出来也是错的。我见过太多这样的例子题库里 AC 数量刷到几百一到正式比赛就卡在第二题。问题往往不出在算法知识上出在“写出来的代码没有经过严格验证”这个环节。平时做题有在线判题告诉你对错那只是最终结果你并不知道自己错在哪一组数据、为什么错甚至不知道自己这次没出错是因为数据太弱。而历年真题恰恰是那种“看上去简单、暗坑特别多”的类型尤其是计几、图论建模、多组数据输入这些题边界问题非常密集。这套流程适合谁刚接触算法竞赛、准备开始系统刷真题的同学肯定用得上正在准备校赛、区域赛想把手感提上来的选手也用得上哪怕是工作几年想捡回算法基本功的开发者同样能用它来自学。整个过程对语言没有硬性依赖我用 C 写得比较多但换成其他语言思路完全一样。下面我会把每一步拆开讲包括我实际踩过的坑、参数该怎么定、脚本该怎么写让你看完就能照着搭一套。1.1 三种刷题姿势的差距到底在哪把刷题行为分个类大概能分成三种效果差得非常远。第一种是“看题解型”读完题想三五分钟没思路直接翻题解看完觉得有道理抄一遍代码提交 AC走人。这种刷法的收益极低因为算法思想的迁移能力是在“卡住—尝试—失败—再尝试”这个过程里建立的抄来的代码只会给你一种虚假的掌握感。第二种是“闭门造车型”坚决不看题解一道题卡三天最后要么放弃要么写了个错的自己还不知道。这种也不好时间成本太高而且遇到自己完全没接触过的算法方向硬想是想不出来的纯属浪费时间。第三种是“闭环型”先独立思考 30 到 60 分钟有明确思路后动手写写完先过样例再过自己构造的边界数据然后写暴力程序对拍最后测一下大数据下的耗时。如果这时还过不了再看题解重点看自己卡在思路还是卡在实现。我实践下来第三种方式的单题收益是第一种的五倍以上而且留下的记忆特别牢固。这套流程的核心成本其实很低环境搭一次脚本写一次后面每道题只多花十分钟但这十分钟换来的东西非常值得。1.2 在线判题给不了你的三样东西在线判题平台是刷题的必需品但它有三件事做不了而这恰恰是本地环境的价值所在。第一是过程可见。线上只告诉你 WA、TLE 还是 RE不告诉你在第几组数据上挂的。你在本地跑可以打印中间变量、可以单步调试、可以看自己的程序在哪一步开始偏离预期。特别是那种“样例过、交上去 WA”的题没有本地调试能力你只能瞎改。第二是数据可控。线上的测试数据你拿不到也不能自己造一组极端数据去试。本地你可以随手构造一个 n1 的、全是相同元素的、或者刚好卡在某个阈值上的数据看程序会不会崩。这种能力在排查边界 bug 时是决定性的。第三是时间可控。线上编译排队、提交限流一次反馈可能要等几十秒本地编译一秒内完成改一版代码马上能看到结果。把一天里等待反馈的时间省下来多做两三道题完全没问题。而且本地跑大数据还能直接看到秒数比线上“TLE 或 AC”的二元反馈精细得多。1.3 一条完整的刷题闭环长什么样我把自己的流程画成一条线你可以对照看自己的流程缺了哪一段。第一步是读题反复读三遍把输入格式、输出格式、数据范围、多组数据与否、有没有特殊约定全部抠清楚。第二步是估算复杂度用数据范围反推应该用哪个量级的算法。第三步是写实现同时把暴力解的思路也在心里过一遍。第四步是样例验证官方样例必须全过。第五步是构造边界数据自测比如最小规模、最大规模、全相同、单调递增递减。第六步是对拍跑几百上千组小数据。第七步是性能测试造一组最大规模数据看耗时。第八步是复盘把这道题的新知识点和踩过的坑写进笔记。这八步里第三到第七步是纯技术活也是最容易偷懒的部分。很多人做到第四步就提交了结果是靠平台的弱数据蒙混过关到了正式比赛原形毕露。下面几章我会把第二、三、四、五、七步的实操细节全部展开。2. 环境搭建把编译、运行、调试压缩到一次按键环境搭建这件事很多人觉得是“装个编译器就完事了”其实差别很大。一套顺手的本地环境能把“改一版代码看结果”的周期从三十秒压到三秒这个差距在连续调试时会非常明显。我见过有同学每次改代码都要手动敲一长串编译命令一天下来光敲命令就废了。这一章讲清楚编译器怎么选、参数怎么加、目录怎么组织、模板怎么写。2.1 编译器与编译参数的门道C 选手基本都是 GCC 系Windows 上一般用 MinGW 版本Linux 和 macOS 自带或者用包管理器装。版本建议 9 以上能支持 C17。历年真题里大量题目的标程都是按 C14 或 C17 写的用太老的编译器可能连语法都不支持。编译参数的配置是重头戏我给一套我自己长期用的g -stdc17 -O2 -Wall -Wextra -Wshadow -o a a.cpp逐个拆解一下为什么这么加。-stdc17是标准版本避免用了新特性本地过、线上不过的问题如果赛方明确用 C14本地就同步改成 c14标准版本一定要和评测环境一致这是我吃过亏的地方本地用 C17 的结构化绑定写得很爽赛场上编译器是 C11直接 CE。-O2是优化等级。这里有个取舍不加优化本地跑大数据可能超时你分不清是算法问题还是没优化加了优化某些未定义行为比如数组越界、有符号溢出可能被优化掉导致本地不崩线上崩。我的做法是调试阶段不加 -O2测性能时再加。你可以准备两套编译命令一个叫dbg一个叫run用别名或者脚本区分。-Wall -Wextra打开常见警告这个太重要了。它会提示未使用变量、有符号无符号比较、可能未初始化、函数缺少返回值。上面这些警告里有相当一部分直接对应线上 WA 或 RE 的成因。-Wshadow更狠一点会警告变量遮蔽比如你在全局定义了n函数里又写了int n它能揪出来。这类 bug 找起来极其痛苦让编译器帮你找最划算。如果你用递归写深搜还得注意栈空间。Linux 下默认栈一般是 8MB深度几十万的递归会直接爆栈。可以临时用ulimit -s unlimited放开但要注意这只影响本地线上该爆还是爆所以根本解决办法是把递归改成手写栈或者加记忆化剪枝。2.2 目录与文件命名约定目录结构看起来是小事刷到第五十道题的时候你就知道重要性了。我用的结构是这样algo/ ├── tasks/ │ ├── 2023-r1-a/ │ │ ├── main.cpp │ │ ├── brute.cpp │ │ ├── gen.cpp │ │ ├── stress.sh │ │ └── notes.md │ └── 2023-r1-b/ ├── template/ │ └── tmpl.cpp └── scripts/ └── newtask.sh每个题目一个独立目录四个文件固定main.cpp是你的解法brute.cpp是暴力对拍程序gen.cpp是数据生成器stress.sh是对拍脚本。目录名用“年份-轮次-题号”的格式一眼能看出是哪个题不用回忆。这个约定的价值在于降低对拍的启动成本。如果每道题都要重新想一遍脚本怎么写、文件叫什么对拍这件事你坚持不了三天就会放弃。固定下来之后新建目录就是一个命令的事脚本甚至可以直接从上一题复制。2.3 编辑器配置一键编译运行编辑器用什么不重要重要的是能不能做到“按一个键编译并运行”。我用的是轻量编辑器配外部命令的方案也可以直接在编辑器里配构建任务。核心是三条命令# 编译 g -stdc17 -O2 -Wall -Wextra -o a a.cpp # 运行带输入文件 ./a in.txt # 运行交互式输入 ./a把前两条绑到快捷键上改完代码按一下编译错误直接显示在终端里看都不用切窗口。如果你习惯用图形化 IDE把这套命令配到它的运行配置里也一样。还有一个细节输出重定向。调试大数组时终端会刷屏把输出写到文件里再用 diff 对比更方便。我平时习惯用./a in.txt out.txt然后用编辑器的文件对比功能看差异。这个习惯在调格式类题目比如要求输出空格分隔的数字时特别有用因为肉眼很难看出行末多了个空格。2.4 代码模板里该放什么不该放什么模板的作用是省掉每题都要敲的头文件、输入输出、常用小工具。但模板也容易写成“全家桶”什么都往里塞结果编译变慢、变量名冲突、用不上的一堆东西干扰阅读。我的原则是只放高频且低耦合的东西。必放的头文件区algorithm、vector、string、iostream 等常用几个、using namespace std、using ll long long、快速读入函数格式化输出题目多的时候有用、一个极简的主函数框架。不放的任何具体算法实现。并查集、线段树、最短路这些每题现场敲敲多了自然快而且现场敲能根据题目特点调整比直接套模板更不容易出错。把大段算法塞进模板的后果是比赛中手生模板里的函数写错一个下标上界你连从哪查都不知道。还有一个小技巧模板里留一段带#ifdef LOCAL的调试输出代码块本地编译时加-DLOCAL打开交题时自动失效不影响性能也不影响输出。#include bits/stdc.h using namespace std; using ll long long; const int MAXN 1e5 5; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int T; cin T; while (T--) { solve(); } return 0; } void solve() { // 实现 }注意上面solve()写在了调用之后这是刻意示范的常见错误类型——严格来说这里应该先声明真实使用时别照抄。主函数里默认处理多组数据的框架是我刷历年题的强烈推荐做法因为很多题目输入格式带 T如果你平时不练这个结构赛场上临时想起来容易漏掉清空全局数组。3. 从题目到代码把“想清楚”翻译成写对写完环境只是开始真正的分水岭在于能不能把思路踏实落地。这一章讲怎么用数据范围定算法方向、哪些坑点必须提前防、调试输出该怎么写。3.1 用数据范围反推复杂度目标这是老手和新手最明显的能力差异之一。新手拿到题先想“这题用什么算法”老手先看数据范围。数据范围直接告诉你可行复杂度的上限等于给你圈定了搜索空间。数据规模 n时间限制 1s 下的可行复杂度常见对应算法方向n ≤ 10O(n!)全排列枚举n ≤ 20O(2^n · n)状压 DP、子集枚举n ≤ 40O(2^(n/2))折半搜索n ≤ 100O(n^4)区间 DP、Floydn ≤ 500O(n^3)三元环、Floyd、区间 DPn ≤ 5000O(n^2)普通 DP、暴力枚举对n ≤ 1e5O(n log n)排序、二分、树状数组、线段树、Dijkstran ≤ 1e6O(n) 或小常数 O(n log n)线性 DP、单调队列、前缀和n ≤ 1e9O(log n)、O(sqrt n)快速幂、二分数位、质因数分解n ≤ 1e18O(log n)矩阵快速幂、数位统计这张表的用法是这样的看到 n ≤ 1e5你就要排除 O(n^2) 的直接枚举同时也不用去想 O(n) 的什么神奇技巧目标就锁在 O(n log n)。看到 n ≤ 20八成是状压。有了这个约束你在一张图或一个 dp 状态设计上纠结的时候可以快速判断“这个设计会不会超时”避免花半小时写了个必然 TLE 的做法。反过来如果一道题你想到的做法复杂度刚好卡在可行范围边缘比如 n1e5你想到 O(n sqrt n)那就要格外注意常数优化并且本地必须实测最大数据。这就是后面性能测试存在的意义。3.2 高频翻车点清单下面这些是历年真题里反复出现的坑我按出错频率排了序刷题时挨个对照检查一遍。第一整数溢出。题目里说答案可能很大你就得全程用 64 位。容易漏的是中间量两个 1e9 的数相乘结果存 int 就爆了但最终答案取模后很小看起来没问题。我的做法是模板里直接using ll long long涉及乘法的中间量全部用 ll然后心里默念一句“凡是乘法先想会不会爆”。无符号数更要小心unsigned和int混合运算结果会隐式转换出现负数会变成巨大的正数这种 bug 极难发现。第二多组数据不清空。带 T 的题目每轮之间必须把上一轮的全局数组、计数器、邻接表清零。典型的漏网之鱼是head[]、cnt、vis[]。我的习惯是所有全局状态集中写一个init()函数每轮开头调一次写完立刻检查有没有遗漏某个新加的数组。第三数组开小。无向图存边要开 2 倍边数线段树要开 4n字符串拼接后的长度是原来两倍离散化后的下标从 1 开始时数组要开 n1。这些数字记不住没关系养成习惯凡是开数组的地方都多想一句“最大可能用到多少”。第四递归爆栈。深搜深度上万就要警惕了尤其是带权并查集的路径压缩、树上的 DFS、Tarjan 求强连通。本地不一定崩评测机栈空间更小就容易崩。解决办法是能迭代就迭代或者减小递归深度。第五sort 比较函数不满足严格弱序。写cmp(a, b) { return a.x b.x; }这种带等号的比较一旦有相等元素某些实现下可能越界崩溃。标准写法是严格小于。这个 bug 在 Linux 和 Windows 下表现不同经常是本地好好的、线上 RE。第六输出格式。行末多余空格、答案之间多空行、最后一组数据后不该换行却换了这类格式错在历年题里很常见。本地自测时要专门用diff对着标准答案看别用眼睛扫。3.3 调试输出的正确姿势调试输出写得好能省掉一半排查时间。我的三个原则一是用 cerr 不用 cout。cerr 不缓冲崩溃时最后的输出还在而且它和 cout 分开不会污染正常输出方便重定向对比。二是用条件编译包裹。所有调试代码包在#ifdef LOCAL里交题时不用手工删一段段注释容易漏删。#ifdef LOCAL #define dbg(...) fprintf(stderr, __VA_ARGS__) #else #define dbg(...) #endif三是输出带上下文。别只打印一个数字要打印“这是第几轮、哪个变量、值是多少”。比如dbg(round %d, dp[%d] %lld\n, t, i, dp[i]);。排查时不带上下文的裸数字等于白打。另外提一句assert。写复杂数据结构时用assert(l r)、assert(0 idx idx n)把不变量断言出来一旦违反立刻停止并打印行号。这比静默地算出一个错答案然后你花一小时反推要高效得多。当然交题前要确认assert在关闭NDEBUG时会被保留——默认情况下加了-DDNDEBUG才会失效不加的话线上遇到非法状态会直接 RE这反而是好事能暴露问题。4. 对拍验证四件套的完整实操对拍这件事是第一梯队的刷题技巧。它解决的是“我怎么知道自己对不对”这个根本问题。原理很简单你有一个高效解法正确性存疑你还有一个暴力解法虽然慢但逻辑一目了然正确性几乎确定。让两者跑同一批小规模数据比对输出。这一章把完整实操讲透。4.1 对拍的原理与前提条件对拍成立的前提有两个。第一你的暴力解必须是显然正确的。它可以慢到 O(n^3)但逻辑必须直白到不需要证明比如“枚举所有点对”“枚举所有子集”“直接模拟每一条指令”。如果暴力解本身就很绕那对拍失去意义——两个错的程序对出一样的结果也很正常。第二数据规模必须小。对拍的价值在于可以跑成百上千组所以单组数据要小到暴力解毫秒级完成。一般把 n 控制在 5 到 10 之间就够用因为大部分逻辑错误在小数据上就会暴露。如果小数据全过、大数据挂那说明问题出在复杂度或者大数组越界上这时候对拍帮不上忙得靠性能测试和边界构造。必须说清楚一点对拍跑过不代表你的代码正确。它只能证明你在生成器覆盖到的数据形态上没出错。如果你的生成器只会生成随机数据而错误只出现在“所有元素相同”或者“恰好成一条链”这种特殊结构上对拍是抓不到的。这一点后面会专门讲怎么补救。4.2 暴力程序怎么写才敢信写暴力有一个反直觉的原则不要复用你主程序的任何函数哪怕只是一行输入输出。从零开始敲独立实现宁可代码丑、跑得慢、写得笨。原因很直接——如果你从主程序复制了一个辅助函数而那个函数本身有 bug两份程序会一起错对拍就白做了。暴力程序的典型写法有几种枚举所有候选答案取最优、模拟每一步操作、用最朴素的递归搜所有可能。比如一道求最短路的题暴力就用 Bellman-Ford 反复松弛 n 次一道求区间最大值的题暴力就每个询问从 l 遍历到 r一道计数题暴力就枚举所有子集统计。暴力程序的输入输出格式必须和主程序完全一致包括多组数据的处理方式、换行、空格。这是新手最容易翻车的地方主程序读 T 组数据暴力只读一组那么对拍出来的所有结果都会不一致你以为是主程序错了实际上是暴力读错了输入。小技巧暴力程序里可以加一点“检查不变量”的 assert比如算出来的答案不会超过输入规模这样暴力本身出问题时能立刻发现。4.3 数据生成器的设计要点生成器是整套对拍里技术含量最高的一环也是决定对拍有效率的关键。第一版生成器通常长这样用一个随机种子用mt19937生成随机数用uniform_int_distribution限范围。一定要用种子做参数保证同一组失败数据能复现这一点是重中之重的。很多新手写的生成器用srand(time(0))对拍挂了你却永远复现不了那组数据只能干瞪眼。#include bits/stdc.h using namespace std; int main(int argc, char* argv[]) { int seed argc 1 ? atoi(argv[1]) : 0; mt19937 rng(seed); int n rng() % 8 1; printf(%d\n, n); for (int i 0; i n; i) { printf(%d , (int)(rng() % 20) 1); } printf(\n); return 0; }第二版生成器要根据题目结构做“参数化”。比如图的题目要能控制点数、边数、有没有重边、是不是连通、边权范围字符串题目要能控制字符集大小全 a 和随机字母是两种完全不同的数据形态序列题目要能生成单调、全等、随机、锯齿几种形态。我一般的习惯是给生成器加一个mode参数根据 mode 走不同分支mode数据形态用途0完全随机覆盖一般情况1极小规模n1 或 2抓边界崩溃2全部元素相同抓去重、相等比较逻辑3严格单调递增抓极端结构4随机但存在大量重复抓哈希、计数5链状/星形结构图论题抓退化情况这个设计花不了多少时间但能把对拍的命中率提高一大截。一个成熟的做法是让对拍脚本轮换 mode每个 mode 跑几十组。4.4 一键对拍脚本的完整实现Linux 下用 bashWindows 下用 bat 或者干脆装个类 Unix 环境。下面是一个我常用的 bash 版本逐行解释。#!/bin/bash # 用法: ./stress.sh 起始种子 结束种子 g -O2 -stdc17 -o main main.cpp || exit 1 g -O2 -stdc17 -o brute brute.cpp || exit 1 g -O2 -stdc17 -o gen gen.cpp || exit 1 for ((i$1; i$2; i)); do ./gen $i in.txt ./main in.txt mine.txt ./brute in.txt std.txt if ! diff -q mine.txt std.txt /dev/null; then echo FAIL at seed $i cp in.txt fail_$i.txt echo 输入已保存到 fail_$i.txt echo --- 期望输出 --- head -5 std.txt echo --- 实际输出 --- head -5 mine.txt exit 1 fi if [ $((i % 50)) -eq 0 ]; then echo 已通过 $i 组 fi done echo 全部通过: $1 ~ $2几点说明。第一行三个编译命令如果任何一个编译失败直接退出避免拿旧的可执行文件跑。diff -q只比较是否相同不打印差异配合重定向到/dev/null保持输出干净。发现不一致时立刻保存输入文件这是最重要的——有了这组数据你可以在主程序里加调试输出慢慢定位问题。最后那条进度打印是为了让你知道脚本还在跑不是卡死了。对于浮点输出的题目diff直接比较会失败因为浮点误差。这时候要换比较方式可以用一个专门的比对程序读两行浮点数判断相对误差是否在 1e-6 以内。写一个小 C 程序大概二十行或者用现成的脚本语言都比手工看强。还有一个小优化把main和brute的编译移到循环外只在脚本开头编译一次否则每跑一组都编译一遍慢得离谱。我一开始就犯过这个错跑一百组花了五分钟后来才发现问题。4.5 对拍挂了之后怎么复现和定位拿到失败数据之后标准操作是三步。第一步确认是不是暴力错了。把失败输入手工跑一遍或者肉眼看一看判断主程序和暴力哪个更可信。有时候暴力写得太笨比如没考虑 0 下标结果反而它错了。第二步在主程序里挨个关键节点打输出和手工推算的结果对照找到第一个开始不对的位置。这个位置之前的所有逻辑都是对的问题就出在那一两步。这种二分定位法比从头到尾读代码高效得多。第三步如果问题涉及算法结构而不是单点计算把数据规模再缩小比如从 n8 缩到 n3手工走一遍完整流程状态的变化一步一步写下来。规模小到能手工算清楚的时候错误藏不住。最后提醒一句修完 bug 之后一定要重跑同样的种子范围确认原来失败的那组数据现在过了而且没有引入新的失败。有时候改了一个地方修好了 A 组数据却让 B 组数据挂了这叫修修补补式调试只有重跑才能发现。5. 常见问题与排查实录对拍跑通只是完成了正确性验证实际提交时还有一堆别的问题。这一章按“症状—原因—排查手段”的格式整理都是我自己和身边人反复踩过的。5.1 对拍全过但提交挂掉这是最让人心态崩的情况但对拍全过不代表没问题常见原因有四类。数据覆盖不够。生成器只会造随机数据而错误只出现在特殊结构上。补救办法就是上一节说的参数化生成器覆盖全相同、严格单调、边界极小、链状这几种形态。数组开小了。小规模数据下越界访问恰好落在空闲内存里没崩也没错大规模数据下越界踩到别的变量结果就变了。这类问题对拍永远抓不到只能靠养成“每个数组都想想最大需要多少”的习惯。整数类型不对。小数据下乘积不溢出大数据下溢出。这个也抓不到得靠数据范围反推只要题目里出现了 1e9 这个量级涉及加法和乘法的地方就全换 64 位。复杂度问题。小数据跑得飞快大数据 TLE。这就是需要性能测试的原因。排查顺序建议是先本地造最大规模数据测时间再看数组大小再检查整数类型最后回头看算法本身有没有遗漏的情况。5.2 运行时错误与编译错误速查下面这张表是我整理的常见报错对照表遇到问题时可以快速定位方向。症状最可能的原因快速排查手段RE 且无任何输出数组越界、空指针、除零开-fsanitizeaddress编译重跑RE 且程序中途停住递归爆栈减小递归深度或改迭代RE 只在个别用例sort比较函数非严格弱序检查所有比较函数CE 提示某函数不存在标准版本不匹配对齐评测环境的-stdWA 但样例全过多组数据未清空、溢出手动构造多组数据测试WA 且只差小数浮点精度改用long double或加容差比较TLE 且算法复杂度正确常数过大、memset大数组用时间函数定位到具体语句TLE 且复杂度明显超算法方向错了回到数据范围重新推复杂度这里特别说两个。-fsanitizeaddress是一个非常好用的工具编译时加上它程序运行时会自动检查越界访问、内存泄漏、使用未初始化内存。它的代价是运行变慢、内存占用变大所以只适合在小数据上抓 bug。很多“本地不崩线上崩”的诡异问题用它一跑就现原形强烈建议加进你的调试工具集。另一个是memset清空大数组。当数组是 1e7 级别而你有 1e5 组数据时每组都memset一遍直接 TLE。解决办法是记录修改过的位置用完逐点复原或者改用时间戳标记法。这类问题不实测大数据的多组数据你根本发现不了。5.3 性能测试与常数优化性能测试的做法很简单造一组最大规模数据直接用time命令跑。g -O2 -stdc17 -o main main.cpp ./gen 1 big.txt # 生成最大规模数据 time ./main big.txt /dev/null输出会给出 real、user、sys 三个时间看 user 时间最准。一般经验是本地耗时控制在时间限制的三分之一以内比较安全因为评测机性能可能有波动加上不同编译器的常数差异留够余量才稳。如果本地耗时贴着限制有几个常规优化方向。输入输出方面ios::sync_with_stdio(false)和cin.tie(nullptr)是必须的或者直接换快速读入。数据结构方面把vector的push_back预留空间、避免频繁扩容用数组代替动态容器减少无意义的拷贝函数参数大对象用引用。算法常数方面unordered_map在特定数据下会被卡可以换排序加二分或者手写哈希bitset在需要固定范围枚举时常常有奇效。我得说一句实话如果本地耗时超过时间限制别急着优化常数先回头检查算法复杂度是不是真的对。常数优化最多帮你去掉两三倍复杂度错了是几十倍的差距方向比努力重要。6. 训练节奏与赛前检查工具和流程都有了最后讲讲怎么把它变成日常习惯。这一章不是方法论套话是我自己实践下来觉得确实有用的几个做法。6.1 一道题练三遍我的习惯是同一道题至少过三遍。第一遍是正常流程从读题到对拍全程走完这一遍耗时最长可能一两个小时。第二遍是隔两三天后重做不看之前的代码只看题目目标是 20 分钟内写完并一次通过。第三遍是隔一周后只看题目标题能否在 5 分钟内说出核心思路和关键坑点。这三遍的收益是递增的。第一遍学的是“怎么做”第二遍练的是“写得快”第三遍确认的是“真的记住了”。很多人刷题的效率低是因为只做了第一遍然后就去刷下一道了结果两周一过全部遗忘题库 AC 数量很多实际能力原地踏步。6.2 复盘笔记怎么写每个题目目录下的notes.md只写三样东西多了没必要。第一是核心转化这道题的关键一步是什么比如“把区间和问题转化成前缀和的差”“把图的最优路径问题转化成最小生成树的路径最大值”。这一句是整个题目最值钱的地方写清楚它以后遇到同类题能瞬间联想到。第二是踩过的坑具体到自己写错的那个点比如“多组数据忘了清空 vis 数组”“下标从 1 开始时数组开成了 n”。“写错了某个算法”这种表述太笼统没有复盘价值。第三是对拍抓到的失败样例把那个fail_xxx.txt的数据形态描述一句比如“全是相同元素时我的去重逻辑错了”。这些样例积累多了就是你的私人题库。6.3 赛前环境检查清单正式比赛前一天花十分钟把下面这张清单过一遍能避免很多低级事故。检查项确认内容编译器版本号、-std参数与赛方一致编译命令保存成脚本或别名的都测试一遍模板文件放在能快速打开的位置主函数结构完整对拍脚本至少准备一份能直接改成新题常用复杂度表打印或记在纸上方便快速定位栈空间设置如果允许确认递归深度上限输出格式复查行末空格、空行、多测试用例分隔还有一个非常实用的建议准备一份“解题检查表”赛场上每写完一道题提交前对着扫一遍。内容包括数据范围里有没有 1e9 以上的量需要 64 位、是不是多组数据、全局数组有没有清、无向图边数组有没有开两倍、输出的格式对不对。这五条能覆盖我统计里七成以上的非算法性错误。比赛时心跳加速、思路发散靠检查表比靠记性靠谱得多。我个人在实际操作中的体会是前面这些流程搭起来的那一周确实有点麻烦各种脚本调不通、生成器写得不对、对拍反而抓出一堆自己没意识到的错挫败感挺强。但过了那个坎之后做题的手感和正确率完全不一样了。最明显的变化是提交前心里有底那种“希望这次能过”的赌博感少了很多取而代之的是“这题我大概率是对的如果挂了我能很快找到原因”的确定感。这种感觉比多刷一百道题更值钱。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑