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

7.7 KiB
Raw Blame History

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

二、分治法三个步骤

  1. 分解(Divide):将原问题分解为若干规模较小的子问题
  2. 求解(Conquer):递归地求解各子问题。若子问题规模足够小,则直接求解
  3. 合并(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) 时间找到第二大的数:

  1. 同时维护最大值和第二大的值
  2. 只有当某个元素大于当前最大值时,才更新第二大值
  3. 一次遍历即可完成
#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。

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)

关联笔记