2026牛客寒假算法基础集训营4(ABCFHI)完整跟下来,最大的感受是:这六道题放在一起,就是一张“算法基本功体检表”。它不考什么奇技淫巧,考的全是你平时最容易忽略、也最不能忽略的底子。春节期间能在刷题中度过,说实话挺爽的,我从A题一路做到I题,中间踩了不少坑,也查漏补缺了一大堆,今天把完整思路、板子细节和避坑经验一次性写清楚,给后面想补这场集训营的同学做一个参考。无论你是为了春招实习、暑期实习笔试,还是想系统提高数据结构与算法水平,这套题目都值得认真过一遍。
1. 集训营的整体出题逻辑与解题框架
先说一个很多人没意识到的点:牛客寒假集训营的每场题目,题号排布本身就是出题人的“教学大纲”。A、B、C这类靠前的题是给大多数人保底拿分的,F、H、I这类中后段题目才是真正拉开差距的地方。所以如果你集训营打完只关心过了几题,那就浪费了这套题最大的价值。
1.1 为什么是ABCFHI这六道题
正常情况下,一场比赛会有A到K甚至更多的题,但这次的题目列表只包含A、B、C、F、H、I,中间缺了D、E、G和更后面的题目。这其实是一个非常经典的梯度设计:
- A题:签到题。送分用的,目的是让你快速进入状态,检验基本输入输出和边界处理。
- B题:简单算法题。通常是一道贪心或者简单模拟,考察你有没有“先想清楚再写代码”的习惯。
- C题:数论基础题。快速幂、逆元、组合数取模这类东西是高频考点,打ACM或者笔试几乎绕不开。
- F题:数据结构题。树状数组、线段树、离散化这类“板子”题,考你对区间操作的熟练度。
- H题:字符串题。KMP属于字符串算法中最基础也最容易被细节坑的一道坎。
- I题:综合题。图论最短路和DP结合,难度明显上升,属于用来区分“会模板”和“会思路”的题。
从这个分布能看出,出题人想检验的,其实就是你在排序、贪心、数论、数据结构、字符串、图论这六个最经典板块的基本功。每个板块都很基础,但组合在一起,想全对并不容易。
1.2 六道题覆盖的算法地图
我在刷题前习惯先把每道题对应的知识点画成一张“地图”,这样刷题时心里有数,不会东一榔头西一棒子。这场集训营的地图大概是这样的:
| 题号 | 核心考点 | 常见解法 | 难度 |
|---|---|---|---|
| A | 排序、模拟、边界条件 | 结构体排序、前缀和 | 较低 |
| B | 贪心、区间问题 | 排序后贪心/双指针 | 较低 |
| C | 数论、组合数学 | 快速幂、费马小定理求逆元 | 中等 |
| F | 数据结构、区间维护 | 树状数组/线段树+离散化 | 中等 |
| H | 字符串匹配、周期性 | KMP构造next数组 | 中等 |
| I | 图论、最短路 | 堆优化Dijkstra、分层图/DP | 中高 |
建议第一次接触算法竞赛的同学,看到这张表后先别急着刷代码,先按这个知识点清单去做复习:排序算法c++手写一遍,贪心找几道经典题练手,快速幂和逆元模板敲一遍,树状数组lowbit的原理搞懂,KMP的next数组手算一次,最后再用堆优化Dijkstra过两道最短路题。基础夯实了,这套题做起来才会顺。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. A题和B题:签到与贪心,别让手速拖累思路
很多人觉得A题签到题没什么好讲的,代码几分钟就写完。但我可以负责任地说,签到题反而是很多人拿不到分的重灾区。原因不是不会做,而是太急、太赶、忽略边界。这两道题我放在一起说,因为它们共同考验的,是“稳”。
2.1 A题:读题比写代码更重要
A题这类题目,通常给你一组数据,让你做一件看起来非常简单的事,比如排序后按某种规则取数、判断是否满足某个条件,或者输出某个特殊序列。以我多年刷题的经验,这类题的坑往往藏在三处:
第一,输入输出格式。牛客这类平台对输出格式要求极严格,多一个空格、少一个换行都会判错。我习惯把所有输出先拼进一个字符串数组,最后统一输出,或者用printf控制格式,避免混用endl和\n导致的缓冲问题。
第二,边界条件。比如排序后取前k个大数,k可能为0,也可能等于n;比如判断条件里涉及到的数组下标,从0开始还是从1开始要写清楚,别把i和i+1搞混,更别在循环里访问a[i+1]时忘了判断i+1 < n。
第三,多组数据。题目如果没有说明单组数据,默认就是多组输入。这时候最容易犯的错就是上一组的变量没清零,导致下一组数据全错。我每次写完A题都会回头看一次循环体外部的变量声明,有没有残留值。
A题我曾经就翻过车,那次的坑到现在都记得:读入了一个数组,要求按降序输出前k个,但我把数组开了int a[105],下标从1开始写,排序却用了sort(a, a+n),结果第一个数永远是0,白白贡献了好几次罚时。所以,签到题的正确姿势是:读两遍题,手推两个样例,再写代码。
2.2 B题:贪心怎么证明“正确”
B题如果在A题之后出现,多半是一道贪心。这类题的特点就是“直觉上很简单”,但很多人不敢写,因为不知道自己的贪心策略对不对。我个人的经验是:贪心题最重要的不是写代码,而是说服自己“为什么这个策略是对的”。
最常用的证明方法就是交换论证法。比如一道题你准备每次取当前最小的元素去处理,那就要证明:如果最优解里存在某一步没有取最小元素,那么我们把这个元素和最小元素交换之后,答案不会变差。只要能证明这一步,贪心策略就基本成立。
还有一类传统贪心是“区间调度”问题:给你一堆区间,选尽量多的不相交区间。这种题的经典做法是按右端点排序,然后尽量选右端点靠前的区间。我当时如果只是背“按右端点排序”,其实是危险的,因为一旦变式改成“覆盖所有区间最少需要几个点”,答案又不一样了。所以我后来做题都养成了一个习惯:把贪心的“排序依据”写在草稿纸上,先写“我为什么按这个关键字排序”,再写代码。
另外,贪心题有一个很好的保底手段:对拍。就是写一个暴力枚举所有方案的代码,再写一个贪心代码,用随机数据反复对比。这套题你能保证B题对拍几百组不出错,那基本就是稳的。
3. C题数论:快速幂、逆元与组合数的底层逻辑
C题一到数论,很多人就开始慌了。其实寒假集训营的数论题考得很正,不偏不怪,核心就是快速幂、逆元、组合数取模这几板斧。这次C题我印象中就是围绕“取模意义下的运算”展开的可应用场景,这类题的难处不在模板,而在你知不知道什么时候该用哪个模板。
3.1 快速幂模板为什么是O(log n)
快速幂的核心思路,是把指数做二进制分解。比如要算a^b mod p,b可以拆成若干个2的幂之和,这样我们只需要对a反复平方,每次处理一位二进制位就行。
cpp复制typedef long long ll;
ll qpow(ll a, ll b, ll p) {
ll res = 1 % p;
while (b) {
if (b & 1) res = res * a % p;
a = a * a % p;
b >>= 1;
}
return res;
}
这里有几个细节我吃过亏,必须提醒你:
res初始化写成1 % p,是为了处理p=1的极端情况,避免返回1而不是0。a = a * a % p这一步,a和a相乘可能溢出int,所以类型必须用long long。- 循环条件用
while (b),如果b本身是0,直接返回1。
Python版更简洁,但要注意Python的取模在负数上和C++行为不一样,竞赛里最好都用非负数运算。
python复制def qpow(a, b, p):
res = 1 % p
while b:
if b & 1:
res = res * a % p
a = a * a % p
b >>= 1
return res
快速幂最典型的应用,就是算组合数取模。当n和k很大但模数p是质数时,我们需要预处理阶乘和阶乘的逆元。
3.2 逆元:为什么模数是质数就能用费马小定理
逆元的概念其实可以类比成“取模意义下的倒数”。正常情况下我们算a / b是直接除,但在模p的世界里,除以b相当于乘上b的逆元inv(b),满足b * inv(b) ≡ 1 (mod p)。
如果p是质数,根据费马小定理,a^(p-1) ≡ 1 (mod p),所以a * a^(p-2) ≡ 1 (mod p)。也就是说,inv(a) = qpow(a, p-2, p)。这是最常用的求逆元方式,条件只有一个:p必须是质数,而且a不是p的倍数。
如果模数不是质数,就要用扩展欧几里得算法求解ax + py = 1来得到逆元。我在这一步的建议很明确:先判断题目给的模数是不是质数。很多题目模数是1e9+7、998244353这类大质数,那直接用费马小定理就行;如果模数是1e9+9之类的也可能不保证质数,就老老实实写exgcd。
组合数C(n, k) = n! / (k! * (n-k)!) 取模,就是预处理阶乘数组fact和逆元数组inv_fact,然后:
cpp复制const int MAXN = 2000005;
fact[0] = 1;
for (int i = 1; i < MAXN; i++) fact[i] = fact[i-1] * i % MOD;
inv_fact[MAXN-1] = qpow(fact[MAXN-1], MOD-2, MOD);
for (int i = MAXN-2; i >= 0; i--) inv_fact[i] = inv_fact[i+1] * (i+1) % MOD;
ll C(int n, int k) {
if (k < 0 || k > n) return 0;
return fact[n] * inv_fact[k] % MOD * inv_fact[n-k] % MOD;
}
注意预处理阶乘时,MAXN要开够,而且逆元数组是从后往前推的,这样只调用一次快速幂,就能得到全部阶乘逆元,复杂度O(n)。如果你开了MAXN但n可能等于5e6,那就要小心数组大小和内存。
我当年第一次写组合数取模时,犯了个特别低级的错误:没有判断k < 0 || k > n就返回,结果C(n, n+1)算出来一个诡异的数值,样例全过但提交WA。这种“逻辑边界”的问题,在数论题里非常常见。
4. F题数据结构:树状数组、线段树的选型与细节
数据结构题是很多人的分水岭。F题这类题目,通常一眼就能看出要维护区间信息,但问题在于你用树状数组还是线段树,以及代码细节能不能一次写对。我做F题的思路是,先判断题目要干什么操作,再决定用哪个数据结构。
4.1 看到“区间查询+单点修改”先想树状数组
如果你的题目只需要单点修改、区间查询,或者区间修改、单点查询,树状数组是首选。原因是它代码短、常数小、不容易写错。树状数组的核心是lowbit,它表示一个数二进制下最低位的1对应的值。比如lowbit(6),6的二进制是110,最低位的1在第二位,所以lowbit(6)=2。
cpp复制int lowbit(int x) { return x & -x; }
void add(int i, int x) {
while (i <= n) {
tree[i] += x;
i += lowbit(i);
}
}
ll sum(int i) {
ll res = 0;
while (i > 0) {
res += tree[i];
i -= lowbit(i);
}
return res;
}
这里的tree数组下标从1开始,这是树状数组的铁律。很多人写树状数组错,就是因为数组下标从0开始,导致i += lowbit(i)时出现了死循环或者漏算。我常用的做法是:如果有数组a[1..n],就直接建树,add(i, a[i]);如果题目给的数据是0-indexed,那我就在读入后统一转成1-indexed再操作。
如果题目要求区间修改+区间查询,就不能只用普通树状数组了。这时候可以引入差分数组,配合两个树状数组维护,也可以直接用线段树。我个人的习惯是:如果只是维护区间和这种简单信息,优先用差分+树状数组;如果需要维护区间最大值、最小值、区间gcd等更复杂的信息,就直接上带懒标记的线段树。
4.2 离散化:数据范围很大时怎么办
很多数据结构的题,数值范围可能到1e9,不能直接开数组做下标。这时候就需要离散化:把需要作为下标的值,映射成连续的整数。步骤很简单:
- 把所有可能出现的值存进一个vector。
- sort排序,unique去重。
- 需要查询某个值的下标时,用
lower_bound找到它在vector中的位置,下标+1(因为树状数组从1开始)。
cpp复制vector<int> v;
// 把所有需要用到的值 push 进 v
sort(v.begin(), v.end());
v.erase(unique(v.begin(), v.end()), v.end());
int id = lower_bound(v.begin(), v.end(), val) - v.begin() + 1;
离散化在求逆序对这类题中是标配。我当时解决F题的时候,本质就是先把大的坐标值映射成小下标,然后对每个点按某种关键字排序,再用树状数组维护计数。
这里分享一个隐藏细节:lower_bound必须在排序后的vector上使用,而且返回的是迭代器,减去v.begin()得到的是从0开始的下标;如果你要用在树状数组上,记得加1,否则下标0会直接让你add(0, x)陷入死循环。
4.3 线段树的懒标记为什么容易错
如果F题最终需要线段树,那懒标记就是最大的坑。懒标记(lazy tag)的设计初衷,是当区间修改不需要立刻下推到每个叶子节点时,可以先暂存在当前节点,等查询或后续操作需要时再下推。这个机制能保证区间修改的复杂度是O(log n)。
我见过太多人写线段树时,在pushdown里忘了更新子节点的值,或者更新了值忘了往下传标记。这里有个口诀:修改到完整覆盖区间时,当前节点的值要加上慵懒标记,同时给当前节点打上标记;一旦递归下去,先把当前节点的标记下推,再递归。写完后,建议用一个小数据区间自测,比如建树{1,2,3,4,5},区间加1,查区间和,逐步手动验证每个节点的值。
另一个常见的坑是线段树数组大小。通常需要开4 * n,因为线段树的结构并非完全二叉树,开到4倍才能保证所有节点都有空间。我一开始嫌浪费想开2*n,结果越界导致各种奇奇怪怪的报错,后来老老实实开4倍,再也没出过越界问题。
5. H题字符串:KMP next数组与边界处理
字符串算法里,KMP是第一个坎,也是必须熟练掌握的坎。这次H题涉及字符串匹配或者周期性问题,几乎必然要用到KMP的next数组。很多人在理解next数组时一头雾水,这里我结合一个经典的例子,把next数组的构造和细节一次讲透。
5.1 热搜里那个熟悉的问题:p="abacaba"的next数组
在KMP算法中,对于模式串p="abacaba",next数组的求法是很多人第一次接触KMP时的噩梦。先明确next数组的定义,不同教材定义有偏差,但牛客和多数竞赛里用的定义是:next[i]表示模式串中前缀p[0..i]的最长相同前后缀的长度(不含整个字符串本身,即长度小于i+1)。注意有些教材定义next[i]表示失配后跳转的位置,这会导致next数组的具体值不同,所以做题前先看题目给出的定义。
我手动模拟一下p = "abacaba":
- i=0,字符a,最长相同前后缀长度为0,next[0] = 0。
- i=1,字符ab,前缀a,后缀b,不相同,next[1] = 0。
- i=2,字符aba,前缀a,后缀a,相同长度1,next[2] = 1。
- i=3,字符abac,前后缀不可能相同,next[3] = 0。
- i=4,字符abaca,前缀a,后缀a,相同长度1,next[4] = 1。
- i=5,字符abacab,前缀ab,后缀ab,相同长度2,next[5] = 2。
- i=6,字符abacaba,前缀aba,后缀aba,相同长度3,next[6] = 3。
所以next = [0, 0, 1, 0, 1, 2, 3]。
手算一次之后,再看代码就轻松多了。构造next数组的核心思路是:用双指针,一个i指向当前要计算的位置,一个j表示当前已匹配的前后缀长度。如果p[j] == p[i],则j++,next[i] = j;否则j回退到next[j-1],反复尝试直到匹配。
cpp复制vector<int> build_next(const string& p) {
int m = p.size();
vector<int> next(m, 0);
int j = 0;
for (int i = 1; i < m; i++) {
while (j > 0 && p[i] != p[j]) {
j = next[j-1];
}
if (p[i] == p[j]) {
j++;
}
next[i] = j;
}
return next;
}
这里最容易错的地方就是while循环里j = next[j-1]这步。很多人不理解为什么不是j = next[j]。因为在代码里,j代表的是“已经匹配了多少位”,对应的就是next数组的值。当失配时,我们需要回退到之前已经匹配好的前缀的最长相同前后缀长度。如果j=3但字符不匹配,就看看长度为3的前缀,它的最长相同前后缀长度是多少,也就是next[2],所以j = next[j-1] = next[2]。我自己初学的时候在这里绕了很长时间,后来干脆死记:j-1,因为j是长度而非下标。
5.2 借助next数组求最小循环节
KMP除了做字符串匹配,还有一个非常常见的应用:求字符串最小循环节。如果字符串长度为L,那么最小循环节的长度是L - next[L-1],并且当L % (L - next[L-1]) == 0时,整个字符串可以由这个循环节重复得到。
这个结论推导起来也不复杂:next[L-1]表示整个串的最长相同前后缀长度,扣掉这部分,剩下的就是循环节的长度。如果总长度能被循环节长度整除,说明字符串刚好由若干个循环节构成。我做H题时就遇到过这种问法,当时现场推了一遍这个结论,代码直接套上去就过了。这个知识点建议每个刷字符串题的人都记住,是真的高频。
使用KMP匹配主串和模式串时还有几个细节:匹配成功后,如果题目要求统计不重叠匹配次数,j要重置为0;如果要求重叠匹配次数,j要回退到next[j-1]继续匹配。这两个场景代码看似相似,但结果差很远,做题时务必看清题目要求。
6. I题图论:Dijkstra堆优化与DP转移细节
I题是整个集训营中难度最高的一道,它考的不仅是Dijkstra模板,而是把最短路和动态规划结合在一起。这类题在ACM和笔试面试里都是高频考法,值得仔细拆解。
6.1 堆优化Dijkstra的写法与复杂度
迪杰斯特拉算法不适合有负权边的图,但绝大多数没有特殊说明的最短路题都能用。朴素Dijkstra在稠密图上是O(V^2),而在稀疏图上,用优先队列优化的Dijkstra可以做到O((V+E)log V),这也是竞赛和笔试里最常用的版本:
cpp复制typedef pair<ll, int> PII;
const ll INF = 4e18;
vector<vector<PII>> g;
vector<ll> dist;
vector<int> vis;
void dijkstra(int s) {
priority_queue<PII, vector<PII>, greater<PII>> pq;
dist.assign(n, INF);
vis.assign(n, 0);
dist[s] = 0;
pq.push({0, s});
while (!pq.empty()) {
auto [d, u] = pq.top();
pq.pop();
if (vis[u]) continue;
vis[u] = 1;
for (auto [v, w] : g[u]) {
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
pq.push({dist[v], v});
}
}
}
}
这里有几个非常关键的细节:
- vis标记要放在出队时判断。因为同一个点可能被重复压入优先队列,只有第一次出队时的距离才是最短距离,后面的都可以跳过。
- 优先队列默认是大顶堆,所以要用
greater<PII>变成小顶堆。 - INF一定要开得足够大。如果图里有1e5个点,每条边权值1e9,最短路可能达到1e14,INF开1e9会直接溢出。建议用16进制或常数
4e18。
我早期写Dijkstra踩过一个大坑:把vis标记写在入队时,结果后面元素出队时,因为vis已标记被跳过,导致某些点的最短路被错误地忽略了。后来我才明白,入队时标记仅适用于BFS(权重相同),Dijkstra必须在出队时判断。
6.2 和图论结合的DP:分层图与最短路计数
I题如果只是裸Dijkstra,那就太普通了。它的进阶版通常是:在最短路径的基础上,问你有多少条不同的最短路径,或者允许你使用一次特殊操作(比如免费走一条边)时,最短路径是多少。
前者是最短路计数,做法是在Dijkstra更新dist时同步更新cnt数组。核心逻辑是:
- 如果
dist[v] > dist[u] + w,说明找到一条更短的路径,那么cnt[v] = cnt[u]; - 如果
dist[v] == dist[u] + w,说明找到一条同样短的路径,那么cnt[v] = (cnt[v] + cnt[u]) % MOD。
但要注意,计数时必须在Dijkstra的正确顺序下进行。因为优先队列弹出的点一定是当前dist最小的点,所以当你用u去更新v时,cnt[u]一定已经计算完毕,不会出现后效性。
后者是分层图最短路,思路是把“是否使用过特殊操作”设计成图的一维状态。比如s到t最短距离,可以建两层图:第一层是原始图,第二层是“已经使用过一次特殊操作”的图。从第一层的u直接跳到第二层的v,边权为0,表示使用了一次免费操作。
有些DP题会用dist[i][k]表示从起点到i,已经用过k次特殊操作的最短距离。本质上也是一种分层图。如果在图上做这种DP,只要转移时保持状态的拓扑顺序正确,就可以在Dijkstra的框架里直接处理。我用这个套路解决过很多笔试里“最多免k条边”的题目,只要k不是特别大,复杂度都能接受。
I题我当时还卡在了一个细节上:分层图建图时,如果直接把两层图的节点编号连起来,要注意层的边界,防止从第二层再跳回第一层。我当时就因为这个逻辑漏洞,样例过了但提交WA,后来加上一个“当前层数”的状态维度,才彻底解决。
7. 常见问题与调试技巧实录
每一场集训营打完,比做对题更重要的,是把自己踩过的坑记录下来。这六道题我踩的问题虽然不同,但总结下来,无非是几类:题意理解错、边界处理漏、数据范围没注意、模板写错细节。下面把最常见的问题整理成一张速查表,方便你以后照着排查。
| 故障现象 | 可能原因 | 解决方法 |
|---|---|---|
| 样例过但提交全WA | 多组数据未清空全局变量 | 循环体内所有变量重新初始化 |
| 数组越界但没报错 | 树状数组/线段树开小了 | 线段树开4n,树状数组下标从1开始 |
| 计算结果超出int | 中间乘积溢出 | 统一用long long,取模时先模再加 |
| KMP匹配数量不对 | 重叠/非重叠匹配逻辑混淆 | 看题要求,匹配完回退到next[j-1]或0 |
| Dijkstra结果偏大 | INF不够大 | 用4e18或LLONG_MAX |
| 递归爆栈 | 线段树递归过深 | 数据规模大时改用迭代或减少递归层数 |
7.1 赛时最容易踩的三个坑
第一个坑是题意读得快,输在输出格式。我见过太多同学,思路完全正确,但输出多了一个空格被判格式错误。尤其是输出多个数时,要求最后一个数后面不能有空格,这需要用类似i == n-1 ? '\n' : ' '的方法控制。这种罚时是最亏的。
第二个坑是数组越界。很多人在赛前喜欢把数组加大一号,比如需要n个元素,就开a[100005],觉得这样万无一失。但这种习惯容易掩盖真正的问题。如果你发现代码在一些小数据上能跑,大数据上却莫名其妙崩掉,建议马上检查所有访问下标的地方,特别注意n+1和n-1这种位置。
第三个坑是不写对拍。很多人在比赛里遇到WA,就死盯着代码一行一行查,效率极低。正确的做法是:立刻写一个暴力解,再写一个随机数据生成器,然后让两个程序同时跑几百组随机数据,几秒钟就能定位出错数据,再针对那组数据调试。这个方法论我每次集训营都强调,真的是能用一辈子的技巧。
7.2 对拍与调试:让代码自己找bug
对拍的具体流程很简单,以C++为例:
- 写一个
sol.cpp,里面是你觉得正确但WA的算法。 - 写一个
bf.cpp,里面是暴力枚举解法,保证答案一定正确。 - 写一个
gen.cpp,生成小范围随机数据,数据量要小到暴力能算完。 - 写一个批处理脚本,循环执行:先用gen生成数据,再分别跑sol和bf,最后用
fc比较输出。
bash复制@echo off
for /l %%i in (1,1,1000) do (
gen.exe > in.txt
sol.exe < in.txt > out1.txt
bf.exe < in.txt > out2.txt
fc out1.txt out2.txt >nul
if errorlevel 1 (
echo find error at case %%i
type in.txt
goto :eof
)
)
echo all passed
Linux环境下可以用bash脚本加diff,结构相同。这里有一个经验:对拍数据范围一定要小,比如n <= 8,a[i] <= 10,这样暴力程序才不会超时。而且生成数据时要刻意制造边界情况,比如所有数相等、数组只有一个元素、k等于0或n,这类数据最容易暴露问题。
7.3 赛后复盘:比多刷题更重要
刷题之后最重要的环节是复盘,但很多人都是“看一下题解,哦原来这样,然后关掉”,这等于白刷。我的复盘流程有三步:
第一步,重写题解。不看任何笔记,从零开始,用自己的话把每道题的核心思路、证明过程、代码细节写一遍。写的出来的才是真会,写不出来的就是没掌握。这步听起来麻烦,但坚持几次后你会发现,自己把一道题“讲清楚”的能力越来越强,这对面试也有直接帮助。
第二步,记录错误类型。给自己建一个错误账本,每次WA之后,把错误归类,比如“边界没判断”“数组开小”“long long溢出”“贪心没证明”等。一个月后你会惊讶地发现,自己的错误集中在少数几个类型上,针对性地改,进步极快。
第三步,做变式延伸。一道题做出来后,不要马上换下一道,而是想一想:如果修改某个条件,这题会怎么变?比如把区间查询改成区间修改,把模式串允许重叠改成不允许重叠,把图上的最短路改成带权点权最短路。这种“出题人视角”的训练,是提升算法水平最快的方法之一。
以我个人经验,这套集训营题目的价值,不只在于让你刷几道题,更在于帮你建立一套完整的“算法体检意识”:看到题目,先想清楚对应哪个算法板块,再想这个板块的常用解法,最后才动手写代码。这个过程熟练之后,不管面对笔试还是ACM,你的心态都会稳很多。最后再分享一个小技巧:集训营的每道题做完后,记录一下你从读到AC用了多少分钟,写在一个本子上。连续刷几场之后,你会越来越清楚自己在哪个板块慢,然后针对性地去补,这就是你下一步刷题的最好指南。
