【每日算法】Day 11-1:分治算法精讲——从归并排序到最近点对问题(C++实现)

掌握“分而治之”的算法哲学!今日系统解析分治算法的核心思想与实战应用,覆盖排序优化、数学计算、几何问题等高频场景,彻底理解“分解-解决-合并”的算法范式。


一、分治算法核心思想

分治算法(Divide and Conquer) 是一种将复杂问题分解为相似子问题的算法范式,核心步骤:

  1. 分解(Divide):将原问题划分为多个子问题

  2. 解决(Conquer):递归解决子问题(若子问题足够小则直接求解)

  3. 合并(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& 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& nums, int l, int mid, int r) {
    vector 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& nums) {
    return divide(nums, 0, nums.size()-1);
}

int divide(vector& 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& 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& points) {
    sort(points.begin(), points.end(), compareX);
    return divide(points, 0, points.size()-1);
}

double divide(vector& 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 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

四、分治算法复杂度分析

问题类型 递推公式 时间复杂度 示例
归并排序 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& nums) {
    vector tmp(nums.size());
    return mergeSort(nums, tmp, 0, nums.size()-1);
}

int mergeSort(vector& nums, vector& 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& nums) {
    return divide(nums, 0, nums.size()-1).maxSum;
}

struct Status {
    int lSum, rSum, mSum, tSum;
};

Status divide(vector& 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};
}

六、分治算法优化策略

优化方法 应用场景 优化效果
记忆化 重复子问题 减少重复计算
阈值切换 小子问题直接求解 降低递归开销
并行计算 独立子问题 提升多核利用率
剪枝策略 无效分支提前终止 减少计算量

七、常见误区与调试技巧

  1. 分解不平衡:导致递归深度过大(如快速排序最坏情况)

  2. 合并逻辑错误:未正确处理边界条件(如逆序对计数中的区间索引)

  3. 终止条件缺失:导致无限递归

  4. 调试技巧

    • 打印递归树层级与当前处理范围

    • 可视化中间结果(如归并排序的合并过程)

    • 添加断言检查子问题分解的正确性


LeetCode真题训练:

  • 493. 翻转对

  • 315. 计算右侧小于当前元素的个数

  • 327. 区间和的个数

你可能感兴趣的:(算法,c++,数据结构)