当前位置:   article > 正文

分治算法——经典案例分析_分治算法经典例题

分治算法经典例题

目录

案例一:二分搜索

案例二:数组元素计数

案例三:任务调度

课后习题


分治算法(Divide and Conquer)是一种解决问题的算法设计策略,它将一个大问题分解成若干个规模较小且相互独立的子问题,然后将这些子问题的解合并起来,从而得到原问题的解。

分治算法通常包括以下三个步骤:

  1. 分解:将原问题分解为⼀组⼦问题,⼦问题与原问题类似,但是规模更小
  2. 解决递归求解⼦问题,如果⼦问题⾜够小,停⽌递归,直接求解
  3. 合并:将⼦问题的解组合成原问题的解

分治算法的典型应用包括排序算法(如快速排序、归并排序)、搜索算法(如二分查找)、数学问题(如大整数乘法)等。


案例一:二分搜索

输入:⼀个已排序数组 a[1...n](元素各不相同),⼀个元素 x
输出:如果 x = a[j],返回 j,否则返回 -1

分治测略:

  • 分解:数组Left,中间元素 a[mid],数组Right
  • 解决:如果 a[mid],返回 mid,否则递归求解数组Left或者数组Right
    • 如果数组为空,停⽌递归,直接求解(元素不存在)
  • 合并:不需要额外⼯作
  1. #include <iostream>
  2. #include <vector>
  3. using namespace std;
  4. //分治递归函数,复杂度为O(logn)
  5. int binary_search(const vector<int>& a, int x, int low, int high)
  6. {
  7. if (low > high) return -1;
  8. int mid = (low + high) / 2;
  9. if (a[mid] == x) return mid;
  10. if (a[mid] > x)
  11. return binary_search(a, x, low, mid - 1);
  12. else
  13. return binary_search(a, x, mid + 1, high);
  14. }
  15. int main() {
  16. vector<int> arr = {3, 8, 1, 6, 2, 5, 9, 4, 7};
  17. int x = 8;
  18. int j = binary_search(arr, x, 0, arr.size() - 1);
  19. cout << j << endl; // 输出:1 即8所在的数组下标
  20. return 0;
  21. }

 正确性分析:

命题: 如果x∈a[low ... high],算法返回 j,其中 x=a[j], low j< high,否则返回-1

对数组 a 的长度 n = high - low +1 进行归纳:

  • 基本情况: n = 0

        - low = high +1,此时算法返回-1,显然正确 (空数组不包舍x)

  • 归纳假设: 假设对所有长度小于 k > 1 的a的子数组,命题正确
  • 归纳步骤: 证明对长度为k的数组,命题正确

        - a[mid]=x: 算法返回mid,显然正确
        - a[mid]<x: 因为a有序,所以x ∈ a[mid + l..high],子问题能够正确求解,所以命题正确
        - a[mid]>x: 因为a有序,所以x ∈ a[low..mid-1],子问题能够正确求解,所以命题正确


案例二:数组元素计数

输入:一个已排序数组a[1..n],一个元素x

输出: 元素x的出现次数

方法一:直接使用二分搜索

  • 通过二分搜索可以在O(log n)时间内找到元素 x 所在的块
  • 然后向左 (右)扫描找到块的左 (右)边界
  • 时间复杂度: O(logn +s),其中s是块的长度
  • 最坏情况是Θ(n),即一整个数组都是要找的元素x
  1. int direct_count(const vector<int>& a, int x, int j=-1)
  2. {
  3. count = 0;
  4. j = binary_search(a,x,0,a.size())
  5. if (j) //若x存在,开始左右遍历
  6. i = j-1; k = j+1; count++;
  7. while(a[i]==x && i>=0){ // <-左遍历
  8. count++;
  9. i--;
  10. }
  11. while(a[k]==x && k<a.size()){ // ->右遍历
  12. count++;
  13. k++;
  14. }
  15. return count;
  16. }

