【算法】分治

在这里插入图片描述

上期回顾: 【算法】数学
个人主页:GUIQU.
归属专栏:算法

在这里插入图片描述

正文

1. 分治算法的基本概念

分治算法(Divide and Conquer)是一种基于“分而治之”思想的算法设计策略。其核心思路是将一个复杂的大问题分解成多个规模较小、相互独立且与原问题结构相似的子问题,然后分别求解这些子问题,最后将子问题的解合并起来得到原问题的解。

就好比要处理一整块庞大的任务区域,将它划分成若干个小区域,各个小区域分别去完成相应的任务,等小区域的任务都完成了,再把这些小成果汇总整合,最终达成整个大任务的完成。

2. 分治算法的原理

分治算法通常遵循以下三个基本步骤:

  1. 分解(Divide):把原问题分解为若干个规模较小、相互独立且结构与原问题相同或相似的子问题。例如,对于一个排序问题,可以将一个长数组拆分成两个较短的子数组。
  2. 解决(Conquer):递归地求解这些子问题。因为子问题与原问题结构相似,所以可以用同样的方法去解决它们,不断细分直到子问题的规模小到可以直接求解(通常是一些基础情况,比如子数组只有一个元素时,本身就是有序的,无需再继续分解排序)。
  3. 合并(Combine):将各个子问题的解合并为原问题的解。比如在归并排序中,把两个已排好序的子数组合并成一个完整的有序数组。

3. 分治算法的常见应用场景

3.1 排序算法

  • 归并排序(Merge Sort)
    • 原理:归并排序是典型的分治排序算法。它首先将待排序的数组不断地二等分,直到每个子数组只包含一个元素(这时候子数组自然就是有序的),然后再两两合并这些有序的子数组,在合并过程中通过比较元素大小,将它们按顺序组合起来,最终得到整个有序的数组。
    • 代码示例(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> L(n1), R(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];
            i++;
        } else {
            arr[k] = R[j];
            j++;
        }
        k++;
    }

    while (i < n1) {
        arr[k] = L[i];
        i++;
        k++;
    }

    while (j < n2) {
        arr[k] = R[j];
        j++;
        k++;
    }
}

// 归并排序的递归函数
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);
    }
}

// 归并排序主函数
void mergeSort(vector<int>& arr) {
    mergeSort(arr, 0, arr.size() - 1);
}

int main() {
    vector<int> arr = {5, 3, 8, 6, 2};
    mergeSort(arr);
    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;
    return 0;
}
  • 快速排序(Quick Sort)
    • 原理:快速排序也是基于分治思想。它首先选择一个基准元素(通常可以选择数组的第一个元素、最后一个元素或者中间元素等),然后通过一趟排序将数组分割成两部分,使得左边部分的所有元素都小于等于基准元素,右边部分的所有元素都大于等于基准元素。接着对这两部分子数组分别递归地进行同样的操作,直到整个数组有序。
    • 代码示例(C++)
#include <iostream>
#include <vector>
using namespace std;

// 交换两个元素的函数
void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

// 划分函数,选择基准元素并进行划分
int partition(vector<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(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);
    }
}

// 快速排序主函数
void quickSort(vector<int>& arr) {
    quickSort(arr, 0, arr.size() - 1);
}

int main() {
    vector<int>& arr = {5, 3, 8, 6, 2};
    quickSort(arr);
    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;
    return 0;
}

3.2 大整数乘法

  • 原理:对于两个非常大的整数相乘(超出了计算机基本数据类型所能表示的范围),常规的乘法运算效率较低且可能无法直接处理。分治算法可以将大整数拆分成两部分(比如高位部分和低位部分),然后分别计算子部分的乘积,再通过一定的组合和加法运算来得到最终的乘积结果,这样可以降低计算的复杂度,提高运算效率。
  • 代码示例(简单示意,实际应用中可能需要考虑更多细节如进位处理等,以下以整数拆分为两部分为例)
#include <iostream>
#include <string>
using namespace std;

// 将大整数字符串形式转换为整数数组(每个元素表示一位数字)
vector<int> stringToDigits(const string& numStr) {
    vector<int> digits;
    for (char c : numStr) {
        digits.push_back(c - '0');
    }
    return digits;
}

