vault backup: 2026-06-10 11:17:10
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
---
|
||||
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)**。
|
||||
|
||||
```plaintext
|
||||
function mergeSort(A, left, right):
|
||||
if left < right:
|
||||
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**
|
||||
|
||||
```plaintext
|
||||
function findMaxMin(A, n):
|
||||
// 初始化
|
||||
if n is odd:
|
||||
maxVal = minVal = A[1]
|
||||
start = 2
|
||||
else:
|
||||
if A[1] < A[2]:
|
||||
minVal = A[1], maxVal = A[2]
|
||||
else:
|
||||
minVal = A[2], maxVal = A[1]
|
||||
start = 3
|
||||
|
||||
// 两两比较
|
||||
for i = start to n step 2:
|
||||
if A[i] < A[i+1]:
|
||||
smaller, larger = A[i], A[i+1]
|
||||
else:
|
||||
smaller, larger = A[i+1], 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. 一次遍历即可完成
|
||||
|
||||
```plaintext
|
||||
function findSecondMax(A, n):
|
||||
maxVal = A[1]
|
||||
secondMax = -infinity
|
||||
for i = 2 to n:
|
||||
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|试题册索引]]
|
||||
Reference in New Issue
Block a user