分治(divide-and-conquer)算法
一、概念
百度百科中分治算法的概念为:
分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。即一种分目标完成程序算法,简单问题可用二分法完成。
二、思想或策略
分治算法的主要思想是将原问题分成若干个规模较小的子问题,这些子问题互相独立且与原问题形式相同,直到子问题满足边界条件,递归或迭代地解这些子问题(一般可以直接求解或简单计算)。然后将各子问题的解层层合并得到原问题的解,这种思想或策略就叫做分治法。
【理解】分治字面意思为‘分而治之’,即把一个复杂的问题分解为几个相同或相似的子问题,再把子问题分解为更小的子问题,直到子问题可以简单求解,那么原问题就是这些子问题解的合并。
三、步骤
- 分:递归或迭代地将原问题分解为各个的子问题(性质相同的、相互独立的子问题)
- 治:若子问题规模足够小则直接解决,否则递归解决各子问题
- 合并:逐层合并各子问题的解得到原问题的解
四、分治适用的情况
- 原问题的计算复杂度随着问题的规模的增加而增加
- 原问题可以分解为相同或相似的子问题(类似递归)
- 分解的子问题小到一定程度可以轻易解决
- 子问题的解可以合并成整个问题的解
- 各个子问题相互独立,即不包含公共子问题
【关键】
- 能否利用分治法完全取决于问题是否具有第四条特征,如果具备了第一、二和三条特征,而不具备第四条特征,则可以考虑用贪心法或动态规划法。
- 第五条涉及分治的效率,独立子问题分别求解可以免去很多判断条件以及不必要的工作,如果是重复解决公共子问题还是使用DP算法好。
五、C++实现分治算法的一般模板
void divided_conquer(problem,param1,param2,...){
if(达到解决小问题的条件){
//直接返回 或 简单计算得到子问题结果
}
mid=pre_proc(problem); //准备划分数据(一般是中值或者是将原问题分割的部分值)
subProblem=split_proc(problem,mid); //根据mid(划分条件)将原问题划分为子问题
sub_res1=divided_conquer(subProblem[0],p1,p2...) //递归或迭代处理子问题,得到子问题结果
sub_res2=divided_conquer(subProblem[1],p1,p2...)
......
res=merge_proc(sub_res1,sub_res2,...); //合并子问题的解得到原问题的最终解
}
六、C++实现LeetCode分治相关例题
6.1 Leetcode50–Pow(x,n)
- 题目描述
实现 pow(x, n) ,即计算 x 的 n 次幂函数。
示例 1:
输入: 2.00000, 10
输出: 1024.00000
示例 2:
输入: 2.10000, 3
输出: 9.26100
示例 3:
输入: 2.00000, -2
输出: 0.25000
解释: 2-2 = 1/22 = 1/4 = 0.25
说明:
-100.0 < x < 100.0
n 是 32 位有符号整数,其数值范围是 [−231, 231 − 1] 。
-
解题思路
1、达到解决最小子问题的条件:n不断除以2直到为0或1,为0时返回1(任何数的0次幂为1)、为1时返回x本身(任何数的一次幂为其本身)。
2、原问题分解为子问题:n不断取半,即n=n/2;
3、合并子问题结果:
(1)n为偶数:计算pow(x,n/2)的平方并返回;
(2)n为奇数:计算pow(x,n/2)的平方在乘以一个x并返回
【注】n这里需要先判断正负数,如果为负数,需要n=-n处理。 -
C++实现代码
class Solution {
public:
double myPow(double x, int n) { //分治+递归
if(x==1 || n==0)
return 1;
long long N = n;
return N >= 0 ? helper(x, N) : 1.0 / helper(x, -N);
}
double divide_conquer(double x,long long n){ //【注】这里 n要定义成long long 才能通过提交
if(n == 1)
return x;
if(n%2 ==0){ //偶数
double res=divide_conquer(x,n/2);
return res*res;
}
else{ //奇数
double res=divide_conquer(x,n/2);
return res*res*x;
}
}
};
6.2 Leetcode169–多数元素
- 题目描述
给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入: [3,2,3]
输出: 3
示例 2:
输入: [2,2,1,1,1,2,2]
输出: 2
-
解题思路
1、达到解决最小子问题的条件: nums数组长度为1时,最大子段和就是本身,直接返回。
2、原问题分解为子问题:二分法,每次去nums数组中点,将数组分为左右两个区间,
3、合并子问题结果:分为三种情况
(1)长度为 1 的子数组中唯一的数显然是众数,直接返回即可;
(2)如果它们的众数相同,那么显然这一段区间的众数是它们相同的值;
(3)如果他们的众数不同,比较两个众数在整个区间内出现的次数来决定该区间的众数。 -
C++实现代码
class Solution {
public:
int majorityElement(vector<int>& nums) {
return divide_conquer(nums,0,nums.size()-1);
}
//分治法+递归
int divide_conquer(vector<int>& nums,int i,int j) {
if(i == j) //当区间长度为1时,直接返回该元素
return nums[i];
int mid=(i+j)/2;
int left=divide_conquer(nums,i,mid);
int right=divide_conquer(nums,mid+1,j);
if(count(nums.begin()+i,nums.begin()+mid,left) > (j-i+1)/2)
return left;
if(count(nums.begin()+mid+1,nums.begin()+j,right) > (j-i+1)/2)
return right;
return -1;
}
int count_in_range(vector<int>& nums, int target, int i, int j) {
int count = 0;
for (int k = i; k <= j; ++k)
if (nums[k] == target)
++count;
return count;
}
};
6.3 Leetcode53–最大子序和
- 问题描述
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
-
解题思路
1、达到解决最小子问题的条件: nums数组长度为1时,最大子段和就是本身,直接返回。
2、原问题分解为子问题:二分法,每次去nums数组中点,将数组分为左右两个区间,
3、合并子问题结果:分为三种情况
(1)最大子段和在左区间
(2)最大子段和在右区间
(3)最大子段和横跨两个区间
最终返回左区间的元素、右区间的元素、以及整个区间(相对子问题)和的最大值即可 -
C++实现代码
class Solution {
public:
int maxSubArray(vector<int>& nums) {
return divided_conquer(nums,0,nums.size()-1);
}
// 分治法 求解
int divided_conquer(vector<int>& nums,int left,int right){
if(left == right) //子问题求解条件
return nums[left];
int mid=(left+right)/2;
int left_sum=divided_conquer(nums,left,mid); //左边区间最大子段和
int right_sum=divided_conquer(nums,mid+1,right); //右边区间最大子段和
int mid_sum=crossSubarray(nums,left,mid,right); //横跨左右两区间的连续最大子段和
int res=max(left_sum,right_sum);
return max(res,mid_sum);
}
int crossSubarray(vector<int>& nums,int left,int mid,int right){
//因为mid_sum包含左右两个区间,所以mid一定含有,然后分别(1)左区间从右往左扩展;(2)右区间从左到右扩展
int left_sum=INT_MIN;
int sum=0;
for(int i=mid;i>=left;i--){ //(1)左区间从右往左扩展
sum+=nums[i];
left_sum=max(sum,left_sum);
}
int right_sum=INT_MIN;
sum=0;
for(int j=mid+1;j<=right;j++){ //(2)右区间从左到右扩展
sum+=nums[j];
right_sum=max(right_sum,sum);
}
return (left_sum+right_sum);
}
};
这里由于子区间有重叠的情况,我们使用DP算法将两者做一个对比:
DP算法代码为:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
//动态规划:关键在确定状态
//状态i表示:以i为结尾的最大连续子数组
//状态转移方程:dp[i]=max(dp[i-1]+nums[i],nums[i])
vector<int> dp(nums.size(),0); //初始化为0
dp[0]=nums[0];
int max_res=dp[0];
for(int i=1;i<nums.size();i++){
dp[i]=max(dp[i-1]+nums[i],nums[i]);
if(dp[i]>max_res)
max_res=dp[i]; //遍历nums时,不断更新max_res,直到取得最大连续子段和
}
return max_res;
}
};
提交结果:
可见子区间有重叠时,使用分治的效率反而会比较低!
七、总结
分治算法一般用来解决数据规模比较大、子区间不重叠的情况,用分治算法解决问题的关键在于如何合并子问题,这也是分治区别于DP和贪心的主要特征,像Pow(x,n)和多数元素这两个例题,可以在各自的子区间得到子问题的解然后逐层合并,而最大子序和例题则需要考虑两个子区间重叠的情况,所以她用DP算法来求解效果会好一些!