【算法图解】分治思想:大问题拆小问题

分治不是“拆开写递归”这么简单,它是一种把复杂度压下来的结构化思维。本文用归并排序、快速排序、二分查找与“最大子数组和”等经典例子,把分治的三步走、递推式与工程化落地讲清楚。

14 分钟阅读
小明

【算法图解】分治思想:大问题拆小问题

分治(Divide and Conquer)是算法世界最强的“降维打击”之一。

它的直觉非常朴素:

把一个大问题拆成若干个相同结构的小问题,小问题解决后再合并回去。

但真正掌握分治,你需要的不只是口号,而是:

  • 什么时候适合分治?什么时候是“硬套递归”?
  • 拆到什么粒度才对?
  • 合并成本如何影响复杂度?
  • 如何写出不爆栈/不退化的实现?

这篇文章把分治讲成一套可复用的方法。


1. 分治的标准三步

任何分治题,你都可以用这三步写解法:

  1. Divide(分):把规模 $n$ 的问题拆成若干个规模更小的子问题
  2. Conquer(治):递归解决子问题(直到 base case)
  3. Combine(合):把子问题答案合并成原问题答案

你也可以把它记成一个“面试可复述”的模板:

  • 我先定义子问题与递归边界。
  • 然后递归求解左右子问题。
  • 最后在合并阶段把两边结果组合成最终答案。

2. 分治什么时候有效?关键看“合并成本”

分治不是免费的。

它通常会带来:

  • 递归开销(函数调用、栈深度)
  • 合并开销(把子结果拼起来)

决定复杂度的核心是递推式:

$$T(n) = a,T(n/b) + f(n)$$

  • $a$:子问题个数
  • $b$:每个子问题规模缩小倍数
  • $f(n)$:合并阶段做了多少工作

直觉:

  • 如果 $f(n)$ 很小(比如 $O(1)$),分治往往能做到 $O(\log n)$ 或 $O(n)$
  • 如果 $f(n)$ 很大(比如 $O(n)$),就会变成经典的 $O(n\log n)$
  • 如果 $f(n)$ 甚至更大,分治可能得不偿失

3. 例子一:二分查找(分治的最简形态)

二分是分治的“入门级冠军”。

  • Divide:每次把区间砍半
  • Conquer:递归到左半或右半
  • Combine:不需要合并(直接返回答案)

复杂度:

  • $T(n)=T(n/2)+O(1)$
  • 所以时间 $O(\log n)$,栈空间 $O(\log n)$(递归写法)

工程建议:二分尽量用迭代写,避免栈。


4. 例子二:归并排序(合并阶段是关键)

归并排序完整体现了分治:

  • Divide:拆成左右两半
  • Conquer:递归排序
  • Combine:合并两个有序数组

合并成本是 $O(n)$,递推式:

  • $T(n)=2T(n/2)+O(n)$
  • 时间 $O(n\log n)$
  • 空间通常 $O(n)$(需要辅助数组)

分治能把排序做到 $O(n\log n)$ 的关键原因:

  • 每层合并总成本是 $O(n)$
  • 层数是 $\log n$

5. 例子三:快速排序(分治 + 随机化避免退化)

快排也是分治,但“合并阶段”很特别:

  • Divide:按 pivot 分区,把小的放左边,大的放右边
  • Conquer:递归排左右两边
  • Combine:不需要显式合并(分区已经把 pivot 放对位置)

平均时间 $O(n\log n)$,但最坏 $O(n^2)$。

工程上避免退化的常用手段:

  • 随机 pivot
  • 三数取中
  • 小数组改用插排
  • 递归深度过深时切换到堆排(introsort 思路)

6. 例子四:最大子数组和(分治解法如何“写对合并”)

问题:给定数组,求连续子数组的最大和。

分治思路:

  • Divide:中点拆成左右
  • Conquer:求左边最大子数组、右边最大子数组
  • Combine:还需要求“跨中点”的最大子数组(左边最大后缀 + 右边最大前缀)

最终答案是三者最大:

  • leftMax
  • rightMax
  • crossMax

这里的关键就是:

合并阶段要能在 $O(n)$ 或更低成本拿到 crossMax。

它的复杂度是 $O(n\log n)$;但这个题的最优解是 Kadane(动态规划)$O(n)$。

这也提醒你:

  • 分治不一定最优
  • 但它经常是“结构清晰、容易正确”的强力方案

7. 什么时候不该用分治?

常见反例:

  • 合并阶段接近 $O(n^2)$(例如需要两两比较、全量重建)
  • 子问题之间强耦合,无法独立求解
  • 可以用线性扫描/贪心/DP 一次解决

面试里你可以补一句体现判断力:

  • “我能用分治写 $O(n\log n)$,但这里其实有 $O(n)$ 的 DP/贪心更优。”

8. 实战落地:写分治时的 4 个检查点

  1. base case 是否覆盖空/单元素?
  2. 子问题是否严格变小?(避免无限递归)
  3. 合并逻辑是否只依赖子结果?
  4. 递归深度是否可能爆栈?(必要时迭代/显式栈)

总结

  • 分治的三步:分、治、合。
  • 决定复杂度的关键是合并成本 $f(n)$。
  • 归并/快排/二分都是分治范式的经典代表。
  • 会分治,不是会背,而是能写出正确的递推与合并。