// 大整数加法(辅助函数,用于合并部分乘积结果)
vector<int> addLargeIntegers(const vector<int>& num1, const vector<int>& num2) {
    vector<int> result;
    int carry = 0;
    int i = num1.size() - 1, j = num2.size() - 1;
    while (i >= 0 || j >= 0 || carry > 0) {
        int sum = carry;
        if (i >= 0) {
            sum += num1[i];
            i--;
        }
        if (j >= 0) {
            sum += num2[j];
            j--;
        }
        result.push_back(sum % 10);
        carry = sum / 10;
    }
    reverse(result.begin(), result.end());
    return result;
}

// 大整数乘法的分治核心函数(简单示意)
vector<int> multiplyLargeIntegers(const vector<int>& num1, const vector<int>& num2) {
    int n1 = num1.size();
    int n2 = num2.size();
    if (n1 == 0 || n2 == 0) {
        return {0};
    }
    if (n1 == 1 && n2 == 1) {
        int product = num1[0] * num2[0];
        return {product % 10, product / 10};  // 处理进位,简单返回两位结果示意
    }

    int mid1 = n1 / 2;
    int mid2 = n2 / 2;

    vector<int> num1High(num1.begin(), num1.begin() + mid1);
    vector<int> num1Low(num1.begin() + mid1, num1.end());
    vector<int> num2High(num2.begin(), num2.begin() + mid2);
    vector<int> num2Low(num2.begin() + mid2, num2.end());

    vector<int> product1 = multiplyLargeIntegers(num1High, num2High);
    vector<int> product2 = multiplyLargeIntegers(num1High, num2Low);
    vector<int> product3 = multiplyLargeIntegers(num1Low, num2High);
    vector<int> product4 = multiplyLargeIntegers(num1Low, num2Low);

    // 进行结果的组合和加法运算(此处省略复杂的移位等细节处理,仅示意思路)
    vector<int> result1 = addLargeIntegers(product1, product2);
    vector<int> result2 = addLargeIntegers(product3, product4);
    return addLargeIntegers(result1, result2);
}

int main() {
    string numStr1 = "1234";
    string numStr2 = "5678";
    vector<int> num1 = stringToDigits(numStr1);
    vector<int> num2 = stringToDigits(numStr2);
    vector<int> product = multiplyLargeIntegers(num1, num2);
    for (int digit : product) {
        cout << digit;
    }
    cout << endl;
    return 0;
}

3.3 最近点对问题

  • 原理:在平面上给定一组点,要找出其中距离最近的一对点。分治算法解决此问题的思路是先按照横坐标将所有点分成左右两部分(这就是分解步骤),然后分别在左右两部分中递归地找出各自区域内的最近点对(解决步骤),最后考虑跨越左右两部分的最近点对情况,通过比较这几种情况下的最小距离,得到整个平面内的最近点对(合并步骤)。
  • 代码示例(C++,简单示意核心逻辑,可进一步完善细节)
#include <iostream>
#include <vector>
#include <cmath>
#include <algorithm>
using namespace std;

// 计算两点之间的距离
double distance(const pair<double, double>& p1, const pair<double, double>& p2) {
    double dx = p1.first - p2.first;
    double dy = p1.second - p2.second;
    return sqrt(dx * dx + dy * dy);
}

// 合并左右两部分的最近点对情况,找出全局最近点对(辅助函数)
double merge(vector<pair<double, double>>& points, int left, int mid, int right) {
    double minDistance = DBL_MAX;
    vector<pair<double, double>> strip;
    for (int i = left; i <= right; i++) {
        if (abs(points[i].first - points[mid].first) < minDistance) {
            strip.push_back(points[i]);
        }
    }
    sort(strip.begin(), strip.begin() + strip.size(), [](const pair<double, double>& a, const pair<double, double>& b) {
        return a.second < b.second;
    });
    for (int i = 0; i < strip.size(); i++) {
        for (int j = i + 1; j < strip.size() && (strip[j].second - strip[i].second) < minDistance; j++) {
            double d = distance(strip[i], strip[j]);
            minDistance = min(minDistance, d);
        }
    }
    return minDistance;
}

