--- 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 using namespace std; // merge函数省略,此处仅展示归并排序的分治骨架 // void merge(vector& A, int left, int mid, int right) { ... } void mergeSort(vector& 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 #include using namespace std; // 同时求最大值和最小值,比较次数约 3n/2 pair findMaxMin(vector& 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 #include using namespace std; // 一次遍历找第二大的数,时间O(n) int findSecondMax(vector& 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|试题册索引]]