算法筑基(六):分治算法——大事化小,小事化了

算法筑基(六):分治算法------大事化小,小事化了


📖 前言

分治算法(Divide and Conquer)是一种非常经典的算法设计思想。它的核心很简单:将一个难以直接解决的大问题,分解成若干个规模较小的相同问题,递归求解,最后合并得到原问题的解

这种思想在计算机科学中无处不在:归并排序、快速排序、二分查找、大整数乘法、最近点对......都依赖分治。分治不仅让问题变得可解,常常还能将指数级的复杂度降到多项式级。

本文将从分治的基本框架开始,深入讲解大整数乘法(Karatsuba算法)最近点对问题 这两个经典的分治案例,每个案例都配有完整的C语言代码逐行注释 以及实际应用场景 。同时也会回顾之前已经学过的归并排序、快速排序和二分查找,从分治的视角重新审视它们。最后附上课后练习及答案,巩固所学知识。


📌 本文目录

  1. 分治思想的核心框架
  2. 分治经典回顾
    • 归并排序
    • 快速排序
    • 二分查找
  3. 大整数乘法(Karatsuba算法)
  4. 最近点对问题
  5. 分治算法对比总结
  6. 课后练习与答案

1. 分治思想的核心框架

分治算法的三步曲:

  1. 分解(Divide):将原问题分解为若干个规模较小的相同问题。
  2. 解决(Conquer):递归地求解每个子问题。如果子问题足够小,直接求解。
  3. 合并(Combine):将子问题的解合并成原问题的解。

下面我们通过几个经典例子来体会分治的魅力。


2. 分治经典回顾

2.1 归并排序(Merge Sort)

核心:将数组分成两半,分别排序,再合并。

c 复制代码
void mergeSort(int arr[], int l, int r) {
    if (l < r) {
        int m = l + (r - l) / 2;      // 分解
        mergeSort(arr, l, m);          // 解决左半
        mergeSort(arr, m + 1, r);      // 解决右半
        merge(arr, l, m, r);           // 合并
    }
}

复杂度:O(n log n) 稳定,空间 O(n)。

2.2 快速排序(Quick Sort)

核心:选取基准,将数组划分为左右两部分,递归排序。

c 复制代码
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);         // 解决右半
    }
}

复杂度:平均 O(n log n),最坏 O(n²),空间 O(log n)。

2.3 二分查找(Binary Search)

核心:在有序序列中,每次取中间元素比较,缩小一半范围。

c 复制代码
int binarySearch(int arr[], int l, int r, int target) {
    if (l > r) return -1;
    int mid = l + (r - l) / 2;
    if (arr[mid] == target) return mid;
    if (arr[mid] < target) 
        return binarySearch(arr, mid + 1, r, target);
    else 
        return binarySearch(arr, l, mid - 1, target);
}

复杂度:O(log n)。

以上三个经典算法我们已在前几篇文章中详细实现过,这里不再赘述。接下来重点介绍两个更"分治味"浓厚的算法。


3. 大整数乘法(Karatsuba算法)

3.1 问题背景

两个 n 位数相乘,普通乘法需要 O(n²) 次乘法。Karatsuba 算法通过分治将复杂度降至 O(n^log₂3) ≈ O(n¹·⁵⁸)。

3.2 算法思想

假设有两个 n 位数 x 和 y,将 x 和 y 拆分成高低两部分(n 为偶数时):

复制代码
x = a * 10^{n/2} + b
y = c * 10^{n/2} + d

复制代码
x * y = ac * 10^n + (ad + bc) * 10^{n/2} + bd

直接计算需要四次乘法(ac, ad, bc, bd)。Karatsuba 观察到:

复制代码
ad + bc = (a+b)(c+d) - ac - bd

这样只需要三次乘法:ac, bd, (a+b)(c+d)。

3.3 C语言实现(使用字符串表示大整数)

c 复制代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

// 辅助函数:去掉前导零
char* trimLeadingZeros(char* s) {
    while (*s == '0' && *(s+1) != '\0') s++;
    return s;
}

// 辅助函数:两个字符串数字相加,返回新字符串
char* addStrings(char* a, char* b) {
    int lenA = strlen(a), lenB = strlen(b);
    int maxLen = lenA > lenB ? lenA : lenB;
    char* result = (char*)malloc(maxLen + 2);
    int carry = 0, i = lenA - 1, j = lenB - 1, k = maxLen;
    result[maxLen + 1] = '\0';
    while (i >= 0 || j >= 0 || carry) {
        int sum = carry;
        if (i >= 0) sum += a[i--] - '0';
        if (j >= 0) sum += b[j--] - '0';
        result[k--] = (sum % 10) + '0';
        carry = sum / 10;
    }
    if (k >= 0) {
        result[k] = '0' + carry;
        return trimLeadingZeros(result);
    }
    return trimLeadingZeros(result + 1);
}

