Files
final-exam/算法设计与分析/复习文档/分治法.md
T

232 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags:
- 算法设计与分析
- 复习
- 分治法
create time: 2026-06-10 11:11
---
# 分治法
## 概述
分治法(Divide and Conquer)是算法设计中最基础、最重要的策略之一。其核心思想是"大事化小":将一个难以直接解决的大问题,分割成若干规模较小的子问题,递归地解决这些子问题,然后将子问题的解合并得到原问题的解。本文将从基本思想出发,讲解分治法的三步骤、适用条件以及多个经典应用。
## 正文
### 一、分治法基本思想
分治法将一个大规模问题分解为 **k 个规模较小的子问题**,这些子问题互相独立且与原问题形式相同。递归地求解这些子问题,然后将子问题的解合并,得到原问题的解。
```mermaid
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
```
### 二、分治法三个步骤
1. **分解(Divide)**:将原问题分解为若干规模较小的子问题
2. **求解(Conquer)**:递归地求解各子问题。若子问题规模足够小,则直接求解
3. **合并(Merge)**:将子问题的解合并为原问题的解
### 三、适用条件
分治法适用需要同时满足以下条件:
- 问题可以**分解**为若干规模较小的子问题
- 子问题**相互独立**(没有重叠子问题)
- 子问题的解可以**合并**为原问题的解
> [!warning] 分治法与动态规划的关键区别
> 分治法要求子问题**相互独立、没有重叠**。如果子问题之间有重叠(即同一个子问题被多次计算),则应该使用动态规划。这是两者最本质的区别。
> [!question] 分治法和动态规划的本质区别是什么?
> 核心区别在于子问题是否重叠。分治法的子问题互相独立,每次求解不会重复;动态规划的子问题有重叠,所以需要把子问题的解存储起来避免重复计算。例如归并排序的子问题不重叠(左半和右半不交叉),适合分治;而Fibonacci的子问题严重重叠,适合动态规划。
### 四、经典应用
#### 4.1 归并排序(Merge Sort)
归并排序是分治法最经典的应用之一,时间复杂度 **O(n logn)**。
```cpp
#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**
```cpp
#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)** 时间找到第二大的数:
1. 同时维护最大值和第二大的值
2. 只有当某个元素**大于当前最大值**时,才更新第二大值
3. 一次遍历即可完成
```cpp
#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 枚硬币,其中一枚是假币(重量不同),用天平称量找出假币。
**分治策略**:
1. 将硬币分为**三组**(尽量均分)
2. 用天平称量其中两组
- 若平衡:假币在第三组
- 若不平衡:假币在较轻(或较重)的一组
3. 递归处理包含假币的那一组
**时间复杂度**:每次排除 2/3,总共只需 **O(log₃n)** 次称量。
> [!tip] 为什么是三路划分而不是二路?
> 天平有三种结果(左重、右重、平衡),对应三路划分。利用三种结果信息,每次排除 2/3 的可能性,效率远高于二分法的 1/2。
```mermaid
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) |
## 关联笔记
- [[算法设计与分析/试题册/index|试题册索引]]