第48章 分治算法

第48章 分治算法

分治算法(Divide and Conquer)核心思想为分而治之:将一个规模大、难以直接求解的原问题,拆分为若干结构相同、规模更小的独立子问题;递归求解所有子问题后,再将子问题的解合并,得到原问题的最终答案。本章对应 GESP C++ 五级知识块 7:分治算法,要求能用归并排序和快速排序对数组排序。

归并排序:分治拆分 + 合并

48.1 基本概念

48.1.1 定义

分治三步核心流程:

  1. 分解(Divide):把原问题均匀拆分为多个独立子问题;
  2. 解决(Conquer):子问题规模足够小时直接求解,否则递归分治;
  3. 合并(Combine):将各个子问题的结果整合,得到原问题解。

48.1.2 分治与递归的关系

分治大多依靠递归实现,但二者不等价:

  • 分治一定包含分解 + 合并两个特有步骤;
  • 单纯递归(如朴素斐波那契)无合并逻辑,不属于分治。

48.2 分治通用代码框架

返回类型 divideConquer(问题参数) {
// 基本情况:规模足够小,直接返回解
if (问题规模足够小) {
return 直接求解结果;
}
// 1. 分解:拆分出多个子问题
子问题1, 子问题2... = 分解原问题;
// 2. 递归求解所有子问题
解1 = divideConquer(子问题1参数);
解2 = divideConquer(子问题2参数);
// 3. 合并子问题答案
原问题解 = 合并(解1, 解2...);
return 原问题解;
}

48.3 经典分治算法示例

48.3.1 归并排序(Merge Sort)

  1. 分解:将数组从中间切分为左右两个等长子数组;
  2. 解决:递归分别排序左右子数组;
  3. 合并:双指针合并两个有序数组,生成整体有序数组。
#include <iostream>
using namespace std;

// 合并 [left,mid] 和 [mid+1,right] 两个有序区间
void merge(int arr[], int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
int* L = new int[n1];
int* R = new int[n2];
// 复制数据到临时数组
for (int i = 0; i < n1; i++) L[i] = arr[left + i];
for (int j = 0; j < n2; j++) R[j] = arr[mid + 1 + j];
int i = 0, j = 0, k = left;
// 双指针合并
while (i < n1 && j < n2) {
if (L[i] <= R[j]) arr[k++] = L[i++];
else arr[k++] = R[j++];
}
// 复制剩余元素
while (i < n1) arr[k++] = L[i++];
while (j < n2) arr[k++] = R[j++];
delete[] L;
delete[] R;
}

// 归并排序主函数
void mergeSort(int arr[], int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
mergeSort(arr, left, mid); // 左半分治
mergeSort(arr, mid + 1, right); // 右半分治
merge(arr, left, mid, right); // 合并有序区间
}
}

48.3.2 快速排序(Quick Sort)

  1. 分解:选取基准 pivot,将数组划分成「小于基准」「大于基准」两部分;
  2. 解决:递归排序左右两部分;
  3. 合并:无需额外合并,划分后数组天然有序。
#include <iostream>
using namespace std;

// 划分函数,返回基准最终下标
int partition(int arr[], int low, int high) {
int pivot = arr[high]; // 选最右侧为基准
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
swap(arr[i], arr[j]);
}
}
swap(arr[i + 1], arr[high]);
return i + 1;
}

void quickSort(int arr[], int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1); // 基准左侧
quickSort(arr, pi + 1, high); // 基准右侧
}
}

48.3.3 最大子数组和(分治解法)

问题:求数组中连续一段数字的最大和。 分治思路:

  1. 分解:数组拆分为左、右两半;
  2. 解决:递归求左半最大、右半最大;
  3. 合并:计算跨越中点的最大子数组,三者取最大值。
#include <climits>
// 求跨越中点的最大和
int maxCrossingSum(int arr[], int left, int mid, int right) {
int sum = 0, leftMax = INT_MIN;
for (int i = mid; i >= left; i--) {
sum += arr[i];
if (sum > leftMax) leftMax = sum;
}
sum = 0;
int rightMax = INT_MIN;
for (int i = mid + 1; i <= right; i++) {
sum += arr[i];
if (sum > rightMax) rightMax = sum;
}
return leftMax + rightMax;
}