// 辅助函数:两个字符串数字相减(假设a >= b)
char* subStrings(char* a, char* b) {
    int lenA = strlen(a), lenB = strlen(b);
    int maxLen = lenA > lenB ? lenA : lenB;
    char* result = (char*)malloc(maxLen + 2);
    int borrow = 0, i = lenA - 1, j = lenB - 1, k = maxLen;
    result[maxLen + 1] = '\0';
    while (i >= 0 || j >= 0) {
        int diff = (i >= 0 ? a[i--] - '0' : 0) - borrow;
        if (j >= 0) diff -= b[j--] - '0';
        if (diff < 0) {
            diff += 10;
            borrow = 1;
        } else {
            borrow = 0;
        }
        result[k--] = diff + '0';
    }
    return trimLeadingZeros(result + (k + 1));
}

// 辅助函数:字符串数字乘以10的n次方(末尾加零)
char* multiplyByPowerOf10(char* s, int n) {
    int len = strlen(s);
    char* result = (char*)malloc(len + n + 1);
    strcpy(result, s);
    for (int i = 0; i < n; i++) {
        result[len + i] = '0';
    }
    result[len + n] = '\0';
    return result;
}

// Karatsuba 乘法
char* karatsuba(char* x, char* y) {
    // 去除前导零
    x = trimLeadingZeros(x);
    y = trimLeadingZeros(y);
    
    int lenX = strlen(x);
    int lenY = strlen(y);
    
    // 如果任意一个数为0,返回"0"
    if ((lenX == 1 && x[0] == '0') || (lenY == 1 && y[0] == '0')) {
        char* zero = (char*)malloc(2);
        zero[0] = '0'; zero[1] = '\0';
        return zero;
    }
    
    // 递归基:如果两个数都很小,直接计算
    if (lenX <= 4 && lenY <= 4) {
        int a = atoi(x);
        int b = atoi(y);
        int res = a * b;
        char* result = (char*)malloc(20);
        sprintf(result, "%d", res);
        return result;
    }
    
    // 取最大长度,使两个数长度一致(前面补零)
    int maxLen = lenX > lenY ? lenX : lenY;
    if (maxLen % 2 != 0) maxLen++;
    char* xPadded = (char*)calloc(maxLen + 1, 1);
    char* yPadded = (char*)calloc(maxLen + 1, 1);
    int padX = maxLen - lenX;
    int padY = maxLen - lenY;
    memset(xPadded, '0', padX);
    strcpy(xPadded + padX, x);
    memset(yPadded, '0', padY);
    strcpy(yPadded + padY, y);
    
    int half = maxLen / 2;
    // 分割为 high 和 low
    char* a = (char*)malloc(half + 1);
    char* b = (char*)malloc(half + 1);
    char* c = (char*)malloc(half + 1);
    char* d = (char*)malloc(half + 1);
    strncpy(a, xPadded, half); a[half] = '\0';
    strncpy(b, xPadded + half, half); b[half] = '\0';
    strncpy(c, yPadded, half); c[half] = '\0';
    strncpy(d, yPadded + half, half); d[half] = '\0';
    
    // 递归计算三个乘积
    char* ac = karatsuba(a, c);
    char* bd = karatsuba(b, d);
    
    // 计算 a+b 和 c+d
    char* a_plus_b = addStrings(a, b);
    char* c_plus_d = addStrings(c, d);
    char* sum_ab_cd = karatsuba(a_plus_b, c_plus_d);
    
    // 计算 (a+b)(c+d) - ac - bd
    char* sub1 = subStrings(sum_ab_cd, ac);
    char* ad_bc = subStrings(sub1, bd);
    
    // 合并结果: ac * 10^maxLen + (ad+bc) * 10^{half} + bd
    char* part1 = multiplyByPowerOf10(ac, maxLen);
    char* part2 = multiplyByPowerOf10(ad_bc, half);
    char* temp = addStrings(part1, part2);
    char* result = addStrings(temp, bd);
    
    // 释放内存
    free(a); free(b); free(c); free(d);
    free(ac); free(bd); free(a_plus_b); free(c_plus_d);
    free(sum_ab_cd); free(sub1); free(ad_bc);
    free(part1); free(part2); free(temp);
    free(xPadded); free(yPadded);
    
    return result;
}