// 分治求解最近点对问题的递归函数
double closestPair(vector<pair<double, double>>& points, int left, int right) {
    if (left >= right) {
        return DBL_MAX;
    }
    int mid = left + (right - left) / 2;
    double leftMin = closestPair(points, left, mid);
    double rightMin = closestPair(points, mid + 1, right);
    double minDistance = min(leftMin, rightMin);
    return min(minDistance, merge(points, left, mid, right));
}

// 最近点对问题主函数
double closestPair(vector<pair<double, double>>& points) {
    return closestPair(points, 0, points.size() - 1);
}

int main() {
    vector<pair<double, double>> points = {
        {1.0, 2.0},
        {3.0, 4.0},
        {5.0, 6.0},
        {7.0, 8.0}
    };
    double minDistance = closestPair(points);
    cout << "最近点对的距离为: " << minDistance << endl;
    return 0;
}

4. 分治算法的优缺点

  • 优点

    • 降低问题复杂度:通过将大问题拆解成小问题,可以把复杂的计算或操作简化,使得问题更易于处理,尤其对于一些规模庞大、直接求解困难的问题效果显著。
    • 便于并行处理:由于子问题之间相互独立,在具备并行计算能力的环境下,可以同时对多个子问题进行求解,大大提高了整体的计算效率,比如在多核处理器或者分布式计算系统中应用分治算法可以充分发挥其优势。
    • 代码结构清晰:遵循分解、解决、合并的步骤来设计算法,使得代码的逻辑结构相对清晰,易于理解、编写和维护,符合模块化编程的思想。
  • 缺点

    • 合并步骤可能复杂:在某些情况下,将子问题的解合并为原问题的解这一过程可能会比较复杂,需要设计巧妙的算法和数据结构来实现高效的合并操作,否则可能会影响整个算法的效率,甚至导致合并操作的时间复杂度高于分解和解决子问题所节省的时间成本。
    • 递归调用开销:分治算法通常采用递归的方式来实现,而递归调用会占用额外的栈空间,当问题规模非常大或者递归层次过深时,可能会导致栈溢出等问题,需要谨慎处理递归的深度和空间使用情况。

总之,分治算法是一种强大且应用广泛的算法设计策略,在很多领域如算法设计、数据处理、计算机图形学等都有着重要的应用,合理运用分治思想能够高效地解决许多复杂的问题。

5. 分治算法的拓展与变体

5.1 动态规划与分治的联系与区别

  • 联系
    动态规划和分治算法都有将大问题拆解成子问题来求解的思路。它们都是通过解决子问题,并利用子问题的解来构建原问题的解。例如,在计算斐波那契数列时,既可以用分治算法(简单递归的方式,但会有很多重复计算),也可以用动态规划(通过记录已经计算过的子问题结果来避免重复计算)。

  • 区别
    分治算法解决的子问题通常是相互独立的,比如归并排序中左右子数组的排序互不影响。而动态规划处理的子问题之间往往存在重叠,即同一个子问题可能会被多次求解,所以动态规划需要通过“记忆化”(如使用数组等数据结构记录子问题的解)来避免重复计算,提高效率。另外,动态规划更侧重于寻找最优解,通常基于状态转移方程来递推得到整个问题的最优结果,而分治算法重点在于按照步骤分解、解决和合并子问题来得到原问题的解,不一定是求最优解相关的问题。

5.2 分支限界法(一种带有剪枝策略的类似分治的算法思想)

  • 原理
    分支限界法也是基于分治的一种搜索算法思想,它在对问题进行分解和搜索子问题空间时,会通过设定一些界限(如最优解的上界、下界等)来对搜索空间进行剪枝,也就是提前排除那些不可能产生最优解的子问题分支,从而缩小搜索范围,提高搜索效率,更快地找到最优解。

  • 示例场景(以求解 0 - 1 背包问题为例,简单示意)
    在 0 - 1 背包问题中,有多个物品和一个限定容量的背包,每个物品有重量和价值属性,要选择一些物品放入背包使得背包价值最大且不超过背包容量。分支限界法会构建一棵搜索树,树的每个节点代表一种物品选择情况(选或不选),在搜索过程中,通过计算当前节点所能达到的价值上界(比如通过贪心算法等方式估算),如果这个上界小于已经找到的最优解价值,就直接剪掉该节点及其后续分支,不再继续搜索,以此来加快找到最优解的速度。

结语
感谢您的阅读!期待您的一键三连!欢迎指正!

在这里插入图片描述

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Guiat

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值