分治算法:把大问题切成小块吃掉的智慧
困难16用分治算法,把大问题切成小块吃掉的秘诀
你有没有遇到过这样的场景:妈妈让你整理一箱子杂乱无章的玩具,你直接动手肯定累得不行。但如果先把玩具分成几堆——乐高一堆、小汽车一堆、布娃娃一堆,每堆再分别摆好,最后合到一起就整整齐齐了。这就是“分治”思想的日常版本。
在计算机程序里,分治算法(Divide and Conquer)就是做同样的事:把一个大问题拆成若干个规模更小、结构相似的子问题,分别解决它们,再把结果合并起来,得到最终答案。听起来很简单?没错!很多复杂的问题,比如排序、搜索、求最大值,都能用这种方法轻松搞定。
分治的三大步骤:拆、解、合
分治算法的核心只有三步,我们挨个来看:
- 分解:把一个大问题分成若干个相同类型的子问题。就像把50个同学的名单分成两组,每组25人。
- 解决:分别解决这些子问题。如果子问题仍然很大,可以继续递归分解,直到子问题小到可以直接解决(比如只剩1个人,他的分数就是他自己)。
- 合并:把子问题的解组合起来,得到原问题的答案。比如把两组的总分加起来,就是全班总分。
生活例子:你想知道全班同学的身高总和。可以先把名单平均分成两组,每组25人;再每组分成两小组,直到只剩1个人——那他的身高就是他自己。然后从最小的组开始,把两个身高加起来,一步步往上合并,最后得到全班总和。整个过程就像搭积木,一层一层往上堆。
代码实战:用分治求数组的最大值
我们来看一段具体的C++代码。它不断把数组从中间切开,直到只剩一个数,然后比较左右两边的最大值,取较大的那个。
#include <iostream>
using namespace std;
// 函数:找到数组 arr[left...right] 中的最大值
// left: 左边界索引,right: 右边界索引
int findMax(int arr[], int left, int right) {
// 如果只剩一个元素,它就是最大值(递归终止条件)
if (left == right) {
return arr[left];
}
// 从中间分开
int mid = (left + right) / 2;
// 递归求左半部分的最大值
int leftMax = findMax(arr, left, mid);
// 递归求右半部分的最大值
int rightMax = findMax(arr, mid + 1, right);
// 合并:取两者中较大的那个
return (leftMax > rightMax) ? leftMax : rightMax;
}
int main() {
int arr[] = {3, 8, 1, 6, 9, 2, 7}; // 待查找的数组
int n = sizeof(arr) / sizeof(arr[0]); // 数组元素个数
cout << "数组中的最大值是: " << findMax(arr, 0, n - 1) << endl;
return 0;
}
运行这段代码,它会输出9。你可能会想:这个代码用了递归,执行时到底怎么工作的?我们来模拟一下。假设数组是 {3, 8, 1, 6, 9, 2, 7},索引从0到6。
- 第一次调用
findMax(arr, 0, 6),mid=3,分成左半 [0..3] 和右半 [4..6]。 - 左半继续分:[0..1] 和 [2..3];右半分:[4..5] 和 [6..6](因为只剩一个元素,直接返回7)。
- 再继续分,直到每个子数组只有一个元素。比如 [0..0] 返回3,[1..1] 返回8,然后比较得到 leftMax=8;[2..2] 返回1,[3..3] 返回6,得到 leftMax=6(注意这里左右名字有重名,但理解过程就好)。
- 然后向上合并:[0..1]得到8,[2..3]得到6,比较得8;[4..5]中[4..4]=9, [5..5]=2,得9;[6..6]=7。最后合并 [0..3]=8 和 [4..6]=9,得到全局最大值9。
你看,分治算法把复杂问题变得像切蛋糕一样简单。
更多生活中的分治例子
- 切蛋糕:你有一块大蛋糕,要分给全班同学。先把蛋糕切成两半,每半再切成两半,直到每个人得到一小块。最后你不需要关心整块蛋糕有多大,只要每块分对就行——这就是分治。
- 计算班级平均分:把全班按学号分成两组,每组算出平均分,然后取两个平均分的平均值?等等,这不对!平均分不能直接这样合并,因为两组人数可能不同。所以分治思想不是万能的,它要求子问题结构相同、可独立求解,且合并方式正确。正确的做法是用分治算总分和总人数,最后再除。
- 找围棋棋盘上最大的一块黑子区域:把棋盘分成四块,分别找每块中的最大区域,再考虑跨边界的情况——这就要用到更高级的分治了。
新手最容易犯的四个错误
-
忘记写递归终止条件
比如上面代码中,if (left == right)就是终止条件。如果漏掉,递归会无限循环下去,直到栈溢出(程序崩溃)。你可以试着删掉那两行,运行一下——会报错“Segmentation fault”或“Stack overflow”。 -
边界划分出错
分界点mid = (left + right) / 2后,左半部分是[left, mid],右半部分必须是[mid+1, right]。有些人会写成[left, mid-1]或[mid, right],导致数组元素丢失或重复计算。比如用[left, mid-1]就会漏掉mid位置的那个元素。 -
合并逻辑错误
拿求最大值来说,合并时应该取leftMax和rightMax的最大值,但如果写成leftMax + rightMax就会得到和——那就全错了。分治的合并步骤一定要根据问题来,不能一概而论。 -
递归层数太深导致栈溢出
如果数组非常大(比如100万个元素),递归会调用很多层(约log2(100万) ≈ 20层,其实还好)。但如果你写的是线性分割(比如每次只减1),递归层数就可能等于数组长度,那就容易爆栈。所以分治通常采用对半分割,让递归深度为O(log n),很安全。
完整示例:用分治求数组的和
学会了找最大值,我们换个例子:求所有数的和。代码非常类似,只是合并时改成加法。
#include <iostream>
using namespace std;
// 函数:计算数组 arr[left...right] 的和
int arrSum(int arr[], int left, int right) {
if (left == right) { // 只剩一个元素,直接返回它
return arr[left];
}
int mid = (left + right) / 2; // 从中间切开
int leftSum = arrSum(arr, left, mid); // 左半部分的和
int rightSum = arrSum(arr, mid + 1, right); // 右半部分的和
return leftSum + rightSum; // 合并:两边相加
}
int main() {
int arr[] = {3, 8, 1, 6, 9, 2, 7}; // 待求和数组
int n = sizeof(arr) / sizeof(arr[0]);
int total = arrSum(arr, 0, n - 1);
cout << "数组所有元素之和是: " << total << endl; // 输出 36
return 0;
}
运行它会输出 36(3+8+1+6+9+2+7=36)。这个例子和求最大值几乎一样,只是合并时把“取较大”改成“相加”。说明分治算法的框架是可以复用的,你只需要改合并的逻辑,就能解决不同问题。
为什么学完分治,还要学其他的?
分治思想是很多经典算法的基础。比如:
- 归并排序:把数组拆成两半,分别排序,然后合并成有序数组。
- 快速排序:选一个基准值,把小于基准的放左边,大于的放右边,再分别递归。
- 二分查找:在有序数组中找目标值,每次把范围缩小一半——这也是分治(但没有合并步骤)。
- 棋盘覆盖问题、最近点对问题等。
掌握了分治,你就拥有了“大事化小”的思维。遇到任何复杂问题,先想想能不能拆成更简单的小问题,解决后再合起来。这种能力不仅在编程中有用,在生活中也同样管用哦!
相关指引:
如果你已经理解了分治的基本思想,可以继续学习:
试着用分治思想写一个“求数组中最大两个数的和”的程序吧!
例题精讲
在分治算法中,将问题分解为子问题后,通常需要对子问题的解进行合并。以下哪个排序算法使用了分治策略?
二分查找算法属于分治算法,因为它将查找范围每次缩小一半。
以下是用分治思想求数组最大子段和的C++代码片段,请填空。\nint maxCrossingSum(int arr[], int l, int m, int h) {\n int sum = 0, left_sum = INT_MIN;\n for (int i = m; i >= l; i--) {\n sum += arr[i];\n if (sum > left_sum) left_sum = sum;\n }\n sum = 0; int right_sum = INT_MIN;\n for (int i = m+1; i <= h; i++) {\n sum += arr[i];\n if (sum > right_sum) right_sum = sum;\n }\n return left_sum + right_sum;\n}\nint maxSubArraySum(int arr[], int l, int h) {\n if (l == h) return arr[l];\n int m = (l + h) / 2;\n return max(___(1)___, max(___(2)___, ___(3)___));\n}在快速排序的分治过程中,partition函数返回一个索引,使得该索引左边的元素都小于等于pivot,右边的元素都大于等于pivot。以下关于快速排序的描述错误的是:
分治算法总是将问题分解成两个规模相等的子问题。