【每日算法】Day 11-1:分治算法精讲——从归并排序到最近点对问题(C++实现)
掌握“分而治之”的算法哲学!今日系统解析分治算法的核心思想与实战应用,覆盖排序优化、数学计算、几何问题等高频场景,彻底理解“分解-解决-合并”的算法范式。
一、分治算法核心思想
分治算法(Divide and Conquer) 是一种将复杂问题分解为相似子问题的算法范式,核心步骤:
分解(Divide):将原问题划分为多个子问题
解决(Conquer):递归解决子问题(若子问题足够小则直接求解)
合并(Combine):将子问题的解合并为原问题的解
适用场景:
-
子问题相互独立且与原问题形式相同
-
合并操作的复杂度低于直接求解原问题
-
典型应用:归并排序、快速排序、矩阵乘法优化
二、分治算法模板(C++)
通用模板结构
Result divideConquer(Problem problem) {
if (problem is trivial) return solveDirectly(problem);
// 分解为子问题
SubProblem sub1 = split(problem);
SubProblem sub2 = split(problem);
// 递归解决子问题
Result res1 = divideConquer(sub1);
Result res2 = divideConquer(sub2);
// 合并结果
return merge(res1, res2);
}
关键特性:
-
递归终止条件:定义最小子问题的处理方式
-
分解策略:决定如何分割问题(均匀分割/按特征分割)
-
合并逻辑:影响最终时间复杂度的重要因素
三、四大经典应用场景
场景1:归并排序(时间复杂度O(n log n))
void mergeSort(vector<int>& nums, int l, int r) {
if (l >= r) return;
int mid = l + (r - l)/2;
mergeSort(nums, l, mid); // 分解左半
mergeSort(nums, mid+1, r); // 分解右半
merge(nums, l, mid, r); // 合并有序数组
}
void merge(vector<int>& nums, int l, int mid, int r) {
vector<int> tmp(r-l+1);
int i = l, j = mid+1, k = 0;
while (i <= mid && j <= r) {
tmp[k++] = nums[i] < nums[j] ? nums[i++] : nums[j++];
}
while (i <= mid) tmp[k++] = nums[i++];
while (j <= r) tmp[k++] = nums[j++];
for (int m = 0; m < tmp.size(); ++m) {
nums[l + m] = tmp[m];
}
}
场景2:快速幂算法(LeetCode 50)
double myPow(double x, int n) {
if (n == 0) return 1.0;
long long N = n;
if (N < 0) { x = 1/x; N = -N; }
return fastPow(x, N);
}
double fastPow(double x, long long n) {
if (n == 0) return 1.0;
double half = fastPow(x, n/2);
if (n % 2 == 0) return half * half;
else return half * half * x;
}
场景3:多数元素(LeetCode 169)
int majorityElement(vector<int>& nums) {
return divide(nums, 0, nums.size()-1);
}
int divide(vector<int>& nums, int l, int r) {
if (l == r) return nums[l];
int mid = l + (r-l)/2;
int left = divide(nums, l, mid);
int right = divide(nums, mid+1, r);
if (left == right) return left;
int cntLeft = count(nums, l, r, left);
int cntRight = count(nums, l, r, right);
return cntLeft > cntRight ? left : right;
}
int count(vector<int>& nums, int l, int r, int target) {
int cnt = 0;
for (int i=l; i<=r; ++i) {
if (nums[i] == target) cnt++;
}
return cnt;
}
场景4:最近点对问题(进阶几何问题)
struct Point { double x, y; };
bool compareX(const Point& a, const Point& b) { return a.x < b.x; }
bool compareY(const Point& a, const Point& b) { return a.y < b.y; }
double closestPair(vector<Point>& points) {
sort(points.begin(), points.end(), compareX);
return divide(points, 0, points.size()-1);
}
double divide(vector<Point>& points, int l, int r) {
if (r - l <= 3) return bruteForce(points, l, r);
int mid = l + (r-l)/2;
double dl = divide(points, l, mid);
double dr = divide(points, mid+1, r);
double d = min(dl, dr);
vector<Point> strip;
for (int i=l; i<=r; ++i) {
if (abs(points[i].x - points[mid].x) < d)
strip.push_back(points[i]);
}
sort(strip.begin(), strip.end(), compareY);
for (int i=0; i<strip.size(); ++i) {
for (int j=i+1; j<strip.size() && (strip[j].y - strip[i].y) < d; ++j) {
d = min(d, distance(strip[i], strip[j]));
}
}
return d;
}
四、分治算法复杂度分析
问题类型 | 递推公式 | 时间复杂度 | 示例 |
---|---|---|---|
归并排序 | T(n) = 2T(n/2) + O(n) | O(n log n) | 排序、逆序对计数 |
快速幂 | T(n) = T(n/2) + O(1) | O(log n) | 幂运算、斐波那契 |
最近点对 | T(n) = 2T(n/2) + O(n) | O(n log n) | 几何计算 |
二分搜索 | T(n) = T(n/2) + O(1) | O(log n) | 有序数组查找 |
五、大厂真题实战
真题1:数组中的逆序对(剑指 Offer 51)
分治解法(归并排序优化):
int reversePairs(vector<int>& nums) {
vector<int> tmp(nums.size());
return mergeSort(nums, tmp, 0, nums.size()-1);
}
int mergeSort(vector<int>& nums, vector<int>& tmp, int l, int r) {
if (l >= r) return 0;
int mid = l + (r-l)/2;
int count = mergeSort(nums, tmp, l, mid)
+ mergeSort(nums, tmp, mid+1, r);
int i = l, j = mid+1, pos = l;
while (i <= mid && j <= r) {
if (nums[i] <= nums[j]) {
tmp[pos++] = nums[i++];
} else {
count += mid - i + 1; // 统计逆序对
tmp[pos++] = nums[j++];
}
}
while (i <= mid) tmp[pos++] = nums[i++];
while (j <= r) tmp[pos++] = nums[j++];
copy(tmp.begin()+l, tmp.begin()+r+1, nums.begin()+l);
return count;
}
真题2:最大子序和(LeetCode 53)
分治解法:
int maxSubArray(vector<int>& nums) {
return divide(nums, 0, nums.size()-1).maxSum;
}
struct Status {
int lSum, rSum, mSum, tSum;
};
Status divide(vector<int>& nums, int l, int r) {
if (l == r) return {nums[l], nums[l], nums[l], nums[l]};
int mid = l + (r-l)/2;
Status left = divide(nums, l, mid);
Status right = divide(nums, mid+1, r);
int tSum = left.tSum + right.tSum;
int lSum = max(left.lSum, left.tSum + right.lSum);
int rSum = max(right.rSum, right.tSum + left.rSum);
int mSum = max({left.mSum, right.mSum, left.rSum + right.lSum});
return {lSum, rSum, mSum, tSum};
}
六、分治算法优化策略
优化方法 | 应用场景 | 优化效果 |
---|---|---|
记忆化 | 重复子问题 | 减少重复计算 |
阈值切换 | 小子问题直接求解 | 降低递归开销 |
并行计算 | 独立子问题 | 提升多核利用率 |
剪枝策略 | 无效分支提前终止 | 减少计算量 |
七、常见误区与调试技巧
-
分解不平衡:导致递归深度过大(如快速排序最坏情况)
-
合并逻辑错误:未正确处理边界条件(如逆序对计数中的区间索引)
-
终止条件缺失:导致无限递归
-
调试技巧:
-
打印递归树层级与当前处理范围
-
可视化中间结果(如归并排序的合并过程)
-
添加断言检查子问题分解的正确性
-
LeetCode真题训练:
-
493. 翻转对
-
315. 计算右侧小于当前元素的个数
-
327. 区间和的个数