期末那几天,总有学弟学妹把“习题5.1”拍给我看,说递归式套主定理老是套错,明明觉得是 Theta(n^2),答案却写 Theta(n^2 log n),怎么想都不对。说实话,我当年学《算法设计与分析》的时候也在这个地方卡了很久。第五章的分治算法看起来简单,但一到复杂度的递推式求解,就暴露出两个问题:一是不知道用代入法、递归树、主定理中的哪个;二是知道了工具,却不会判断该套哪一种情形。
这篇我不打算干巴巴地讲定义,而是拿一道典型的“习题5.1”类题目,把整个分析过程从头到尾走一遍。你会看到为什么答案是那个log,也会看到主定理在临界情况下的判断有多容易出错。不管你是在准备“算法设计与分析期末”考试,还是平时做课后作业,这套思路都可以直接复用到其他递推式上。
1. 习题5.1在考什么:递推关系式的渐近界
1.1 我先把你大概会遇到的题面还原出来
很多算法教材的第五章,主题是分治策略。对应到作业和考试,习题5.1这类题目往往是同一个味道:给你一个分治算法的递推式,要求写出时间复杂度的渐近紧确界。
常见的题面有几种版本:
T(n)=2T(n/2)+nT(n)=4T(n/2)+n^2T(n)=3T(n/2)+n^2T(n)=T(n/2)+1
我这次重点用 T(n)=4T(n/2)+n^2 来讲。原因很简单:这道题表面上看,4T(n/2) 的开销和 n^2 好像“势均力敌”,很容易让人犹豫。而犹豫的地方,恰恰就是主定理最有价值的地方。
如果你手头的习题5.1不是这道,也没关系。只要它长得像 T(n)=aT(n/b)+f(n) 这种标准形式,下面这套判断方法完全通用。
1.2 为什么这类题被放在第五章
你可能觉得递归式只是一个数学题,但它的应用场景非常具体。归并排序的复杂度是 T(n)=2T(n/2)+Theta(n),二分查找是 T(n)=T(n/2)+Theta(1),大整数乘法、棋盘覆盖、最近点对,几乎所有经典分治算法的复杂度分析都会落到同一个模型上:
- 把规模为
n的问题拆成a个子问题; - 每个子问题规模约
n/b; - 合并这些子问题结果需要
f(n)时间。
所以写出 T(n)=aT(n/b)+f(n) 只是第一步,真正值钱的是后续的化简能力。第五章的习题5.1就是给你一个集中练习化简能力的机会。你要是能把这个递推式玩明白,后面学动态规划的二维DP、状态转移复杂度分析时,会发现压力小很多。
1.3 这类题的核心目标:渐近紧确界
注意题目里如果写“求渐近紧确界”,那答案不能用 O(n^2 log n) 糊弄,最好写 Theta(n^2 log n)。因为 O 只给上限,Theta 才同时给上下限。期末考试最忌讳的就是只记“大概是一个n平方乘个log”,却分不清自己到底证没证下界。
牢记一个判断标准:如果主定理能套用,它给出的结论条件足够强,可以直接写 Theta。如果能套但你没写 Theta,阅卷老师可能扣分,这就是第一道隐藏的失分点。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 三种解法不是三选一,是一条验证链
很多人以为代入法、递归树、主定理是三种互相独立的方法,考试时选一个就行。实际做题时,它们更像一条验证链:主定理负责快速给答案,递归树负责解释答案为什么合理,代入法则负责严格证明。
2.1 代入法:先猜后证,注意常数松弛
代入法的思想很直白:先猜一个渐近界,再用数学归纳法证明它。
比如我们要证明 T(n)=4T(n/2)+n^2 是 O(n^2 log n)。猜测存在常数 c>0,使得:
T(n) <= c n^2 log n
对足够大的 n 成立。
代入递归式右边:
T(n) <= 4 * c * (n/2)^2 * log(n/2) + n^2
化简:
= c n^2 (log n - 1) + n^2
= c n^2 log n - c n^2 + n^2
要让这个结果小于等于 c n^2 log n,只需要 1-c <= 0,也就是 c >= 1。
这里其实藏了一个新手常犯的错:归纳证明时,要保证对递归调用那一层也成立,所以归纳假设必须对 n/2 成立。而我们证明的是 n,这个逻辑闭合没有问题。但还有一种更隐蔽的问题,就是底数。log(n/2)=log n - 1 这个式子默认了底数是2,因为层数算出来是 log_2 n。所以在写证明步骤时,别把底数丢掉,否则递归树和主定理里的 log_b a 会对不上。
代入法在这道题里能证上界,但如果你只证了 O,没证 Omega,严格来说还不叫“紧确界”。要证下界,通常用 T(n) >= c n^2 log n 同样代入一遍。很多教材把这一步省略,但考试写完整答案时,至少要在最后说一句“类似可证下界”。
2.2 递归树:把复杂度“看”出来
代入法最大的问题是:你猜错了方向,后面全白费。所以面对陌生递归式,我更喜欢先画递归树,把每一层的开销看明白,再去猜。
还是拿 T(n)=4T(n/2)+n^2:
- 根节点:规模
n,自身开销n^2; - 第二层:有4个节点,每个规模
n/2,每个开销(n/2)^2,这层总开销是4 * n^2/4 = n^2; - 第三层:有16个节点,每个规模
n/4,每个开销(n/4)^2,这层总开销是16 * n^2/16 = n^2。
看出来了吧?每一层总开销都是 n^2,没有衰减也没有增长。
树长到叶子节点的时候,叶子规模变成常数。因为每次除以2,层数是 log_2 n。把所有层加起来:
n^2 + n^2 + ... + n^2
一共有 Theta(log n) 层,所以总量是 n^2 * Theta(log n) = Theta(n^2 log n)。
递归树在考试里的价值不只是让你“看出答案”,它还能帮你检验主定理套得对不对。如果你在这个例子里看到每层开销恒定,你就知道结果必然带一个 log n,不可能只有 n^2。
2.3 主定理:最省事,但也有前提
主定理是专门针对 T(n)=aT(n/b)+f(n) 的快捷工具。条件包括 a>=1、b>1、f(n) 是正的渐近函数。
它比较的是 f(n) 和 n^(log_b a) 的大小关系:
| 情况 | 条件 | 结论 |
|---|---|---|
| Case 1 | f(n)=O(n^(log_b a - epsilon)),存在 epsilon>0 |
T(n)=Theta(n^(log_b a)) |
| Case 2 | f(n)=Theta(n^(log_b a) log^k n),k>=0 |
T(n)=Theta(n^(log_b a) log^(k+1) n) |
| Case 3 | f(n)=Omega(n^(log_b a + epsilon)),存在 epsilon>0,且满足正则条件 a f(n/b) <= c f(n) |
T(n)=Theta(f(n)) |
对 T(n)=4T(n/2)+n^2 来说:
a=4, b=2n^(log_b a)=n^(log_2 4)=n^2f(n)=n^2
f(n) 和 n^(log_b a) 完全相等,所以落进Case 2的 k=0 情形,结论是:
T(n)=Theta(n^2 log n)
主定理最怕的不是记忆,而是“看起来有点像,实际差一点”。很多人把Case 1理解成“f(n)比n^(log_b a)小就能用”,这是错的,下一节我专门说这个问题。
3. 主定理使用中那些让人踩坑的边界
3.1 不要只盯着指数大小
Case 1的那个 epsilon 是什么意思?它的意思是:f(n) 不仅要小,而且要小出一个“多项式级别的余量”。
举个例子,T(n)=2T(n/2)+n 里面:
n^(log_b a)=n^(log_2 2)=nf(n)=n
它们相等,不满足Case 1的“小一个epsilon”,也不满足Case 3的“大一个epsilon”,只能套Case 2。最终答案是 Theta(n log n),不是 Theta(n)。
如果题目变成 T(n)=2T(n/2)+n/log n,那更麻烦。f(n)=n/log n 比 n^(log_b a)=n 要小,但小得很慢,不满足 O(n^(1-epsilon)) 对任意 epsilon>0 成立的条件。这种情况下标准主定理直接失效,需要用递归树或者更精细的版本。考试一般不会故意出这种变态题,但平时作业里看到它,要能意识到“标准主定理套不了”。
3.2 Case 3的正则条件是隐性大坑
很多同学只记Case 3开头那半句“f(n) 比 n^(log_b a) 大一个多项式量级”,然后把正则条件忘得一干二净。
正则条件长这样:
a f(n/b) <= c f(n)
对某个常数 c<1 和足够大的 n 成立。
平时遇到的题目绝大多数都满足这个条件。比如 T(n)=3T(n/2)+n^2:
a=3, b=2n^(log_b a)=n^(log_2 3) ≈ n^1.585f(n)=n^2,确实比n^1.585大一个epsilon=0.415的量级
验证正则条件:
a f(n/b)=3*(n/2)^2=(3/4)n^2
取 c=3/4,就有 (3/4)n^2 <= c n^2,条件满足。所以答案可以写 Theta(n^2)。
但如果你是考试,把正则条件单独列一行,一方面显得你懂原理,另一方面也防止主定理只是“恰好蒙对”。如果题目换成 f(n)=n^2(1+sin n),这种振荡函数无法满足正则条件,主定理就不适用了。虽然考试不常考,但理解了这一步,你对主定理的信任度会高很多。
3.3 临界情况下的log因子怎么变
最容易搞混的其实是Case 2里的细节。标准主定理教材里常写:
如果 f(n)=Theta(n^(log_b a)),则 T(n)=Theta(n^(log_b a) log n)。
这里的 log n 多出来,就是因为每一层开销都差不多,叠了 log_b n 层。但如果 f(n) 本身还有一个log因子,例如:
T(n)=4T(n/2)+n^2 log n
那答案不是 n^2 log n,而是:
Theta(n^2 log^2 n)
用扩展版的Case 2理解:如果 f(n)=Theta(n^(log_b a) log^k n),则:
T(n)=Theta(n^(log_b a) log^(k+1) n)
很多期末考试会把这道变体放进选择题,专门钓那些只背基本主定理的人。看到 n^2 log n 别兴奋,先看 k 是几,再决定log要加几次方。
4. 把习题5.1的完整解题模板走一遍
4.1 写在试卷上的标准过程
考试答案不要只写一个最终结果,最好按下面的模板写:
code复制T(n)=4T(n/2)+n^2
令 a=4, b=2, f(n)=n^2。
n^(log_b a)=n^(log_2 4)=n^2。
因为 f(n)=Theta(n^2)=Theta(n^(log_b a)),
属于主定理情况2,k=0。
所以 T(n)=Theta(n^2 log n)。
这个结构有三个关键量必须写清楚:a、b、以及 n^(log_b a)。只要你把这三个量摆出来,阅卷老师一眼就能看出你会不会。很多人跳步直接写“主定理得”,一旦结果算错,过程分都没有。
4.2 为什么递归树验算能发现主定理套错
有同学可能觉得:既然主定理算出来是 Theta(n^2 log n),为什么不用它直接交差?
因为人都会算错。比如把 log_2 4 算成 2,n^2 就相等,这个没问题。但一旦遇到 a=4, b=2, f(n)=n^3,有人会机械地比较“n^3和n^2谁大”,看到前者大就套Case 3,得到 Theta(n^3)。这个答案没错,但中间思考过程如果不用递归树验证一遍,就不知道Case 3凭什么成立。
用递归树看 T(n)=4T(n/2)+n^3:
- 第一层开销
n^3; - 第二层有4个节点,每个开销
(n/2)^3=n^3/8,总开销n^3/2; - 第三层总开销
n^3/4。
每层都按比例衰减,所以整体复杂度由根节点主导,最终是 Theta(n^3)。这正好对应Case 3的直觉:如果合并开销增长太快,子问题再分裂也追不上,根节点说了算。
反过来,如果每层开销都增长,比如 T(n)=2T(n/2)+n^3,其实第一层 n^3,第二层两个 (n/2)^3,总 n^3/4,还是在衰减。真正增长的情形需要 a 和 b 的比值达到一定程度。这些画一遍树就懂了。
4.3 如果题目要求证明主定理里的Case 2
有些学校期末不满足于“会用”,还会让你简单说明为什么Case 2的结论带log。
思路就是从递归树推导。一般形式:
T(n)=aT(n/b)+f(n)
树的高度是 log_b n。第 j 层有 a^j 个节点,每个节点的子问题规模是 n/b^j,所以第 j 层的总开销是:
a^j f(n/b^j)
如果取 f(n)=n^2,且 a=b^2,代入得:
a^j (n/b^j)^2 = n^2 (a/b^2)^j = n^2
因为 a/b^2=1,所以每一层开销恒为 n^2,层数 log_b n 乘上去,结果就是 n^2 log n。这里如果用 log_2 还是 log_b,只差一个常数,Theta表示里可以忽略。
这个推导不复杂,但建议亲手算一次。很多同学纠结log底数,其实完全没必要,因为Theta符号会吸收常数因子。考试里你写 log n,默认就是以2为底,不会出问题。
5. 期末考试复习:把一道题扩展到一类题
5.1 必背的常见递推式结果表
复习到后期,我会建议学生把高频递推式的结果直接背下来,不是为了偷懒,而是为了在考场上建立“答案敏感度”。下面这几个必须一眼能反应:
| 递推式 | 结果 | 说明 |
|---|---|---|
T(n)=2T(n/2)+n |
Theta(n log n) |
归并排序,Case 2,最经典 |
T(n)=2T(n/2)+1 |
Theta(n) |
满二叉树节点数 |
T(n)=T(n/2)+1 |
Theta(log n) |
二分查找,Case 2边界 |
T(n)=T(n/2)+n |
Theta(n) |
根节点主导 |
T(n)=4T(n/2)+n^2 |
Theta(n^2 log n) |
每层开销相等,本章主角 |
T(n)=4T(n/2)+n^2 log n |
Theta(n^2 log^2 n) |
扩展Case 2,k=1 |
T(n)=3T(n/2)+n^2 |
Theta(n^2) |
Case 3,正则条件成立 |
T(n)=2T(n/4)+sqrt(n) |
Theta(sqrt(n) log n) |
Case 2,sqrt n |
背这张表不是终点,你要能用自己的话解释每一行为什么。解释不出来的,立刻画递归树。
5.2 遇到递推式,先做这三步
我自己的做题顺序是:
- 先看是不是
T(n)=aT(n/b)+f(n)这种分治形态。如果不是,比如T(n)=T(n-1)+n,那就是累加求和,别硬套主定理。T(n)=T(n-1)+n的结果是Theta(n^2),因为从1加到n。 - 如果是标准形态,先把
a、b、n^(log_b a)写出来。 - 比较
f(n)和n^(log_b a):大于、小于、还是临界相等。然后判断能不能套Case 1或Case 3。如果不能严格判断,或者结果不敢确认,画递归树。
这个顺序可以避免最常见的“拿到题就套主定理,套完发现条件不满足”的尴尬。
5.3 几个我亲眼见过的考场失分点
我把这几年帮人改作业时发现的共性错误列在下面:
- 把
a和b弄反。T(n)=4T(n/2)里,a=4是分支数,b=2是规模缩小的倍数。写成n^(log_b a)时是n^(log_2 4),不是n^(log_4 2)。 - 把
n^(log_b a)和f(n)的相等误判成Case 1或者Case 3。相等就是Case 2。 - Case 3忘记验证正则条件。考卷上写一句“且正则条件成立”不亏,但别只在心里觉得。
- 最后结果只写
O不写Theta。题目说“渐近界”时,通常默认要紧确界。 - 忽略“假设n是2的幂”这个前置。严格地说,递推式在任意整数n下可能因为取整产生小差异,但大多数教材默认n是b的幂,不影响最终渐近界。考试时如果题目没写,你可以在解答开头说“不失一般性,假设n/b为整数”,这句话很加分。
5.4 我自己的复习方法:一次改参数,顶十道新题
如果你期末时间紧,我不建议一口气刷几十道递推式。更好的办法是拿一道题反复改参数,观察答案变化。
就拿 T(n)=4T(n/2)+f(n) 做模板:
f(n)=n,结果接近Theta(n^2);f(n)=n^2,结果是Theta(n^2 log n);f(n)=n^2 log n,结果是Theta(n^2 log^2 n);f(n)=n^3,结果是Theta(n^3)。
你会发现,合并开销 f(n) 从“小”到“临界”再到“大”的过程中,复杂度的主导项会从“叶子节点多”转向“每层叠加”,最后转向“根节点主导”。这三个区间正好对应主定理的三种情形。
理解了这一层,习题5.1就不再是“一道要背答案的题”,而是一个判断工具。真正考试的时候,你只要看到递推式,大脑里就能自动浮现出递归树的层数、每层开销、以及最终答案的形状。这个熟练度,比临时抱佛脚背十道题靠谱得多。
最后再分享一个小经验:我每次讲递推式,都会让学生在草稿纸上先花十秒钟画递归树,哪怕题目已经明说用主定理。因为这十秒钟能过滤掉至少一半的粗心错误。等到考试时,你哪怕只在脑子里画一下,也能立刻发现自己的答案是不是少了一个log。
