本节摘要:分治法的三段式是分解、解决、合并——把规模 n 的问题切成 a 个规模 n/b 的同型子问题,递归解决后合并结果。复杂度由子问题个数 a、缩小比例 b、合并成本三要素决定,主定理给出三个量级档位。本节用归并思想数逆序对、用平方取幂演示"每轮减半"的威力,并划出分治的适用边界(子问题必须独立,否则请看下一节动态规划)。
分治法的三步你早已见过两次:快速排序先分区(分解出两个独立子数组),各自递归(解决),无需合并;归并排序对半切(分解免费),递归到单元素(解决天然完成),双指针合并(合并是主戏)。抽象成模板:
三步里藏着一个生死条件:子问题之间必须互相独立。切出来的两半各算各的、互不引用对方的中间结果,合并时只需要最终答案。一旦子问题重叠(比如朴素斐波那契的 fib(n-1) 与 fib(n-2) 都要算 fib(n-3)),分治就把同一份账翻来覆去地算——那是下一节动态规划的地盘。
复杂度账记成 T(n) = a·T(n/b) + f(n):a 个子问题、每个规模缩到 n/b、合并花 f(n)。主定理用三个量级档位给答案,不需要背完整定理,记三个代表就够:
| 局面 | 直觉 | 量级 | 代表 |
|---|---|---|---|
| 每层总工作量随深度递减(叶子侧重) | 大量微小子问题 | O(n^(log_b a)) | 快速选择、Strassen 矩阵乘 |
| 每层工作量持平 | 均匀分摊 | O(f(n)·log n) | 归并排序 O(n log n)、二分 O(log n) |
| 每层工作量随深度递增(根部侧重) | 根部合并最贵 | O(f(n)) | 少见,如 T = 2T(n/2)+n² 即 O(n²) |
二分查找是 a=1、b=2、f=O(1) 的极简分治:一层只留一个子问题,每层只花常数——O(log n) 的另一副面孔。
逆序对:i 小于 j 但 a[i] 大于 a[j] 的配对,是 5.1 节"乱度"的硬指标。暴力数要 O(n²);归并排序的合并步骤里,每次从右段取元素时,左段剩余的所有元素都比它大——一次打包计数:
# 逆序对计数:分治三段式 + 暴力对拍 def count_inversions_dc(a): count = [0] def msort(seg): if len(seg) <= 1: return seg mid = len(seg) // 2 left, right = msort(seg[:mid]), msort(seg[mid:]) # 分解 + 解决 out, i, j = [], 0, 0 while i < len(left) and j < len(right): # 合并:顺便计数 if left[i] <= right[j]: out.append(left[i]); i += 1 else: count[0] += len(left) - i # 右段胜出:左段剩余全体与它构成逆序对 out.append(right[j]); j += 1 out.extend(left[i:]); out.extend(right[j:]) return out msort(list(a)) return count[0] def count_inversions_brute(a): # O(n²) 对拍用 c = 0 for i in range(len(a)): for j in range(i + 1, len(a)): if a[i] > a[j]: c += 1 return c data = [5, 2, 4, 1, 3] print("分治计数:", count_inversions_dc(data), ";暴力对拍:", count_inversions_brute(data)) # 输出:分治计数: 7 ;暴力对拍: 7 # 七对:5 压 2、4、1、3;2 压 1;4 压 1、3——逐对可验证
这正是"分治顺手做统计"的通用模式:合并时左右两段各自有序,跨段的统计量可以用段长直接结算,不必逐对枚举。归并求逆序对之外,平面最近点对、大整数乘法(Karatsuba)走的都是同一条路。
计算 x 的 n 次方,朴素做法连乘 n-1 次;分治的视角是 xⁿ 等于 x 的 n/2 次方的平方(n 偶数)或再乘一个 x(n 奇数)——每轮规模减半:
# 快速幂:递归减半 vs 朴素连乘,乘法次数计数 def naive_pow(x, n): r, mults = 1, 0 for _ in range(n): r *= x mults += 1 return r, mults def fast_pow(x, n): mults = [0] def rec(x, n): if n == 0: return 1 half = rec(x, n // 2) # 只算一次半规模 mults[0] += 1 # 平方计一次 r = half * half if n % 2 == 1: r *= x mults[0] += 1 # 补乘计一次 return r return rec(x, n), mults[0] v1, m1 = naive_pow(3, 100) v2, m2 = fast_pow(3, 100) print("结果一致:", v1 == v2) print(f"朴素连乘 {m1} 次乘法;快速幂 {m2} 次乘法") # 输出: # 结果一致: True # 朴素连乘 99 次乘法;快速幂 10 次乘法 # 指数每轮减半:乘法次数介于 log2(n) 与 2·log2(n) 之间,本例 10 次; # 指数越大差距越悬殊——n 为一百万时,朴素百万次对快速幂约四十次
快速幂是密码学的地基之一(模幂运算),也是"减治法"(每轮只留一个子问题)的代表——与二分查找同一族。
⚠️ 常见坑:只看分解的层数不看合并的成本。切得再碎,合并若要 O(n²),总量仍是 O(n²)。动笔前把三要素 a、b、f 都摆上桌,查档位表再下手。
💡 关键直觉:分治的本质优势是"对数深度的复用"——每层把问题规模按比例压,log 轮就到底。它不是让每层变快,而是让层数变成对数。
典型事故:用分治写斐波那契。fib(30) 触发两百多万次调用(1.3 节已复现),因为 fib(28) 被两条递归链各算一遍——子问题不独立,分治的复杂度账 T(n) = 2T(n-1) + O(1) 直接爆炸成指数。判断口诀:画出前两层的递归调用树,若同一子问题出现两次以上,分治就该退场,记忆化或填表(下一节)登场。
子问题一旦重叠,账本就该登场。下一节:动态规划,用一张表终结重复计算。