方法二:改进二分搜索

  • 分别找到块的左右边界
  • 时间复杂度:O(2log n)
  1. //二分找左边界
  2. int first(const vector<int>& a, int x, int left, int right)
  3. {
  4. int leftIndex = -1; //左边界下标
  5. while (left <= right) {
  6. int mid = left + (right - left) / 2;
  7. if (a[mid] == x) {
  8. leftIndex = mid;
  9. right = mid - 1; //右侧向左靠拢,不断逼近左边界
  10. }
  11. else if (a[mid] > x) {right = mid - 1;}
  12. else {left = mid + 1;}
  13. }
  14. }
  15. //二分找右边界
  16. int last(const vector<int>& a, int x, int left, int right)
  17. {
  18. int rightIndex = -1; //右边界下标
  19. while (left <= right) {
  20. int mid = left + (right - left) / 2;
  21. if (a[mid] == x) {
  22. rightIndex = mid;
  23. left = mid + 1; //左侧向右靠拢,不断逼近右边界
  24. }
  25. else if (a[mid] > x) {right = mid - 1;}
  26. else {left = mid + 1;}
  27. }
  28. }
  29. int count(const vector<int>& a, int x)
  30. {
  31. int i = first(a, x, 0, a.size() - 1);
  32. if (i == -1) return 0;
  33. int j = last(a, x, i, a.size() - 1);
  34. return j - i + 1;
  35. }

案例三:任务调度

给你k个任务和n台机器,其中机器i处理一个任务所需的时间为t_{i};

求处理所有任务所需的最短时间。

比如k=8,n=3,t[1..3] ={2,3,1}时,最短时间为9

这道题同样可以采用二分法进行递归求解。解题思路:

  • 初始一个最小的时间上界T0,比如将所有任务交给完成速度最快的机器
  • 给定时间T后,每个机器都可以计算出在T时间内的处理任务数量\left \lfloor \frac{T}{t_{i}} \right \rfloor(向下取整)
  • 计算n个机器的任务数量和k^{'}= \sum_{i=1}^{n}\left \lfloor \frac{T}{t_{i}} \right \rfloor,即是T时间内所能处理的最大任务量
  • 不断二分更新T,直到k'<k
  1. #include <iostream>
  2. #include <vector>
  3. #include <algorithm>
  4. using namespace std;
  5. int calculateTasks(vector<int>& times, int T) {
  6. int tasks = 0;
  7. for(int time : times) {
  8. tasks += T / time;
  9. }
  10. return tasks;
  11. }
  12. int findShortestTime(vector<int>& times, int machines, int totalTasks) {
  13. int left = 1;
  14. int right = *min_element(times.begin(), times.end()) * totalTasks;
  15. int shortestTime = right;
  16. while (left <= right) {
  17. int mid = left + (right - left) / 2;
  18. int completedTasks = calculateTasks(times, mid);
  19. if (completedTasks >= totalTasks) {
  20. shortestTime = mid;
  21. right = mid - 1;
  22. } else {
  23. left = mid + 1;
  24. }
  25. }
  26. return shortestTime;
  27. }
  28. int main() {
  29. vector<int> times = {2, 3, 1};
  30. int machines = 3;
  31. int totalTasks = 8;
  32. int shortestTime = findShortestTime(times, machines, totalTasks);
  33. cout << "处理所有任务所需的最短时间为:" << shortestTime << endl;
  34. return 0;
  35. }

课后习题

 1.寻找中位数

输入: 数组a[1..n]
输出:a[1],..,a[n]的中位数

2. 逆序对

在一个数组A[1...n]中,逆序对 (inversion) 是一对索引(i,j),满足i<j 且A[i]>A[j]。一个包含n个元素的数组中的逆序对数量介于0(如果数组已排序)和2n(如果数组完全逆序)之间。设计一个高效的算法计算数组A[1...n]中逆序对的数量。

3.支配点

给定二维平面上两个不同的点p和q,如果p.x < q.xp.y < q.y,称q支配p。给定一个点集P,设计一个高效的算法,计算每一个点p∈P支配的点的数量。给出算法的基本思路和伪代码描述,分析算法的时间复杂度。

习题答案:分治算法课后习题

习题答案:分治算法课后习题2

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/716353
推荐阅读
相关标签
  

闽ICP备14008679号