7.7 KiB
tags, create time
| tags | create time | |||
|---|---|---|---|---|
|
2026-06-10 11:11 |
分治法
概述
分治法(Divide and Conquer)是算法设计中最基础、最重要的策略之一。其核心思想是"大事化小":将一个难以直接解决的大问题,分割成若干规模较小的子问题,递归地解决这些子问题,然后将子问题的解合并得到原问题的解。本文将从基本思想出发,讲解分治法的三步骤、适用条件以及多个经典应用。
正文
一、分治法基本思想
分治法将一个大规模问题分解为 k 个规模较小的子问题,这些子问题互相独立且与原问题形式相同。递归地求解这些子问题,然后将子问题的解合并,得到原问题的解。
flowchart TD
Problem["Original Problem P"]
Divide["Divide: Split into subproblems"]
Sub1["Subproblem P1"]
Sub2["Subpattern P2"]
Sub3["Subproblem Pk"]
Conquer["Conquer: Solve each subproblem recursively"]
Merge["Merge: Combine subproblem solutions"]
Solution["Solution to P"]
Problem --> Divide
Divide --> Sub1
Divide --> Sub2
Divide --> Sub3
Sub1 --> Conquer
Sub2 --> Conquer
Sub3 --> Conquer
Conquer --> Merge
Merge --> Solution
二、分治法三个步骤
- 分解(Divide):将原问题分解为若干规模较小的子问题
- 求解(Conquer):递归地求解各子问题。若子问题规模足够小,则直接求解
- 合并(Merge):将子问题的解合并为原问题的解
三、适用条件
分治法适用需要同时满足以下条件:
- 问题可以分解为若干规模较小的子问题
- 子问题相互独立(没有重叠子问题)
- 子问题的解可以合并为原问题的解
[!warning] 分治法与动态规划的关键区别 分治法要求子问题相互独立、没有重叠。如果子问题之间有重叠(即同一个子问题被多次计算),则应该使用动态规划。这是两者最本质的区别。
[!question] 分治法和动态规划的本质区别是什么? 核心区别在于子问题是否重叠。分治法的子问题互相独立,每次求解不会重复;动态规划的子问题有重叠,所以需要把子问题的解存储起来避免重复计算。例如归并排序的子问题不重叠(左半和右半不交叉),适合分治;而Fibonacci的子问题严重重叠,适合动态规划。
四、经典应用
4.1 归并排序(Merge Sort)
归并排序是分治法最经典的应用之一,时间复杂度 O(n logn)。
#include <vector>
using namespace std;
// merge函数省略,此处仅展示归并排序的分治骨架
// void merge(vector<int>& A, int left, int mid, int right) { ... }
void mergeSort(vector<int>& A, int left, int right) {
if (left < right) {
int mid = (left + right) / 2;
mergeSort(A, left, mid); // 分解:排序左半部分
mergeSort(A, mid + 1, right); // 分解:排序右半部分
// merge(A, left, mid, right); // 合并:合并两个有序子数组
}
}
- 分解:将数组从中间分为两半
- 求解:递归排序左右两半
- 合并:将两个有序子数组合并为一个有序数组
4.2 快速排序(Quick Sort)
- 平均时间复杂度 O(n logn),最坏情况 O(n²)
- 最坏情况发生在每次 pivot 选择都不理想(如已排序数组选第一个元素为 pivot)
[!question] 快速排序最坏情况下时间复杂度为什么是O(n²)? 当每次选择的 pivot 都是最大或最小元素时,划分极度不平衡,一边有 n-1 个元素,另一边为 0 个。递归方程变为 T(n) = T(n-1) + O(n),解为 O(n²)。
4.3 同时求最大值和最小值
朴素方法:分别查找最大值和最小值,需要 2(n-1) 次比较。
分治改进:将数组两两分组,每组内先比较,再分别维护最大值和最小值。
- 每组2个元素:组内比较1次,再与当前最大、最小各比较1次
- 比较次数从 2(n-1) 降至约 3n/2
#include <vector>
#include <utility>
using namespace std;
// 同时求最大值和最小值,比较次数约 3n/2
pair<int, int> findMaxMin(vector<int>& A, int n) {
int maxVal, minVal;
int start;
// 初始化:根据元素个数的奇偶性决定初始值
if (n % 2 == 1) {
maxVal = minVal = A[0];
start = 1;
} else {
if (A[0] < A[1]) {
minVal = A[0]; maxVal = A[1];
} else {
minVal = A[1]; maxVal = A[0];
}
start = 2;
}
// 两两比较:每组内比较1次,再分别与全局最大最小比较
for (int i = start; i < n; i += 2) {
int smaller, larger;
if (A[i] < A[i + 1]) {
smaller = A[i]; larger = A[i + 1];
} else {
smaller = A[i + 1]; larger = A[i];
}
if (smaller < minVal) minVal = smaller;
if (larger > maxVal) maxVal = larger;
}
return {maxVal, minVal};
}
解释:每次循环处理两个元素,组内比较1次确定大小,再分别与全局最大最小比较最多2次,所以每组最多3次比较。总比较次数约为 3(n-1)/2。
4.4 找第二大/第二小的数
用分治法可以 O(n) 时间找到第二大的数:
- 同时维护最大值和第二大的值
- 只有当某个元素大于当前最大值时,才更新第二大值
- 一次遍历即可完成
#include <vector>
#include <climits>
using namespace std;
// 一次遍历找第二大的数,时间O(n)
int findSecondMax(vector<int>& A, int n) {
int maxVal = A[0];
int secondMax = INT_MIN;
for (int i = 1; i < n; i++) {
if (A[i] > maxVal) {
secondMax = maxVal; // 原来的最大变为第二
maxVal = A[i]; // 更新最大
} else if (A[i] > secondMax) {
secondMax = A[i]; // 更新第二
}
}
return secondMax;
}
4.5 伪币问题
问题:有 n 枚硬币,其中一枚是假币(重量不同),用天平称量找出假币。
分治策略:
- 将硬币分为三组(尽量均分)
- 用天平称量其中两组
- 若平衡:假币在第三组
- 若不平衡:假币在较轻(或较重)的一组
- 递归处理包含假币的那一组
时间复杂度:每次排除 2/3,总共只需 O(log₃n) 次称量。
[!tip] 为什么是三路划分而不是二路? 天平有三种结果(左重、右重、平衡),对应三路划分。利用三种结果信息,每次排除 2/3 的可能性,效率远高于二分法的 1/2。
flowchart TD
Start["n coins, find fake"]
Split["Divide into 3 groups: A, B, C"]
Weigh["Weigh group A vs group B"]
Balance["A == B: fake in C"]
Left["A < B: fake in A (lighter)"]
Right["A > B: fake in B (lighter)"]
Recurse["Recurse on the group with fake coin"]
Found["Found the fake coin"]
Start --> Split
Split --> Weigh
Weigh --> Balance
Weigh --> Left
Weigh --> Right
Balance --> Recurse
Left --> Recurse
Right --> Recurse
Recurse --> |"n > 1"| Weigh
Recurse --> |"n = 1"| Found
五、分治法的递归方程求解
分治法的一般递归方程:
T(n) = aT(n/b) + f(n)
其中 a 是子问题个数,n/b 是子问题规模,f(n) 是分解与合并的代价。
| 算法 | a | b | f(n) | 复杂度 |
|---|---|---|---|---|
| 二分查找 | 1 | 2 | O(1) | O(logn) |
| 归并排序 | 2 | 2 | O(n) | O(n logn) |
| 快速排序(平均) | 2 | 2 | O(n) | O(n logn) |
| 伪币问题 | 1 | 3 | O(1) | O(log₃n) |