int maxSubArraySum(int arr[], int left, int right) {
// 基本情况:区间仅一个元素
if (left == right) return arr[left];
int mid = left + (right - left) / 2;
int leftSum = maxSubArraySum(arr, left, mid);
int rightSum = maxSubArraySum(arr, mid + 1, right);
int crossSum = maxCrossingSum(arr, left, mid, right);
// 三者取最大
return max(max(leftSum, rightSum), crossSum);
}

48.4 分治时间复杂度(主定理)

分治递推标准形式:

T(n)=a⋅T(nb)+f(n)T(n) = a \cdot T\left(\frac{n}{b}\right) + f(n)

  • aa:拆分后的子问题数量;
  • bb:原问题缩小倍数;
  • f(n)f(n):分解 + 合并操作的时间复杂度。

主定理三种情况:

  1. 若 f(n)=O(nc), c<log⁡baf(n) = O(n^c),\ c < \log_b a,则 T(n)=O(nlog⁡ba)T(n) = O(n^{\log_b a})
  2. 若 f(n)=O(nc), c=log⁡baf(n) = O(n^c),\ c = \log_b a,则 T(n)=O(nclog⁡n)T(n) = O(n^c \log n)
  3. 若 f(n)=O(nc), c>log⁡baf(n) = O(n^c),\ c > \log_b a,则 T(n)=O(f(n))T(n) = O(f(n))

典型分治复杂度:

  • 归并排序:T(n)=2T(n/2)+O(n)  ⟹  O(nlog⁡n)T(n) = 2T(n/2) + O(n) \implies O(n\log n)
  • 快速排序平均:O(nlog⁡n)O(n\log n),最坏有序数组 O(n2)O(n^2)
  • 二分查找:T(n)=1⋅T(n/2)+O(1)  ⟹  O(log⁡n)T(n) = 1 \cdot T(n/2) + O(1) \implies O(\log n)

48.5 分治算法优缺点

优点

  1. 大规模问题拆分为小规模,逻辑清晰;
  2. 子问题相互独立,可并行计算;
  3. 排序、查找等场景效率优秀。

缺点

  1. 递归调用带来栈开销;
  2. 合并步骤复杂时性能损耗大;
  3. 极小数据规模下,暴力循环比分治更快。

48.6 分治、贪心、动态规划对比

算法核心逻辑关键特征典型例题
分治拆分子问题,合并结果子问题独立,必须合并归并排序、快速排序
贪心每一步局部最优无回溯、不存子解活动选择、Kruskal
动态规划存重叠子问题最优解子问题重叠,有后效性01 背包、LCS

48.7 选择题练习

  1. 分治算法的三个核心步骤依次是: A. 合并、分解、解决 B. 分解、解决、合并 C. 解决、合并、分解 D. 分解、合并、解决

    查看答案

    答案:B。分治三步:分解(Divide)→ 解决(Conquer)→ 合并(Combine)。

  2. 归并排序的时间复杂度是: A. O(n)O(n) B. O(nlog⁡n)O(n \log n) C. O(n2)O(n^2) D. O(log⁡n)O(\log n)

    查看答案

    答案:B。归并排序每层合并 O(n)O(n),共 log⁡2n\log_2 n 层,稳定 O(nlog⁡n)O(n\log n)。

  3. 快速排序最坏情况(如输入已有序且基准取端点)的时间复杂度是: A. O(n)O(n) B. O(nlog⁡n)O(n \log n) C. O(n2)O(n^2) D. O(log⁡n)O(\log n)

    查看答案

    答案:C。最坏每次划分极不平衡(一边 n−1n-1、一边 0),退化为 O(n2)O(n^2)。

  4. 下列算法中,不属于分治思想的是: A. 归并排序 B. 快速排序 C. 二分查找 D. 朴素斐波那契(fib(n)=fib(n-1)+fib(n-2))

    查看答案

    答案:D。朴素斐波那契只有递归分解、没有"合并"步骤,且子问题重叠,不属于分治;其余三者均为分治。

  5. 根据主定理,递推式 T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n) 的解为: A. O(n)O(n) B. O(nlog⁡n)O(n \log n) C. O(n2)O(n^2) D. O(log⁡n)O(\log n)

    查看答案

    答案:B。a=2,b=2,c=1a=2, b=2, c=1,log⁡ba=1=c\log_b a = 1 = c,属第二种情况,得 O(nlog⁡n)O(n \log n)。

SShowball OI知识手册