int main() {
    char a[] = "123456789";
    char b[] = "987654321";
    char* product = karatsuba(a, b);
    printf("%s * %s = %s\n", a, b, product);
    free(product);
    return 0;
}

3.4 案例:高精度计算

Karatsuba 算法常用于大数库(如 Python 的 int 乘法),在加密算法、科学计算中都有应用。上面的代码虽然简化,但展示了分治乘法的核心思路。

3.5 复杂度分析

  • 传统乘法:O(n²)
  • Karatsuba:O(n^log₂3) ≈ O(n¹·⁵⁸)

4. 最近点对问题

4.1 问题描述

给定平面上 n 个点,找出欧几里得距离最近的一对点。

4.2 分治策略

  1. 分解:按 x 坐标排序,分成左右两半,递归求左右半内的最近点对距离 d₁ 和 d₂。
  2. 合并:考虑跨越中线的点对。只有在中线左右距离 d = min(d₁, d₂) 范围内的点才可能产生更小距离。在带状区域内按 y 坐标排序,对每个点检查其后最多 7 个点即可。
  3. 基例:点数很少时直接枚举。

4.3 C语言实现

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

typedef struct {
    double x, y;
} Point;

// 按 x 排序
int cmpX(const void* a, const void* b) {
    Point* p1 = (Point*)a;
    Point* p2 = (Point*)b;
    return (p1->x - p2->x) > 0 ? 1 : -1;
}

// 按 y 排序
int cmpY(const void* a, const void* b) {
    Point* p1 = (Point*)a;
    Point* p2 = (Point*)b;
    return (p1->y - p2->y) > 0 ? 1 : -1;
}

// 计算两点距离
double dist(Point a, Point b) {
    double dx = a.x - b.x;
    double dy = a.y - b.y;
    return sqrt(dx*dx + dy*dy);
}

// 暴力求解小规模
double bruteForce(Point points[], int n) {
    double min = 1e20;
    for (int i = 0; i < n; i++) {
        for (int j = i+1; j < n; j++) {
            double d = dist(points[i], points[j]);
            if (d < min) min = d;
        }
    }
    return min;
}

// 在带状区域中寻找最小距离
double stripClosest(Point strip[], int size, double d) {
    double min = d;
    // 按 y 排序
    qsort(strip, size, sizeof(Point), cmpY);
    for (int i = 0; i < size; i++) {
        for (int j = i+1; j < size && (strip[j].y - strip[i].y) < min; j++) {
            double d2 = dist(strip[i], strip[j]);
            if (d2 < min) min = d2;
        }
    }
    return min;
}

// 分治主函数
double closestUtil(Point points[], int left, int right) {
    if (right - left + 1 <= 3) {
        return bruteForce(points + left, right - left + 1);
    }
    
    int mid = left + (right - left) / 2;
    Point midPoint = points[mid];
    
    double dl = closestUtil(points, left, mid);
    double dr = closestUtil(points, mid + 1, right);
    double d = dl < dr ? dl : dr;
    
    // 收集带状区域内的点
    Point* strip = (Point*)malloc((right - left + 1) * sizeof(Point));
    int stripSize = 0;
    for (int i = left; i <= right; i++) {
        if (fabs(points[i].x - midPoint.x) < d) {
            strip[stripSize++] = points[i];
        }
    }
    
    double dMin = stripClosest(strip, stripSize, d);
    free(strip);
    return dMin;
}

double closestPair(Point points[], int n) {
    qsort(points, n, sizeof(Point), cmpX);
    return closestUtil(points, 0, n-1);
}

int main() {
    Point points[] = {{2, 3}, {12, 30}, {40, 50}, {5, 1}, {12, 10}, {3, 4}};
    int n = sizeof(points) / sizeof(points[0]);
    double minDist = closestPair(points, n);
    printf("最近点对距离: %lf\n", minDist);
    return 0;
}

4.4 案例:地理信息系统

在 GIS 中,需要快速找到距离最近的两个兴趣点(如加油站),分治法能高效处理数万个点。

4.5 复杂度分析

  • 时间复杂度:O(n log n)(主要来自排序和递归)。
  • 空间复杂度:O(n)。

📊 分治算法对比

算法 分解方式 合并复杂度 总复杂度
归并排序 按位置二分 O(n) O(n log n)
快速排序 按基准划分 O(n) 平均 O(n log n)
二分查找 按大小二分 O(1) O(log n)
Karatsuba 按数位二分 O(n) O(n^1.585)
最近点对 按坐标二分 O(n log n) O(n log n)

🎯 如何设计分治算法?

  1. 找到合适的分解方式:尽量使子问题规模均衡。
  2. 递归基:当问题足够小时,直接求解。
  3. 合并策略:这是分治的难点,需要巧妙设计。

