考完算法设计与分析,出考场的那一刻,我最想做的不是对答案,而是把整张卷子按题型拆开复盘一遍。这门课期末能不能拿高分,基本不取决于你记住了多少经典算法,而取决于你能不能在两小时内把平时练过的推导节奏稳定复现出来。下面这份整理,来自我自己参加的一次期末考试复盘,题面细节做了模糊处理,但每道题背后的考点、解法思路和踩坑点都是完整的。想考前突击的同学、想理解算法课考试套路的同学、以及单纯想看看某些经典题怎么推标准答案的同学,都能从里面找到自己需要的东西。
1. 考前的信息战:怎样判断一门算法课的高频考点
1.1 先把课程主线压缩成考点地图
期末考试不是考察“你认识多少算法”,而是“你能否在约束条件下完成推导”。我复习时做的最有效的一件事,就是把一学期的内容压缩成六大块:复杂度分析与递推求解、分治策略、动态规划、贪心、回溯与分支限界、图算法与NP完全性。我没有把“算法分析基础”单独列一块,因为复杂度分析会渗透到每一道题里,后面所有题目几乎都要求你给出时间复杂度和空间复杂度。
最开始我是按课堂讲义一章一章往下看,效率很低,看完了感觉还是会忘。后来我把每章的例题抄在纸上,按题型分类标注,才发现真正反复出现的模型其实比想象中少。比如分治这边来来回回就是最近点对、归并排序、快速排序选第k大这类;动态规划那边则是矩阵链乘、0-1背包、最长公共子序列、最大子段和轮着来。把这些模型摸透,考试时看到的题目再变,底层思路也是熟悉的。
1.2 题型与判分习惯:这门课到底在考什么
从我那届的卷面构成看,大致分成四类。第一类是概念简答题,问你P、NP、NP-C的定义,或者问某个算法的适用条件;第二类是纯粹的计算推导题,给递推式求渐进界,给实例填动态规划表;第三类是算法设计题,要求写出算法思想、伪代码、复杂度、正确性论证;第四类是综合应用题,通常把图算法或者最大流包装成一个小场景。
这门课的判分习惯,给我的感觉是“思路分占大头、答案分占小头”。如果你只写出了最终答案但没有任何推导过程,成绩可能反而不如一个写出了完整思路但最后结果算错的。这一点直接影响我的答题策略——凡是需要推导的题,我都把步骤写得清清楚楚,哪怕最后一步算错了,也能拿到前面那些步骤分。考前把这个判分习惯想明白,比多刷三套题都管用。
2. 复杂度与主定理:第一道大题的稳定拿分姿势
2.1 主定理的三个case,以及递归树兜底法
复杂度计算题基本是送分题,但每年都有同学在这里丢分。主定理针对形如T(n)=aT(n/b)+f(n)的递推式,三个case比较的是f(n)和n^(log_b a)的增长率。case 1是f(n)=O(n^(log_b a - ε)),此时T(n)=Θ(n^(log_b a));case 2是f(n)=Θ(n^(log_b a) log^k n),此时T(n)=Θ(n^(log_b a) log^(k+1) n);case 3是f(n)=Ω(n^(log_b a+ε)),且满足正则条件a f(n/b) <= c f(n),此时T(n)=Θ(f(n))。
很多同学记不住ε和正则条件,我后来改用递归树的直观方式理解:如果把递归树画出来,每一层对总复杂度的贡献若是等比递减,则总复杂度由根节点决定;若是等比递增,则总复杂度由叶子层决定;若每层差不多相等,就乘上树高。这个方法对主定理不适用的情况也能兜底,比如T(n)=T(n/2)+T(n/3)+n这种没法直接套主定理的递推式,画递归树反而能看出每层贡献的量级变化。
2.2 一道复杂度计算题的完整解析示范
卷面上出现过一类递推式,比如T(n)=3T(n/4)+Θ(n²)。我的解析步骤是这样的:画递归树,第一层是n²,第二层是3*(n/4)²=3n²/16,第三层是9n²/256,呈现等比数列,公比3/16<1,所以总复杂度收敛于n²/(1-3/16)=16n²/13这个级别,因此T(n)=Θ(n²)。用主定理也能得到相同结果:a=3,b=4,log_b a=log_4 3≈0.792,f(n)=n²明显高于n^0.792,落到case 3,再检查正则条件:3*(n/4)²=3n²/16 <= (3/16)n²,确实被一个小常数控制住,所以T(n)=Θ(n²)。
这一题的关键不是能不能算出结果,而是书写时要把比较过程写清楚。我犯过的一个错误是只写了“case 3”但没有交代正则条件,模拟测试时被扣了分。后来我把主定理的三段判断范式固定成模板:先写a、b、log_b a,再写f(n)与n^(log_b a)的比较,最后写对应case和结果。这样一套下来,这类题基本不失分。
还有一类在主定理边缘的情况,比如T(n)=2T(n/2)+n log n。此时a=b=2,n^(log_2 2)=n,f(n)=n log n是n的略高阶形式,属于case 2的变种,结果是Θ(n log² n)。我当时先写递归树展开:根节点贡献n log n,两个子树各贡献(n/2)log(n/2),加起来约等于n log n,每层复杂度几乎相同,树高log n,所以总复杂度是Θ(n log² n)。这种展开式推导在考场上非常实用,比死记硬背case 2的公式更不容易出错。
3. 动态规划大题:状态定义对了,后面只是填表
3.1 从矩阵链乘看状态设计的共同套路
动态规划大题几乎是每次期末考试的重头戏。我把常见DP题分成几类:序列型、区间型、背包型、树形DP。矩阵链乘属于典型的区间DP。题目会给你一组矩阵维数,比如A1:10×20,A2:20×50,A3:50×5,问哪种括号化方式使标量乘法次数最少。
解析思路是:先证明最优子结构。假设在最优括号化方案中,最后一次乘法发生在A1..Ak和Ak+1..A3之间,那么两个子链A1..Ak和Ak+1..A3也一定分别是最优的。用反证法想就很清楚:如果某个子链存在更优方案,那整体方案可以替换成更优方案,和“整体最优”矛盾。然后定义状态m[i][j]表示从Ai连乘到Aj的最小代价,递推式是m[i][j]=min_{i<=k<j} {m[i][k]+m[k+1][j]+p[i-1]p[k]p[j]},边界条件m[i][i]=0,表示单个矩阵不需要乘法。
填表时从区间长度2开始递增。比如m[1][2]=102050=10000,m[2][3]=20505=5000;m[1][3]=min{m[1][1]+m[2][3]+10205, m[1][2]+m[3][3]+10505}=min{5000+1000, 10000+2500}=6000。这里的关键是p数组表示维数,p[i-1]是Ai的行数,p[i]是Ai的列数,p[j]是Aj的列数。用额外的s[i][j]记录k的位置,就可以还原出括号方案。复杂度是三层的O(n³)。我的建议是直接在答题纸上先画一张小表格,再在旁边写转移方程,这样阅卷人给分会比较痛快,自己检查也方便。
3.2 动态规划答题模板与常见扣分点
所有DP大题我都用同一套结构来回答:第一,简述最优子结构;第二,定义子问题并写清楚下标范围;第三,给出状态转移方程,解释为什么这么转移;第四,写明初始条件和无意义状态;第五,分析时空复杂度。很多同学只写转移方程不写初始条件,这往往是0分起评的重要原因,因为边界条件决定递推是否成立。
空间优化不是必拿点,如果题目没有要求,我就不会为了展示滚动数组把可读性牺牲掉。我见过有人把dp[i][j]优化成dp[j]后写错了遍历方向,导致整道题作废。滚动数组是一维还是二维、内层循环是正序还是倒序,这些细节一旦错了,整个答案都是错的。
另一种经典题型是0-1背包:dp[i][j]表示前i个物品放进容量为j的背包能获得的最大价值,转移是dp[i][j]=max(dp[i-1][j], dp[i-1][j-w_i]+v_i),边界dp[0][j]=0,最终答案是dp[n][C]。如果要逆推选了哪些物品,需要另开bool数组记录。这里的坑点在于物品下标从0还是从1开始,会直接影响循环边界。我的习惯是让下标从1开始,这样递推式更整洁,也不容易把第0个物品和没物品的情况搞混。考场上填表时,先手动填掉第一行和第一列,往往能立刻暴露转移方程里写反的变量。
4. 贪心、回溯与分支限界:证明和剪枝才是隐藏考点
4.1 贪心算法的正确性证明:交换论证怎么做
贪心题在卷面上往往以“设计一个贪心策略并证明正确性”的形式出现,而不是只要求你写算法。举个例子,活动选择问题:一批活动给出开始时间和结束时间,选择尽量多的活动,使它们互不重叠。贪心策略是按结束时间从小到大排序,依次选最早结束且不冲突的活动。
证明套路叫交换论证。假设某个最优解中第一个选的活动不是贪心算法选择的最早结束活动,把这个活动替换成最早结束活动。替换后,剩余活动集合不会变差,因为最早结束活动的结束时间不会晚于原来的第一个活动,不会挤占后续活动。反复替换之后,就存在一个包含贪心选择的贪心解,因此贪心算法正确。这种证明的核心就是“不差”两个字,只要替换不会让解变差,就能说明贪心选择不会错过最优解。
我在复习过程中把活动选择、霍夫曼编码、最小生成树这三类经典贪心的证明反复抄过三遍。抄到后来说“贪心选择+交换论证”已经变成了条件反射。考场上的贪心题大多不是因为它有多难,而是因为平时只练算法不练证明的人,到了现场写不出严谨的论证过程,白白丢了大分。
4.2 回溯与分支限界的框架:剪枝分寸怎么拿
回溯题通常是0-1背包、装载、图着色。这类题的核心是解空间树加剪枝。我的答题框架是一个递归函数:先判断是否到达叶节点,更新最优解;否则对当前层的每个可能选择尝试,并用约束函数和限界函数决定是否进入子节点。以0-1背包为例,约束函数是重量不超过背包容量,限界函数常用“当前价值+剩余容量按单位价值贪心装入的上界”,如果上界已经不超过当前最优解,就剪枝。
def dfs(level, weight, value): if level == n: best = max(best, value) return if value + bound(level, weight) <= best: return # 限界剪枝:最优也不可能超过当前解 # 不选当前物品 dfs(level + 1, weight, value) # 选当前物品,约束函数判断 if weight + w[level] <= C: dfs(level + 1, weight + w[level], value + v[level])bound(level, weight)的计算方式是:把剩余容量按物品单位价值从高到低依次装入,允许最后一个物品只装一部分,得到的是一个松弛问题的上界。这个上界一定不会低于真实最优值,所以如果它已经不大于当前最优解,就可以安全剪枝。很多人写限界函数时只考虑了剩余价值之和,导致上界偏松,剪枝效果差;合理做法是允许部分装填来估计上界,这样剪枝力度更强。
分支限界与回溯的差别在于搜索顺序和活结点表:队列式分支限界按宽度优先扩展,优先队列式用上界大的结点先扩展。做题时节点要存四样东西:层数、当前重量、当前价值、剩余容量的上界。我踩过的坑是忘记给物品按单位价值排序就直接算上界,导致优先队列弹出的节点顺序完全不对。先把物品按单位价值降序排好,再上界函数里按顺序填装,逻辑才自洽。
5. 图算法与NP完全性:理论题背后的固定套路
5.1 图算法:背熟最短路径和最小生成树的适用边界
图算法在期末卷上一般不是最难题,但容易在“选错算法”上丢分。我整理了一张适用表,考前反复看:
| 算法 | 适用场景 | 复杂度 | 核心注意点 |
|---|---|---|---|
| Dijkstra | 单源正权图 | O((V+E)log V) | 不能处理负权边 |
| Bellman-Ford | 单源图,可有负权边 | O(VE) | 可检测负环 |
| Floyd | 全源最短路径 | O(V³) | 实现简单,限制规模 |
| Prim | 最小生成树,稠密图 | O(V²)或O(E log V) | 从顶点出发扩展 |
| Kruskal | 最小生成树,稀疏图 | O(E log E) | 按边权排序加并查集 |
考场上出题常常给一个带权图,要求求单源最短路。如果你默认所有图都是正权就把Dijkstra写上,很多时候拿不到分,因为老师想考的是你会不会先判断“是否存在负权边”。我的习惯是答题第一行先写“该图权值为正/存在负权边,因此适用XX算法”。这一句话就是送分点,但它体现了你区分适用边界的能力。最大流题目一般会指定用增广路径,要求画残余网络,最终求最大流数值和最小割容量。做这类题时把最小割对应的顶点集合标出来,通常能拿到最后一问的分数,因为最小割容量等于最大流值是课程里最核心的结论之一。
5.2 NPC归约:先定方向,再写构造
NP完全性这一块,是我认为整张卷子最容易被误判为“背诵题”的部分。很多同学把P、NP、NP-C的定义背得滚瓜烂熟,但实际题目是“证明XX问题是NP完全的”,需要做归约,而不是默写定义。套路是固定的:先证X∈NP,给出一个证书以及多项式时间验证算法;然后选一个已知NPC问题Y,把Y归约到X,即Y≤_p X。
注意方向。要证明X是NPC,应该是已知难的Y约化到X,也就是把Y的实例转换成X的实例。转换需要满足两个方向:若Y实例答案为真,则转换后的X实例答案为真;若X实例答案为真,则Y实例答案也为真。卷面上常见的归约包括3-SAT归约到团问题、团问题归约到顶点覆盖、顶点覆盖归约到支配集等。只要把构造和两个方向写全,这道题一般能拿大部分分数。
我采用的记忆方式是建立一张归约链,从SAT到3-SAT,再到CLIQUE,再转到VERTEX COVER。真正考场上遇到类似构造题时,我至少能定位到链上某一步,再根据具体题目微调构造。这里最怕的是把归约方向写反——把X归约到Y,那就变成证明“X最多和Y一样难”,证明不了X是NPC。考前把方向这个问题想透,比多背十个定义都管用。
6. 考场时间分配与复盘:那些丢分点考前根本没人提醒
6.1 我会按什么顺序答题
考试时我先花三分钟浏览全卷,再按“计算推导题→DP题→图算法题→贪心题→回溯题→概念题”的顺序作答。原因是概念题即使留到最后也能凭记忆写,计算推导题却是滚雪球型的,一旦中间思路断掉,后面补救很浪费时间。
每道题分配时间时,我会给DP大题留出二十分钟填表和写转移方程,因为这类题分数重且容易检查。复杂度计算题即使简单我也写满推导,防止因为结果对但过程缺失被扣分。完整推导本身也是得分保障,不只是给阅卷人看,更是给自己确认思路没有跳步。最后二十分钟我用来检查边界条件:递推式里a和b有没有写反,DP表的第一行和第一列有没有填对,NPC归约的两个方向是否写反。这些检查点都是我在平时模拟中真实丢过分的项目。
6.2 复盘后我现在最想改掉的三个习惯
第一,复习时不要只看不写。我见过很多同学说“我理解了那个DP状态”,但真正到考场上填表时才发现下标乱成一团。我后来规定自己,每个DP题必须手写一张至少3×3的小表,用具体例子验证转移方程。矩阵链乘填一次表,比默写十遍递推公式都管用。
第二,证明题不要跳过。贪心正确性证明、DP最优子结构证明,这些是老师用来区分高分的关键。我第一轮复习时觉得“太显然了”,结果模拟考时写不出规范化论证,词不达意。后来我把经典证明当成朗读材料一样逐步口述,直到说得顺口为止。能流畅讲出来的证明,写下来才不会乱。
第三,警惕“因为这道题贴了XX标签所以用XX算法”的惯性。期末考试很多题目是刻意误导的,比如把最大流场景包装成路径选择问题,把二分图匹配包装成调度问题。关键是要能从一个场景里提取出问题本质,而不是看到“调度”两个字就写贪心,看到“路径”两个字就写Dijkstra。先建模、再选算法、最后证明,这个流程在考场上是保命的。
说句实在话,算法设计与分析这门课期末考试真正考的不是记忆力,而是你日常是否养成了“先建模、再选算法、最后证明”的思维习惯。把平时作业里的每一道题都按这个流程过一遍,比考前熬夜刷十套试卷有用得多。这次考完复盘,我自己最大的收获也在这里。