GESP五级通关指南:递归边界、逆序对、二分答案、贪心与DFS剪枝

发布时间:2026/9/24 22:27:21
GESP五级通关指南:递归边界、逆序对、二分答案、贪心与DFS剪枝 从报名GESP五级到真正坐在考场里很多孩子会发现一个扎心的事实一二三四级靠“多刷题”能堆过去到了五级光刷题不够了。五级是GESP序列里第一道真正的分水岭它开始系统考察递归、分治、排序、二分、贪心这些“算法思维”而不是单纯的语法堆砌。2025年后GESP五级和CSP-J的衔接政策一出来备战五级的人数又涨了一波。但真题做下来你会发现五级的难不在“题面看不懂”而在五个特别容易翻车的深水区递归边界写错、排序交换次数不会推、二分答案边界崩、贪心没有反例意识、深搜剪枝不到位。这篇文章我结合历年真题逻辑和带学生备考的实战经验把这五个深水区逐一拆开讲透顺便把“饮品调制”这类组合枚举题的解题思路也一并交代清楚。无论你是准备五级还是想提前给CSP-J打底这篇都值得认真看完。1. 递归与递推五级真正的“入场券”递归这个东西一二三级也在提但基本是“知道有这个概念”的程度。到了五级递归是实打实要用来解题的而且经常和递推、分治混在一起考。很多孩子能看懂递归代码但自己一写就崩问题基本都出在边界条件和调用关系上。1.1 从“母牛生小牛”看递推公式和边界GESP五级特别喜欢考一类题初始条件给几个数后面每一项由前面几项决定问你第n项是多少。比如经典的“母牛生小牛”一头母牛从第四年开始每年生一头小牛问第n年一共有多少头牛。这题用递推写就是f(n) f(n-1) f(n-3)前三年的值要手动初始化。这类题表面是“找规律”本质是考察你有没有理解“递推公式”和“边界条件”是算法的两个核心支柱。我见过太多孩子背下了斐波那契的代码但题目一变形比如“从第四年开始生”就不知道初始条件怎么设了。这里有个直观的方法先在草稿纸上写几行小数据把前5项都手算出来再回头看递推式对不对。比如母牛题第1年1头第2年1头第3年1头第4年2头第5年3头第6年4头第7年6头。如果你按f(n)f(n-1)f(n-3)算第4年就是f(3)f(1)112对得上。这一步看似笨但能帮你拦住一大半初始条件设错的低级失误。再说说记忆化。五级的递归题如果不用记忆化纯递归去算斐波那契第40项你的程序会卡到怀疑人生。GESP的测评机对时间限制卡得很紧一般就1秒。纯递归的时间复杂度是指数级的而五级题目数据范围动不动就给到n 10^6这个量级只能用递推或者带记忆化的递归。我给学生讲记忆化的时候最爱打一个比方你每天背单词背过的单词记在单词本上第二天复习时直接翻本子而不是重新去把整本词典背一遍。记忆化就是这个“单词本”用一个数组把已经算过的结果存起来下次要用直接查时间复杂度从指数级降到线性级。#include bits/stdc.h using namespace std; const int MAXN 1000005; long long memo[MAXN]; long long f(int n) { if (n 3) return 1; // 边界前三年都只有1头 if (memo[n] ! 0) return memo[n]; // 记忆化查单词本 return memo[n] f(n - 1) f(n - 3); // 递推今年 去年总数 三年前的总数它们开始生小牛 }这里有个细节memo数组的类型建议用long long别用int。五级的递推题很多答案会迅速超过int的范围比如斐波那契第50项就超过20亿了一个int直接溢出变成负数你查错半天都想不到是这里的问题。这也是真题出题人最喜欢埋的坑之一。1.2 递归边界自查清单写代码前的4个必问递归函数的坑总结下来翻来覆去就那么几个。我在带学生复盘真题错题时整理了一份“递归四问”自查清单每次写完递归代码对照着问一遍能避免80%的错。终止条件是什么是否覆盖了所有“最小情况”比如f(0)、f(1)都要单独确认。每递归一次参数是否在向终止条件逼近有没有可能在某些输入下永远递归不到边界递归函数返回值用什么类型会不会溢出要不要用long long递归深度会不会超过系统栈限制如果深度达到10^6级别递归大概率会爆栈要考虑改写成递推或者手动模拟栈。尤其第四点很多孩子忽略。GESP有些题看着能用递归写但数据范围大到n10^6你递归深度要是10^6运行时会直接Segmentation Fault。这不是算法问题是栈空间问题。测评机的栈一般默认8MB递归一层大约占用几十字节算下来十万层就到极限了。遇到这种题老老实实用递推循环实现别头铁递归。五级真题里还有一种常见考法让你判断一个递归函数的输出。这类题考察的就是“追踪递归调用过程”的能力。我的建议是画递归树从根节点开始把每次调用的参数和返回值都写在树上一层一层展开。很多孩子嫌麻烦直接心算算着算着就乱了。画递归树虽然慢一点但准确率高很多而且画多了之后你对递归结构的直觉会明显变强后期速度反而会上来。2. 排序算法交换次数背后藏着逆序对GESP四级、五级都考排序但考法完全不同。四级考的是“会不会调用sort或者手写冒泡”五级考的是“排序算法的本质逻辑”。最典型的一道题就是热词里的“冒泡排序交换次数”这题看起来人畜无害实际上把排序、计数、分治三个知识点串在了一起。2.1 冒泡排序的交换次数为什么等于逆序对数先问一个问题冒泡排序每交换一次实际上交换的是什么很多人的回答是“两个数交换位置”但这个回答不够本质。冒泡排序每次交换本质上是在消除一个“逆序对”——也就是一对“左边的数比右边大”的组合。你从一个完全乱序的数组开始每交换一次逆序对数量就减一数组排好序时逆序对数量为0。所以总的交换次数恰好等于初始数组的逆序对数量。这个结论GESP怎么考最常见的是给你一个数组问冒泡排序需要交换多少次。有些孩子真去模拟冒泡排序循环套循环去数交换次数。这个方法在n 1000时勉强能用但五级数据范围经常给到n 10^5你模拟一遍冒泡排序O(n^2) 的时间复杂度直接超时。正确解法是用归并排序求逆序对数量时间复杂度只有 O(n log n)秒过。这里还有一个容易混淆的点选择排序和插入排序的交换次数和冒泡不一样。选择排序每次交换可能消除多个逆序对所以不能直接用逆序对数算选择排序的交换次数。GESP很喜欢在这种细节上出判断题比如“选择排序的交换次数一定小于冒泡排序”——这句话是错的因为选择排序在最坏情况下要交换 n-1 次但每次交换不一定消除所有逆序对。真题考的就是你对算法过程的细致理解。2.2 归并排序求逆序对的完整代码与证明归并排序求逆序对的思路不复杂在合并两个有序子数组时如果左边数组的某个数比右边数组的某个数大那么左边数组从这个数开始到末尾的所有数都大于右边这个数。把这些数量累加起来就是逆序对数量。#include bits/stdc.h using namespace std; const int MAXN 100005; int a[MAXN], tmp[MAXN]; long long cnt 0; void merge_sort(int l, int r) { if (l r) return; int mid (l r) / 2; merge_sort(l, mid); merge_sort(mid 1, r); int i l, j mid 1, k l; while (i mid j r) { if (a[i] a[j]) { tmp[k] a[i]; } else { tmp[k] a[j]; cnt mid - i 1; // 左边从 i 到 mid 所有数都比 a[j] 大 } } while (i mid) tmp[k] a[i]; while (j r) tmp[k] a[j]; for (int i l; i r; i) a[i] tmp[i]; }注意代码里cnt mid - i 1这一步是整个算法的核心。每次发现右边的数小于左边的数时说明左边数组从当前位置到mid的所有数都能和当前的右边数组成逆序对。这里数量不是1而是mid - i 1很多初学的小朋友写代码时容易漏掉这个1导致结果少算。另外cnt用long long也是必须的一个长度为10^5的完全逆序数组逆序对数量接近5 * 10^9远超int能表示的范围。归并排序在这里的意义不只是求逆序对它本身就是五级的核心考点。五级真题会考察归并排序的稳定性、时间复杂度、空间复杂度甚至让你手写归并排序过程。我建议你把归并排序的代码写到“肌肉记忆”的程度——闭着眼睛都能写对。因为到了六级七级归并排序思想还会用在更多高级算法里。3. 二分答案把“最优化问题”变成“判定问题”五级考二分不是只考“从一个有序数组里找一个数”。那种题用binary_search就能过太基础了。真正的深水区是“二分答案”——当一个题目让你求“最大值最小”或者“最小值最大”时你要意识到这可能不是一道模拟题而是一道二分题。3.1 二分查找和二分答案的区别在哪二分查找解决的是“已知数组有序快速找目标值”的问题二分答案解决的是“已知一个解的范围快速找最优解”的问题。两者共享同一个灵魂每次把搜索区间砍一半O(log n) 的效率逼近答案。生活化类比二分查找像“查字典”——字典按拼音排好序了你要找一个字就直接翻到中间看是在前面还是后面二分答案像“猜价格”——你不知道商品实际价格但知道一个范围每次猜一个数对方告诉你“高了”还是“低了”你不断缩小范围最后猜中。区别在于二分查找的“字典”是现成的二分答案的“价格”需要你写一个check函数去验证“这个答案可不可行”。GESP五级最常见的二分答案题是“书架/分组问题”有 n 本书每本有厚度要分成连续的 m 组问每组厚度之和的最大值最小是多少。这种题的经典解法就是二分这个“最大值”然后写一个check函数判断在这个最大值下能不能分成不超过 m 组。check函数往往是贪心的——尽量把书往一组里塞塞不下就新开一组。3.2 二分边界模板理解了就不会死循环二分答案写起来最让人崩溃的就是边界问题。l和r的初始值怎么定while循环里是l r还是l rmid要不要1这些细节稍微错一个轻则死循环重则答案差1。我自己的习惯是统一用“左闭右开”的写法因为它在处理整数二分时最不容易出错int l 1, r 1e9 1; // 答案范围[l, r) while (l r) { int mid (l r) / 2; if (check(mid)) { r mid; // mid 可行答案可能是 mid 或更小 } else { l mid 1; // mid 不可行答案一定比 mid 大 } } // 循环结束后 l r就是最小可行答案注意这里r初始值为什么是1e9 1而不是1e9因为开区间 [l, r) 里r本身不参与检查。如果你把r设成可能的最大答案而实际答案恰好等于这个最大值程序就可能漏掉正确答案。多给1的余量是一种防御性写法。五级真题里的二分题数据范围经常给到10^9你把它当int处理没问题但如果你把l r直接相加在某些极端情况下可能溢出。写成int mid l (r - l) / 2;是更安全的写法。二分答案的核心其实是写对check函数。很多孩子框架写得飞快到check就卡住了。我的建议是写二分题时先不去想二分框架先把check函数写出来——给定一个值判断它是否可行。check写对了二分框架套上去基本是半小时以内的事。反过来如果你先套框架再想check很容易被框架的边界问题搅乱思路越改越乱。4. 贪心策略会排顺序更要会证明贪心算法是五级考试的“送分题”和“送命题”的结合体。说送分是因为经典贪心题套路固定排序 扫描就完事说送命是因为出题人稍微变一下条件你以为的贪心策略就失效了而你自己还浑然不知代码写得越自信错得越离谱。4.1 区间调度和排队问题为什么先排序就对了五级最常考的贪心有两类。第一类是区间调度有一堆活动每个活动有开始时间和结束时间要在同一个教室办活动问最多能办几个。经典贪心策略是按“结束时间”从早到晚排序然后依次选择结束时间最早且不与前面冲突的活动。为什么按结束时间排因为结束早的活动给后面的活动留了更多空间这是在所有可行策略里对全局最有利的。第二类是排队问题有若干个人排队接水每个人有一个接水时间问如何安排顺序能让所有人等待时间之和最短。答案是“用时短的人先接水”。这个策略的直观解释是如果让一个用时很长的人排前面那么后面所有人都在等他整体等待时间被拉长短任务先做后面排队的人等待成本更小。这类题在GESP五级真题中一般会给一个简单的“排序 累加”的代码就能过但它真正的考点是“你知不知道要先排序”——很多孩子一看题目就想着模拟排队过程完全没意识到要先排序导致复杂度爆炸。这两类题还有一个共同点都要求你能证明“为什么这个策略是对的”。五级的题目不要求你写严谨证明但你心里得有数。我教学生一个常用的证明思路叫“交换论证”假设最优解里存在相邻两个元素不符合你想要的顺序尝试交换它们证明交换后结果不会变差。如果能证明说明排序策略正确。这个思路看起来抽象但练几道题之后你会形成条件反射看到贪心题第一反应就是“能不能用交换论证”。4.2 贪心的反例意识为什么“性价比”有时候不成立贪心最容易翻车的地方是直觉上认为对的策略被一个精心构造的数据卡掉。我经常给学生举一个例子现在有一个背包容量是10有三个物品分别是重量6价值12、重量5价值10、重量4价值8。如果按“性价比”价值除以重量从高到低选三个物品性价比都是2随便选一个最后发现不管怎么选都只能装一个最大价值12。倒是直接选重量5和价值5的另一个组合可能更优。你看这道题用整数物品0-1背包时“性价比”策略就失效了。但GESP五级一般不直接考背包因为背包是七级动态规划的内容。五级考贪心时数据范围通常会保证“贪心策略恰好成立”比如分数背包物品可以切割这时候性价比排序才是对的。出题人真正想考的是你有没有意识到“什么条件下贪心可用”。检验贪心策略是否成立我有一个很实用的土办法快速写一个暴力枚举代码再写一个贪心代码自己生成几百组小随机数据对拍。如果贪心在几千组数据下都和暴力结果一致那大概率是没问题的一旦对拍发现有一组不一致恭喜你你成功发现了一个反例这比考试时发现要好一万倍。五级备赛阶段学会对拍是一个能让你少丢很多分的重要技能后面的搜索章节我会详细介绍。5. DFS与剪枝搜索是五级隐藏的“大题关卡”GESP五级的最后一类深水区是深搜——DFS。很多备考机构的模拟题里五级压轴题经常是一道带有搜索背景的组合枚举题。热词里的“饮品调制”就是典型给你几种原料每种原料可以选不同量要凑出某个目标值问有多少种组合方式。这种题表面是个生活化场景本质就是DFS枚举所有方案再用剪枝优化时间。5.1 搜索框架先画递归树再写代码DFS代码本身并不长难的是把问题转成“搜索树”。我拿“饮品调制”举例假设有三种原料A、B、C每种原料可以选0到若干毫升目标是调配出正好100毫升的混合饮品。这个问题的搜索树长这样第一层决定A选多少第二层决定B选多少第三层决定C选多少同时每层都要判断当前已选总量有没有超过100超过了就没必要继续往下选了。写DFS的第一步不是敲代码而是画递归树。在我的教学经验里一个孩子如果能正确画出搜索树代码基本能一次写对画不出搜索树的代码写得再快也是空中楼阁。递归树要明确三件事每一层代表哪个变量决策维度、每个节点有多少个分支决策范围、叶子节点需要满足什么条件约束条件。“饮品调制”这类题每一层是一种原料分支是原料的用量选择叶子条件是“正好凑到目标值”。画完递归树后代码就水到渠成了#include bits/stdc.h using namespace std; int n, target, ans 0; int volumes[15]; // step: 当前决定第几种原料cur: 已累计的容积 void dfs(int step, int cur) { if (cur target) return; // 剪枝1当前值已超目标后面不可能合法 if (step n) { // 所有原料都决定完了 if (cur target) ans; return; } for (int v 0; v volumes[step]; v) { dfs(step 1, cur v); // 尝试下一种原料选 v 毫升 } }这个代码能过小数据但如果你直接拿它去跑大数据大概率超时。问题出在循环里的v从0枚举到volumes[step]如果每种原料能加的量很大比如1000毫升那三层循环就是1000^3 10^9种方案任何测评机都扛不住。这时候就必须剪枝了。5.2 剪枝的三个层次可行性、最优性、对称性剪枝是DFS的灵魂。五级真题里的搜索题数据范围通常专门设计成“不剪枝超时剪枝刚好能过”的状态目的就是考察你有没有这个优化意识。我把剪枝分成三个层次从易到难在“饮品调制”这类题上逐层叠加第一层是可行性剪枝。最简单的判断就是“当前累计值已经超过目标值直接返回”。上面代码开头的if (cur target) return;就是可行性剪枝。这个剪枝看似不起眼但在数据随机分布时能把搜索空间砍掉一大半。更进一步的可行性剪枝是提前计算后缀和如果当前值加上剩余所有原料的最大可能值还小于目标值说明后面再怎么加也凑不够直接返回。第二层是最优性剪枝。这一层主要用于求解“最大值/最小值”的题目核心是维护一个当前的“最优值”。搜索过程中如果发现当前状态的代价已经比已知最优解还差就没必要继续搜了。举个例子如果题目改成“求凑出100毫升饮品最少用几种原料”你一旦发现当前已经用了超过最优解数量的原料马上剪掉。第三层是对称性剪枝。这层用得少但对特定题目杀伤力极大。比如“饮品调制”如果三种原料AB和BA算同一种调制方案那么你就可以在搜索时强制下一个原料的编号不小于上一个避免重复枚举。对称性剪枝的本质是“把搜索空间的重复部分砍掉”它要求你对题目理解得非常清楚只要不误伤合法方案效果立竿见影。三种剪枝叠加后同样的搜索题运行时间可能从超时降到0.01秒。我带学生刷题时要求每道搜索题至少写3个版本第一版纯DFS跑小样例第二版加可行性剪枝跑中样例第三版加最优性/对称性剪枝跑满数据。通过这个过程孩子们能真切感受到剪枝对性能的影响而不是把它当概念背。5.3 对拍提升正确率的“笨办法”也是最好的办法讲DFS必须讲对拍因为搜索题的正确性比性能更让人焦虑。你写了一个DFS加剪枝样例过了但总担心还有隐蔽的边界情况没考虑到。这时候最靠谱的方法就是写一个“暴力解”来对拍——用完全朴素、不剪枝但保证正确的做法在两个程序之间跑几百组随机数据比对输出。我自己调试搜索题的标准流程是这样的先写正确的暴力DFS再用剪枝优化版。然后用脚本生成随机小数据同时跑两个程序对比输出。只要两组数据不一致立即停下来排查。GESP备考中这个“小数据暴力对拍”的习惯能帮你规避从思路到代码的绝大部分bug。#include bits/stdc.h using namespace std; // 对拍脚本框架生成随机数据分别运行两个程序对比输出 int main() { for (int t 1; t 10000; t) { // 1. 生成随机小数据写入 input.txt // 2. 运行 ./solve_brute input.txt out1.txt // 3. 运行 ./solve_optimized input.txt out2.txt // 4. 用 system(diff out1.txt out2.txt) 比较 } }很多时候你发现优化版和暴力版结果不一致问题反而不在剪枝上而在“边界条件”——比如step n和cur target的判断顺序写反了导致最后一个原料还没加入就提前结算。这种问题肉眼盯代码很难发现一对拍立刻现原形。所以我的结论是搜索题的正确性不是靠“细心”保证的是靠“对拍”保证的。五级考场上虽然不能对拍但平时训练养成了对拍习惯你写代码时自然会谨慎很多犯错率也就下降了。再说回“饮品调制”这道题的启示。它本质上就是“组合枚举 剪枝”但它给了我们一个重要信号GESP五级的压轴题越来越喜欢披着生活场景的外衣考察算法题目里出现“调制饮品”“安排活动”“规划路线”这些词汇时不要被题面迷惑要迅速翻译成“这是DFS枚举”“这是区间贪心”“这是二分答案”。这种“翻译能力”才是五级真题最想考察的核心素养。我个人的体会是五级备考最有价值的事情不是机械地刷几百道题而是每做完一道真题都问自己三个问题这道题考的是哪个算法的哪个细节我卡在哪一步如果数据范围扩大100倍我的程序还能过吗带着这三个问题复盘比闷头刷十套卷子都管用。等你能把五级的递归边界、逆序对、二分答案、贪心反例、搜索剪枝这五个深水区都摸透后面的六级七级动态规划和图论你也不会觉得是空中楼阁——因为那些进阶内容本质都是今天这些基础算法思维的延伸和组合。最后分享一个我自己带学生时反复强调的小习惯整理错题时不要只抄题目和代码要把“错误类型”也记下来。比如“递归边界漏了n0”“逆序对cnt忘用long long”“二分check写反了”“贪心没验证就提交”“DFS剪枝条件截止到下界”。记录错误类型你会发现自己来来回回踩的坑就那么几个提前知道这些坑考场上的你就能少慌一大半。