在算法设计里,“分治”(Divide & Conquer)是一类非常经典、也非常实用的思想。许多著名算法,例如归并排序、快速排序、最近点对、快速傅里叶变换(FFT)等,背后都在用分治的套路。
这篇文章不讲某一个具体题目,而是总结分治算法的一般思路,并给出一个可以"套用"的通用伪代码模板,方便写代码时直接代入。
一、什么是 Divide & Conquer?
分治(Divide & Conquer),直译就是"分而治之":把一个规模较大的问题拆成若干个规模更小、形式相同的子问题,分别解决这些子问题后,再把它们的解合并,得到原问题的解。
它的三个关键词可以概括为:
- Divide(分):拆分问题
- Conquer(治):递归求解子问题
- Combine(合):合并子问题的解
只要一个问题满足下面这几个特征,就比较适合用分治:
- 原问题可以拆分成若干个规模更小但结构相同的子问题。
- 子问题之间相对独立,也就是彼此之间关联不太大。
- 有一个明确的"最小规模",当规模足够小时,可以不用再拆,直接解决。
- 能设计出一个高效的合并步骤,把子问题的答案拼回到原问题的答案上。
如果"分"之后合并的代价太大,或者拆出来的子问题彼此强耦合(高度依赖),那么分治就不一定划算,甚至可能比简单的暴力还慢。
二、分治算法的一般套路
从实现角度看,绝大多数分治算法都可以抽象成对一个"问题实例"的递归函数调用。伪代码的逻辑大致都是下面这四步:
判断是否为基本情况(Base Case)
当问题规模足够小时(例如数组长度为 1、区间只剩一个元素,或者规模小于一个常数阈值),不再继续拆分,直接用简单方法求解。
分(Divide):拆分为子问题
把原问题拆成若干个更小的子问题。
常见方式包括:
- 区间对半拆分(如
[l, mid]和[mid+1, r]) - 按中点划分平面或空间
- 把数据分成若干块分别处理
治(Conquer):递归求解子问题
对每个子问题,递归调用同一个分治函数,得到子问题的解。
合(Combine):合并子问题结果
根据具体问题的需求,把子问题的解整合起来。
可能是:
- 简单的拼接(如归并排序的"归并"步骤)
- 在多个子问题解里取一个最佳的(如取最大值 / 最小值)
- 结合子问题解再加上一些"跨区"处理(例如最大子段和、最近点对等)
只要清晰地设计出这四步,一个分治算法的框架就基本成型了。
三、通用伪代码模板
下面给出一个比较通用的伪代码模板,可以理解为一个"可配置版"的分治函数。不同的具体问题,只是填入了不同的 Split、DirectSolve、Combine 等函数而已。
// 解决某个抽象的"problem"
function DivideAndConquer(problem):
// 1. 基本情况:问题足够小,直接解决
if size(problem) <= THRESHOLD then
return DirectSolve(problem)
// 2. 分:把问题拆成若干个子问题
subProblems = Split(problem) // 例如拆成 P1, P2, ..., Pk
// 3. 治:递归求解每个子问题
subSolutions = empty list
for each subProblem in subProblems do
subSolutions.append( DivideAndConquer(subProblem) )
// 4. 合:把子问题的解合并为原问题的解
result = Combine(subSolutions)
return result
其中每一个"占位符"都对应实际代码里要实现的部分:
THRESHOLD
- 表示"多小的规模算是基本情况"。
- 比如数组长度为 1、2,或 n <= 16 等。
- 这个取值有时会影响常数因子,甚至会影响实际运行时间,可以视具体情况微调。
DirectSolve(problem)
- 对小规模问题,直接用简单方法求解。
- 典型例子:
- 归并排序里,长度为 1 的数组不用再排,直接有序。
- "最大子段和"问题中,区间只剩一个元素时,答案就是这个元素本身。
Split(problem)
- 把一个问题拆分成若干子问题。
- 例如:
- 数组或区间问题里,按中点一分为二。
- 几何问题里,按一条直线把点集分成左右两半。
- 字符串问题里,按中点或某个关键下标拆成两段。
Combine(subSolutions)
- 根据实际问题的定义来组合子解。
- 例子:
- 归并排序:将两个有序子数组"归并"为一个更长的有序数组。
- 最大子段和:在左右子区间答案中取最大值,同时考虑"跨越中点"的子段和。
- 最近点对:比较左半、右半最优解和跨分割线的候选解,取最小的距离。
可以这样理解:
具体问题的"精髓",就在于如何设计 Split 和 Combine;
而基本框架的递归结构基本不变。
四、和递归、动态规划的关系
分治的实现几乎总是用递归,因此常被简单地等同于"递归算法",但两者不完全等价:
- 分治更强调:
- 问题被拆分成相互独立的子问题;
- 子问题规模更小、形式相同;
- 有一个合并过程。
- 动态规划则强调:
- 子问题往往互相重叠;
- 通过记忆化或表格,自底向上消除重复计算。
有一句常见的"演化关系"可以帮助记忆:
暴力穷举 → 分治(剪枝、组织结构) → 记忆化搜索 → 动态规划
很多题目一开始可以先按照分治思路写出正确但可能较慢的做法,再视情况引入记忆化或改写为 DP,从而去掉重复计算,进一步加速。
五、如何从零开始设计一个分治算法?
面对一个新问题,可以按以下步骤尝试用分治来建模:
- 找"规模"的定义
- 比如是"数组长度 n"、“区间长度 r - l + 1”、"点的数量"等。
- 思考"如果规模更小、已经有解了,我能不能把它们拼成大问题的解?"
- 这一步是在构思 Combine。
- 再反过来问:“为了让上一步可行,我应该如何拆分原问题?”
- 这一步是在构思 Split。
- 最后补齐:
- 定义基本情况(什么时候不再拆)。
- 对小规模问题的直接解法。
- 考虑时间复杂度(可以写出递推式,例如 T(n)=aT(n/b)+f(n)T(n) = aT(n/b) + f(n)T(n)=aT(n/b)+f(n))。
在刷题时,可以尝试把常见的分治例题(如归并排序、快速排序、最大子段和、最近点对等)都按照上面的通用模板重新"翻译"一遍,会对 Divide & Conquer 的套路有更深的体感。
通用思路与伪代码模板&spm=1001.2101.3001.5002&articleId=155795408&d=1&t=3&u=a685e475f4864fa987f5ae7f5464df42)
3561

被折叠的 条评论
为什么被折叠?



