分治算法(Divide and Conquer)
分治算法是一种重要的算法设计范式,其核心思想是将一个复杂的问题分解为多个规模较小的相同问题,逐步解决这些较小的问题,最后将这些问题的解合并成原问题的解。分治算法通常包含三个步骤:
- 分解(Divide):将问题分解成几个子问题,子问题应该是原问题的规模较小的版本。
- 解决(Conquer):递归地解决每个子问题。如果子问题足够小,则直接解决。
- 合并(Combine):将子问题的解合并成原问题的解。
应用
分治算法在很多经典问题中都得到了应用,以下是几个典型的应用:
- 归并排序(Merge Sort)
- 快速排序(Quick Sort)
- 最大子数组和问题(Maximum Subarray Problem)
- 大数相乘(Karatsuba Algorithm)
- 求逆序对的个数
归并排序(Merge Sort)
归并排序是一个典型的分治算法应用。其时间复杂度是 O(nlogn),比冒泡排序和插入排序等算法更高效。
归并排序的步骤:
- 分解:将数组分成两个子数组,递归排序这两个子数组。
- 合并:将两个已经排序的子数组合并成一个有序的数组。
代码实现(c++)
#include <iostream>
#include <vector>
using namespace std;
// 合并两个已排序的子数组
void merge(vector<int>& arr, int left, int mid, int right) {
int n1 = mid - left + 1; // 左子数组的大小
int n2 = right - mid; // 右子数组的大小
vector<int> leftArr(n1), rightArr(n2);
// 将数据拷贝到临时数组
for (int i = 0; i < n1; ++i) {
leftArr[i] = arr[left + i];
}
for (int j = 0; j < n2; ++j) {
rightArr[j] = arr[mid + 1 + j];
}
int i = 0, j = 0, k = left;
// 合并两个子数组
while (i < n1 && j < n2) {
if (leftArr[i] <= rightArr[j]) {
arr[k++] = leftArr[i++];
} else {
arr[k++] = rightArr[j++];
}
}
// 将剩余的元素拷贝到原数组
while (i < n1) {
arr[k++] = leftArr[i++];
}
while (j < n2) {
arr[k++] = rightArr[j++];
}
}
// 归并排序主函数
void mergeSort(vector<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); // 合并
}
}
int main() {
vector<int> arr = {38, 27, 43, 3, 9, 82, 10};
cout << "原数组: ";
for (int num : arr) {
cout << num << " ";
}
cout << endl;
mergeSort(arr, 0, arr.size() - 1); // 排序
cout << "排序后数组: ";
for (int num : arr) {
cout << num << " ";
}
cout << endl;
return 0;
}
代码说明
mergeSort
函数:递归地分解数组,将其拆分成小的子数组,直到每个子数组只有一个元素或为空。merge
函数:用于合并两个已经排序的子数组,保证合并后形成一个新的排序数组。- 在
main
函数中,定义了一个整数数组arr
,然后调用mergeSort
对数组进行排序。
复杂度分析
-
时间复杂度:归并排序的时间复杂度是 O(nlogn),其中 n是数组的大小。每次分解数组的过程需要 O(logn) 层,每层的合并操作需要 O(n)的时间。
-
空间复杂度:归并排序需要额外的空间来存储临时数组,所以空间复杂度是 O(n))。
快速排序(Quick Sort)
快速排序是另一种基于分治思想的排序算法,其时间复杂度的平均情况也是 O(nlogn),但最坏情况下可能是 O(n^2)。
代码实现(c++)
#include <iostream>
#include <vector>
using namespace std;
// 分区函数
int partition(vector<int>& arr, int low, int high) {
int pivot = arr[high]; // 选择最后一个元素作为基准
int i = low - 1; // i 是较小元素的索引
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(vector<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); // 排序基准元素右边的部分
}
}
int main() {
vector<int> arr = {38, 27, 43, 3, 9, 82, 10};
cout << "原数组: ";
for (int num : arr) {
cout << num << " ";
}
cout << endl;
quickSort(arr, 0, arr.size() - 1); // 排序
cout << "排序后数组: ";
for (int num : arr) {
cout << num << " ";
}
cout << endl;
return 0;
}
复杂度分析
-
最坏情况:O(n^2)(选择的基准元素极差,如每次选择最小或最大元素)
-
最好情况:O(nlogn)(每次选中中位数基准,数组均匀划分)
-
平均情况:O(nlogn)(常见情况,基准元素大致均匀分布)
-
空间复杂度:O(logn) 到 O(n),取决于递归栈深度
最大子数组和问题(Maximum Subarray Problem)
给定一个整数数组,要求找出一个连续子数组,使得其元素和最大。这个问题可以通过分治算法来求解。
代码实现(c++)
#include <iostream>
#include <vector>
#include <algorithm> // 用于max
using namespace std;
// 求解最大子数组和
int maxCrossingSum(const vector<int>& arr, int left, int mid, int right) {
int left_sum = INT_MIN, right_sum = INT_MIN;
int sum = 0;
// 从中点向左扫描,找出最大子数组和
for (int i = mid; i >= left; --i) {
sum += arr[i];
left_sum = max(left_sum, sum);
}
sum = 0;
// 从中点向右扫描,找出最大子数组和
for (int i = mid + 1; i <= right; ++i) {
sum += arr[i];
right_sum = max(right_sum, sum);
}
return left_sum + right_sum; // 返回跨越中点的最大子数组和
}
int maxSubArraySum(const vector<int>& arr, int left, int right) {
if (left == right) {
return arr[left]; // 基本情况:只有一个元素
}
int mid = left + (right - left) / 2; // 找到中点
// 分治递归计算左半部分、右半部分以及跨越中点的最大子数组和
int left_sum = maxSubArraySum(arr, left, mid);
int right_sum = maxSubArraySum(arr, mid + 1, right);
int cross_sum = maxCrossingSum(arr, left, mid, right);
return max({left_sum, right_sum, cross_sum}); // 返回三者中的最大值
}
int main() {
vector<int> arr = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
cout << "原数组: ";
for (int num : arr) {
cout << num << " ";
}
cout << endl;
int n = arr.size();
int max_sum = maxSubArraySum(arr, 0, n - 1);
cout << "最大子数组和是: " << max_sum << endl;
return 0;
}
代码说明
-
maxCrossingSum
函数:用于计算跨越中点的子数组和。它首先从中点向左扫描,找到最大左半部分的和,再从中点向右扫描,找到最大右半部分的和。最后返回两个部分的和。 -
maxSubArraySum
函数:递归地计算数组的最大子数组和。它通过不断将数组分成两部分来求解,并合并结果。
复杂度分析
-
时间复杂度:
-
每次分割数组的时间复杂度是 O(n),因为我们需要计算跨越中点的子数组和。
-
每次递归的深度是 logn,因为每次将数组分成两半。
因此,分治法的时间复杂度为 O(nlogn)。
-
-
空间复杂度:
- 由于使用递归,空间复杂度主要由递归栈深度决定,最深递归深度为 logn。
- 因此空间复杂度为 O(logn)。
逆序对问题(Inversion Count Problem)
逆序对的定义是,对于一个数组中的两个元素,若前面的元素大于后面的元素,则它们构成一个逆序对。分治算法可以有效地计算数组中的逆序对数。
代码实现(c++)
#include <iostream>
#include <vector>
using namespace std;
// 合并两个已排序的子数组,同时计算逆序对
int mergeAndCount(vector<int>& arr, int left, int mid, int right) {
int count = 0;
int n1 = mid - left + 1;
int n2 = right - mid;
// 创建临时数组
vector<int> leftArr(n1), rightArr(n2);
// 拷贝数据到临时数组
for (int i = 0; i < n1; ++i) {
leftArr[i] = arr[left + i];
}
for (int i = 0; i < n2; ++i) {
rightArr[i] = arr[mid + 1 + i];
}
// 合并两个排序好的子数组,同时统计逆序对
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (leftArr[i] <= rightArr[j]) {
arr[k++] = leftArr[i++];
} else {
arr[k++] = rightArr[j++];
count += (n1 - i); // 右数组的当前元素小于左数组的所有剩余元素
}
}
// 将剩余元素拷贝到原数组
while (i < n1) {
arr[k++] = leftArr[i++];
}
while (j < n2) {
arr[k++] = rightArr[j++];
}
return count;
}
// 递归分治函数
int mergeSortAndCount(vector<int>& arr, int left, int right) {
int count = 0;
if (left < right) {
int mid = left + (right - left) / 2;
count += mergeSortAndCount(arr, left, mid); // 左半部分
count += mergeSortAndCount(arr, mid + 1, right); // 右半部分
count += mergeAndCount(arr, left, mid, right); // 合并并计算逆序对
}
return count;
}
int main() {
vector<int> arr = {1, 20, 6, 4, 5};
cout << "原数组: ";
for (int num : arr) {
cout << num << " ";
}
cout << endl;
int n = arr.size();
int inv_count = mergeSortAndCount(arr, 0, n - 1);
cout << "数组中的逆序对数: " << inv_count << endl;
return 0;
}
代码说明
-
mergeAndCount
函数:在合并两个已排序的子数组时,统计逆序对。当右侧子数组的元素小于左侧子数组的元素时,所有左侧子数组的剩余元素都会与该右侧元素形成逆序对。 -
mergeSortAndCount
函数:递归地分治计算逆序对数,并调用mergeAndCount
来合并并计算逆序对。
复杂度分析
- 时间复杂度为 O(nlogn)
- **空间复杂度为 **O(n)
递归矩阵乘法(Divide and Conquer Matrix Multiplication)
矩阵乘法的传统方法是每个元素通过行列相乘来计算,时间复杂度为 O(n^3)。但是可以使用分治法来优化矩阵乘法的实现。
代码实现(c++)
#include <iostream>
#include <vector>
using namespace std;
// 矩阵乘法
void matrixMultiply(const vector<vector<int>>& A, const vector<vector<int>>& B, vector<vector<int>>& C, int n) {
if (n == 1) {
C[0][0] = A[0][0] * B[0][0];
return;
}
int mid = n / 2;
// 创建四个子矩阵
vector<vector<int>> A11(mid, vector<int>(mid)), A12(mid, vector<int>(mid)),
A21(mid, vector<int>(mid)), A22(mid, vector<int>(mid));
vector<vector<int>> B11(mid, vector<int>(mid)), B12(mid, vector<int>(mid)),
B21(mid, vector<int>(mid)), B22(mid, vector<int>(mid));
vector<vector<int>> C11(mid, vector<int>(mid)), C12(mid, vector<int>(mid)),
C21(mid, vector<int>(mid)), C22(mid, vector<int>(mid));
vector<vector<int>> temp1(mid, vector<int>(mid)), temp2(mid, vector<int>(mid));
// 分解A和B为4个子矩阵
for (int i = 0; i < mid; i++) {
for (int j = 0; j < mid; j++) {
A11[i][j] = A[i][j];
A12[i][j] = A[i][j + mid];
A21[i][j] = A[i + mid][j];
A22[i][j] = A[i + mid][j + mid];
B11[i][j] = B[i][j];
B12[i][j] = B[i][j + mid];
B21[i][j] = B[i + mid][j];
B22[i][j] = B[i + mid][j + mid];
}
}
// 计算C11, C12, C21, C22
matrixMultiply(A11, B11, C11, mid);
matrixMultiply(A12, B21, C12, mid);
matrixMultiply(A21, B11, C21, mid);
matrixMultiply(A22, B21, C22, mid);
// 合并四个子矩阵得到最终的结果矩阵C
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
C[i][j] = C11[i][j] + C12[i][j] + C21[i][j] + C22[i][j];
}
}
}
int main() {
int n = 4; // 2x2 矩阵
vector<vector<int>> A = {{1, 2}, {3, 4}};
vector<vector<int>> B = {{5, 6}, {7, 8}};
vector<vector<int>> C(n, vector<int>(n));
matrixMultiply(A, B, C, n);
cout << "矩阵C的结果是:\n";
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
cout << C[i][j] << " ";
}
cout << endl;
}
return 0;
}
复杂度分析
- 时间复杂度为 O(n3)
- 空间复杂度是 O(n^2)