✍️ 课后练习与答案

练习题目

  1. 实现一个非递归版本的归并排序。
  2. 尝试用 Karatsuba 算法计算 9999 × 9999 的精确结果(字符串形式)。
  3. 在最近点对问题中,证明带状区域中每个点只需检查最多 7 个点。
  4. 实现一个分治算法求解"最大子数组和"(例如股票最大收益)。

参考答案

1. 非递归归并排序(迭代版)
c 复制代码
void mergeSortIterative(int arr[], int n) {
    int* temp = (int*)malloc(n * sizeof(int));
    for (int size = 1; size < n; size *= 2) {
        for (int left = 0; left < n - size; left += 2 * size) {
            int mid = left + size - 1;
            int right = (left + 2 * size - 1 < n - 1) ? left + 2 * size - 1 : n - 1;
            merge(arr, left, mid, right, temp);
        }
    }
    free(temp);
}
2. Karatsuba 计算 9999×9999

直接调用 karatsuba("9999", "9999"),结果应为 "99980001"。

3. 带状区域检查点数证明

在最近点对问题中,对任意点 p,在其右侧 d×2d 的矩形内最多有 6 个点(因为若超过 6 个,则必有两点的 y 坐标差小于 d,从而距离小于 d)。实际上,每个点只需检查其后最多 7 个点即可保证找到最小距离。

4. 最大子数组和(分治法)
c 复制代码
#include <stdio.h>
#include <limits.h>

int maxCrossingSum(int arr[], int l, int m, int r) {
    int sum = 0, leftSum = INT_MIN, rightSum = INT_MIN;
    for (int i = m; i >= l; i--) {
        sum += arr[i];
        if (sum > leftSum) leftSum = sum;
    }
    sum = 0;
    for (int i = m+1; i <= r; i++) {
        sum += arr[i];
        if (sum > rightSum) rightSum = sum;
    }
    return leftSum + rightSum;
}

int maxSubArraySum(int arr[], int l, int r) {
    if (l == r) return arr[l];
    int m = l + (r - l) / 2;
    int leftSum = maxSubArraySum(arr, l, m);
    int rightSum = maxSubArraySum(arr, m+1, r);
    int crossSum = maxCrossingSum(arr, l, m, r);
    if (leftSum >= rightSum && leftSum >= crossSum) return leftSum;
    else if (rightSum >= leftSum && rightSum >= crossSum) return rightSum;
    else return crossSum;
}

int main() {
    int arr[] = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
    int n = sizeof(arr)/sizeof(arr[0]);
    int maxSum = maxSubArraySum(arr, 0, n-1);
    printf("最大子数组和: %d\n", maxSum);
    return 0;
}

🌟 寄语

分治思想是人类解决复杂问题的本能------遇到难题,先拆成小块,逐个击破,再拼起来。这种思维方式不仅在算法中重要,在生活和工作中同样适用。

本篇文章我们重新温习了归并、快排、二分,又深入学习了 Karatsuba 乘法和最近点对。这些都是分治思想的杰出代表。希望你能够举一反三,遇到新问题时先想想能否用分治来解决。

下一篇文章我们将进入字符串算法,学习 KMP、Trie 树等文本处理利器。敬请期待!


如果你在阅读过程中有任何疑问,欢迎评论区留言。
如果觉得这篇文章对你有帮助,点个赞👍,支持一下!

相关推荐
Flying pigs~~2 小时前
基于Bert的模型迁移文本分类项目
人工智能·深度学习·算法·大模型·nlp·bert
美式请加冰2 小时前
BFS算法(下)
算法·宽度优先
少许极端2 小时前
算法奇妙屋(三十七)-贪心算法学习之路4
学习·算法·贪心算法·田忌赛马
We་ct2 小时前
LeetCode 373. 查找和最小的 K 对数字:题解+代码详解
前端·算法·leetcode·typescript·二分·
Ricky_Theseus2 小时前
探索群体智慧:蚁群算法(ACO)从原理到实践——python实现
python·算法·机器学习
Rabitebla2 小时前
排序算法专题(一):插入排序 & 希尔排序
数据结构·算法·排序算法
南境十里·墨染春水10 小时前
C++传记(面向对象)虚析构函数 纯虚函数 抽象类 final、override关键字
开发语言·c++·笔记·算法
2301_7971727510 小时前
基于C++的游戏引擎开发
开发语言·c++·算法
有为少年11 小时前
告别“唯语料论”:用合成抽象数据为大模型开智
人工智能·深度学习·神经网络·算法·机器学习·大模型·预训练