在计算机科学中,很少有比"分而治之"更自然的解题思路了。面对一个庞杂的问题,先把它切成几个小块,逐个击破,再拼回整体------这种朴素的分割策略,经过严谨的形式化之后,便成了我们所说的分治范式。
一个标准的分治算法由三个步骤构成:
分解 :将原问题划分为k个规模较小的同类子问题,理想情况下每个子问题规模大致相等。
解决 :递归地求解各个子问题。若子问题规模已缩小到某个阈值(基情形),则直接求解。
合并:将子问题的解组合为原问题的解。
形式化地,若输入规模为n,分解与合并的总代价为f(n),子问题个数为a,每个子问题规模为n/b,则时间复杂度满足递归方程T(n)=aT(n/b)+f(n)------这正是上一篇中我们建立并求解的通用结构。
案例一:归并排序------分治的完美教学样本
归并排序可以说是分治策略最直观的体现。分解阶段,它将数组从中间一分为二,这一步只需计算中点,耗时O(1)。解决阶段,递归地对左右两半分别排序,产生两个规模为n/2的子问题。合并阶段,用两个指针遍历两个已排序的子数组,按序填入原数组,耗时O(n)。
由此得到递归式T(n)=2T(n/2)+O(n),套用主定理情形二,结果是Θ(n log n)。值得强调的是,归并排序的这个复杂度在所有基于元素比较的排序算法中已经达到了理论上界------比较排序的下界就是Ω(n log n),这意味着归并排序在渐进意义下是最优的。
但最优是有代价的:合并需要额外的辅助数组,空间复杂度为O(n)。这一点与原地排序的快速排序形成对照,后者的空间代价是递归调用栈的O(log n)。
案例二:Strassen矩阵乘法------打破常规的优化范例
两个n×n矩阵相乘,按定义计算需做n³次标量乘法。长久以来人们默认这就是下界,直到1969年Strassen用一个精巧的分治构造打破了认知。
标准的分治思路是将每个矩阵切成4个n/2×n/2的子块,利用分块乘法的性质递归求解:C₁₁=A₁₁B₁₁+A₁₂B₂₁,依此类推。这会产生8次n/2规模的子问题乘法和4次矩阵加法,递归式T(n)=8T(n/2)+O(n²),解得T(n)=Θ(n³),毫无改善。
Strassen的洞见在于:通过构造7个巧妙的中间矩阵(而非8个),以增加加减法为代价换取少做一次乘法。具体而言,他用7次乘法和18次加减法完成了8次乘法才能完成的任务。递归式变为T(n)=7T(n/2)+O(n²),解得T(n)=Θ(n^{log₂7})≈Θ(n^{2.81})。
这个指数log₂7≈2.807的下探,在理论上意义深远:它第一次证明了矩阵乘法可以比n³更快。此后数十年,Coppersmith-Winograd算法及其变种将指数一路压低到约2.371,逼近许多人猜测的真正下界------2。但必须指出,这些更快的算法由于常数因子巨大,在实际中极少使用。Strassen算法本身在n足够大时(通常阈值在几十到几百之间)确实有实用价值,这也是理论研究反哺工程实践的典型案例。
分治的边界与讨论
并非所有问题都适合分治。判断一个分治算法能否带来实质优化,关键看两点:其一,子问题之间是否真正独立------若存在重叠计算,动态规划可能是更好的选择;其二,合并代价f(n)是否足够小------若合并本身就需要Θ(n²),而子问题规模缩减有限,分治可能得不偿失。
此外,分治算法天然适合并行计算。子问题的独立求解特性意味着它们可以被分配到不同的计算单元上同时执行,这与当代多核架构高度契合,也是分治思想历久弥新的重要原因。
从下一篇开始,我们将进入分治的另一个精彩应用------最大子数组问题,并用一个线性时间的Kadane算法与其正面交锋,看看分治是否总是最优解。