分治法
五大常用算法设计思想之一:分治算法,介绍其思想和应用场景,提供经典题目的解答。
分治法即“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
✏️ 可使用分治法求解的一些经典问题
二分搜索
大整数乘法
Strassen矩阵乘法
棋牌覆盖
线性时间选择
最接近点对问题
循环赛日程表
汉诺塔
✏️ 算法实现
🖌️ 1、二叉树DFS深度搜索——自下而上
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
// 返回条件(NULL)
if(!root)
return result;
// 分治(Divide)
vector<int> result_left, result_right;
if(root->left){
result_left = preorderTraversal(root->left);
}
if(root->right){
result_right = preorderTraversal(root->right);
}
// 合并结果(Conquer)
result.push_back(root->val);
result.insert(result.end(), result_left.begin(), result_left.end());
result.insert(result.end(), result_right.begin(), result_right.end());
return result;
} 两个 位的大整数相乘,按照基线乘法(也就是笔算乘法或竖式计算法),算法的时间复杂度是 ,基线乘法在 的复杂度上进行计算和向上传递进位,每计算一次单精度乘法都要计算和传递进位,这样的话就使得嵌套循环的顺序性很强,难以并行展开和实现。有一种改进的 Comba 乘法,和普通的笔算乘法很类似,只是每一次单精度乘法只是单纯计算乘法,不计算进位,进位留到每一列累加后进行。所以原来需要 次进位,现在最多只需要 次即可。
然而这个问题可以用分治法来解决,时间复杂度可降至 ,即Karatsuba算法。

算法:首先将 X 和 Y 分成A、B、C、D,此时将 X 和 Y 的乘积转化为图中的式子,把问题转化为求解式子的值。对于这个式子,我们一共要进行4 次乘法(AC、AD、BC、BD),所以 ,建立递归方程:
通过master定理可以求得该算法的时间复杂度为: 。然后用加法来换取乘法:
对于这个公式,一共进行了三次乘法(AC、BD、(A+B)(C+D)),因此, ,建立递归方程:
通过master定理求得时间复杂度为: 。
另外常见的大数相乘算法还有 快速傅里叶变换算法( fast Fourier transform (FFT))。
这道题除了按普通的竖式求解(即遍历 num2 每一位与 num1 进行相乘,将每一步的结果进行累加)外,还可以可以通过优化竖式来求解,两种优化方法:
一、通过两数相乘时,乘数某位与被乘数某位相乘,与产生结果的位置的规律来完成。
具体规律如下:
乘数
num1位数为 ,被乘数num2位数为 ,num1 x num2结果res最大总位数为 。
num1[i] x num2[j]的结果为tmp(位数为两位,"0x","xy"的形式),其第一位位于res[i+j],第二位位于res[i+j+1]。

二、模拟乘法,将所有数据不单独进位(可直接存入数组),最后统一进位。(实现)

🖌️ 3、最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
方法一:Kadane算法
遍历该数组, 在遍历过程中, 将遍历到的元素依次累加起来, 当累加结果小于或等于 0 时, 从下一个元素开始,重新开始累加。
累加过程中, 要用一个变量 result 记录所获得过的最大值。
一次遍历之后, 变量 result 中存储的即为最大子片段的和值。
理解此算法的关键在于:
最大子片段中不可能包含求和值为负的前缀。 例如 【-2, 1,4】 必然不能是最大子数列, 因为去掉值为负的前缀后【-2,1】, 可以得到一个更大的子数列 【4】、
所以在遍历过程中,每当累加结果成为一个非正值时, 就应当将下一个元素作为潜在最大子数列的起始元素, 重新开始累加。
由于在累加过程中, 出现过的最大值都会被记录, 且每一个可能成为 最大子数列起始元素 的位置, 都会导致新一轮的累加, 这样就保证了答案搜索过程的完备性和正确性。
方法二:动态规划
在每一步,维护两个变量,一个是全局最优,就是到当前元素为止最优的解是,一个是局部最优,就是必须包含当前元素的最优的解。假设我们已知第
i步的global[i](全局最优)和local[i](局部最优),那么第i+1步的表达式是:
local[i+1]=Math.max(A[i], local[i]+A[i]),就是局部最优是一定要包含当前元素,所以不然就是上一步的局部最优local[i]+当前元素A[i](因为local[i]一定包含第i个元素,所以不违反条件),但是如果local[i]是负的,那么他就是不需要的,所以直接用A[i];
global[i+1]=Math(local[i+1],global[i]),有了当前的局部最优,那么全局最优就是当前的局部最优或者还是原来的全局最优。
方法三:分治法
将数组均分为两个部分,那么最大子数组会存在于:
左侧数组的最大子数组
右侧数组的最大子数组
左侧数组的以右侧边界为边界的最大子数组+右侧数组的以左侧边界为边界的最大子数组
假设数组下标有效范围是
l到r,将数组分为左半部分下标为(l,mid-1)和右半部分下标为(mid+1,r)以及中间元素下标为mid,接下来递归求出左半部分的最大子序和:left=helper(nums,l,mid-1);右半部分最大子序和right=helper(nums,mid+1,r);接下来再将左半部分右边界,右半部分左边界以及中间元素
nums[mid]整合,用了两个循环,先整合左半部分右边界和中间值,再将整合结果与右半部分左边界整合得到整合以后的最大子序和max_num,最后返回max_num,left,right的最大值即是要求的最大子序和。
🖌️ 4、寻找两个正序数组的中位数
给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。找出这两个正序数组的中位数,并且要求算法的时间复杂度为 。假设 nums1 和 nums2 不会同时为空。
方法一:走一趟遍历,边界情况很多,复杂度满足要求,但是代码很乱。
最后更新于
这有帮助吗?