--- 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|试题册索引]]