分治法是算法学习中很基础的一部分。
其基本思想是:对一个原规模为n的问题,将其分解为多个规模较小的子问题,再将这些子问题的解合并求得原问题的解。
分治法不仅仅是分(Divide)和治(Conquer),其中还有个“合(Combine)”的步骤经常会被人忽略。这也是分治法的三个基本步骤:Divide-Conquer-Combine。
让我们先看一个分治法运用的经典案例:归并排序。
Problem: 对一个无序序列{x1...xn}按从从小到大排序;
Solution:
Divide: 原问题问n规模,将该问题逐次二分,直至递减为常数规模为止。
Conquer: 将这些小规模的排序解决。
Combine: 一一合并已排序好的小规模序列。
Analysis:
Divide: 对k规模问题二分显然只需求一次中点即可,耗时固定。
Conquer: 假如原问题运算时间是T(n),则对二分后的两个子问题,解决时间为2T(n/2)。
Combine: 对两个已排序好的子序列,Combine的基本步骤如下:
构造一个足够大的数组,从两个子序列的首位开始,依次比较,每次将两个数中较小的数放入构造的数组中。
显然,一共会进行k次比较,k值与两个子序列大小的和有关。也就是。
根据以上的分析,我们可以写出归并算法的递归式(忽略基本情况):
其复杂度是:
接下来,第一个问题:分治的关键在哪里?所有的问题都可以用分治吗?
显然不是这样,仔细观察我们会发现,Divide后产生的所有子问题,它们都具有一种相同的解决模式!这是分治算法生效的关键。
第二个问题:分治似乎总是很好,是不是满足了上述条件后,分治就一定有效呢?
这个问题似乎稍显复杂了一些,让我们举一个很典型的例子来看看:
Problem:
Solution: 这是很常见的求矩阵乘法的问题。
首先介绍一下朴素算法,即利用公式进行计算。显然会有三重循环,也就是复杂度为。
很快就有人根据分治法的思想对此作出了“改进”:
学过线性代数的人应该很清楚矩阵的分块运算,如果将矩阵分块的话,对每一小块矩阵(子问题),其运算方法是相同的(解决模式),这似乎已经满足了使用分治法的条件,但是否分治之后会更加优越呢?让我们来分析一下算法:
,这是基本的分块策略:C、A、B均被分为了4个n/2×n/2的矩阵块。求解结果为:
可以看出,我们要做8次规模为n/2的矩阵乘法,4次加法。现在可以很容易的写出递归式:
糟糕的事情发生了:我们并未使得问题简化!分析递归式会发现,问题出在系数8上,很明显,我们做了过多次的子问题,子问题的数量是影响分治复杂度的一个重要指标!
你会问:有没有办法改善这个算法呢?当然有,在此不作具体讨论,提示:改善的方法就是降低系数。
第三个问题:分治未必是最佳选择?
我想说的是,对一个有效的分治算法,它未必是最好的选择。我们知道,在问题规模比较大时,分治往往能取得很好的效果,但如果问题规模小到一定程度后,分治将不再是最佳选择,事实上,对大部分问题,当规模较小的时候,分治都是较低效的做法。例如:对归并排序,我们可以选择在规模小时采取插入排序解决子问题,而不再递归的进行划分。
四、分治的实现。
分治是基于递归的一种算法,但是分治的实现就一定要用递归吗?在多数情况下,我们更愿意用循环替代递归,递归占用了过多的内存空间,使得算法不够高效。
下面两段代码均是求x^n,函数f使用递归实现,函数g使用循环实现:
public static double f(double x, int n){
if(n>1){
if(n%2==0){
double d = f(x, n/2);
return d*d;
}
else if(n%2!=0){
double d = f(x, (n-1)/2);
return d*d*x;
}
else{
return 99999;
}
}
else{
return x;
}
}
public static double g(double x, int n){
double res=x, p=0;
if(n%2!=0){
n--;
p=1;
}
while(n!=1){
res*=res;
n/=2;
}
if(p==1){
res*=x;
}
return res;
}
明显,非递归方法更加高效且编码更加简洁。