分治算法
一 基本概念
《算法导论》中,提到:许多有用的算法在结构上都是递归的:为了解决一个给定的问题,算法一次或多次递归地调用其自身以解决紧密相关的若干子问题。这些算法典型地遵循分治法的思想:将原问题分解为几个规模较小但类似于原问题的子问题,递归地求解这些子问题,然后再合并这些子问题的解来建立原问题的解。(分解–解决–合并)
在计算机科学中,分治法是一种很重要的算法,能大大降低算法的时间复杂度。但其Divide–Conquer–Combine的思想其实很简单。分而治之的思想是很多高效算法的基础,如排序算法(快速排序,归并排序),二叉树遍历,傅立叶变换(快速傅立叶变换)……
任何一个可以用计算机求解的问题所需的计算时间都与其规模有关。问题的规模越小,越容易直接求解,解题所需的计算时间也越少。例如,对于n个元素的排序问题,当n=1时,不需任何计算。n=2时,只要作一次比较即可排好序。n=3时只要作3次比较即可,…。而当n较大时,问题就不那么容易处理了。要想直接解决一个规模较大的问题,有时是相当困难的。
二 策略
分治策略是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。
如果原问题可分割成k个子问题,1 < k ≤ n,且这些子问题都可解并可利用这些子问题的解求出原问题的解,那么这种分治法就是可行的。由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。
三 分治法适用的情况
分治法所能解决的问题一般具有以下几个特征:
1) 该问题的规模缩小到一定的程度就可以容易地解决
2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
3) 利用该问题分解出的子问题的解可以合并为该问题的解;
4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;
第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;
第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。
第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。
四 分治法的基本步骤
分治法在每一层递归上都有三个步骤:
- Divide:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题(注意,子问题最好具有相等的规模,事实上,一般也是这样来分的,而且分为两个实例的居多);
- Conquer:若子问题的规模较小而容易被解决则直接解,否则递归地解决各个子问题;
- Combine:将各个子问题的解合并为原问题的解(事实上,一个分治算法的精华在于合并解的过程)。
分治法的一般的算法设计模式如下:
Divide-and-Conquer(P)
1. if |P| ≤n0
2. then return (ADHOC(P))
3. 将P分解为较小的子问题P1, P2, …, Pk
4. for i <– 1 to k
5. do yi <– Divide-and-Conquer(Pi)△ 递归解决Pi
6. T <– MERGE(y1, y2, …, yk) △ 合并子问题
7. return(T)
其中|P|表示问题P的规模;n0为一阈值,表示当问题P的规模不超过n0时,问题已容易直接解决,不必再继续分解。ADHOC(P)是该分治法中的基本子算法,用来直接解规模较小的问题P。因此,当P的规模不超过n0时直接用算法ADHOC(P)求解。算法MERGE(y1, y2, …, yk)是该分治法中的合并子算法,用于将P的子问题P1,P2, …, Pk的解合并成P的解。
五 分治法的复杂性分析
一个分治法将规模为n的问题划分为b个规模为n/b的实例,其中a个实例是需要求解的,用f(n)表示将求解得到的a个子问题的解合并起来的时间复杂度。则有:
T(n) = aT(n/b)+f(n)
可以根据主定理求解该算法的时间复杂度。
这里对主定理做一些分析:
这个式子是一个通用的数学表达式,在计算机的常用算法策略中,它太概括了,我们往往用到的只是它范围很小的一部分。
a和b的关系,其实a绝大多数时候都是等于b的(因为规模n划分为了b个子规模,需要处理的是a个),a,b的含义告诉我们a <= b(当这是最基本要满足的条件)。常常要么是 a==b(分成的子规模都要处理,然后去合并),要么是a==1(实际上这是减治的思想,分成了b个子规模,但最终却可以排除其他的,只在其中一个子规模中去处理)。一般来说就是这样,所以a,b并不是随意的。
其实b要么为2(几乎所有情况)要么为3(极少数情况),这个分治思想里面就说啦,一般来说都是分为2个规模相等的子规模(当然谁都想分的越多越好,这样算法就更快,但是现实是问题往往没有那么高效的算法,找到一个3的分治就已经很不错了)
f(n)是线性的的情况很多(即d=1的情况是最多的)。再来看看a和b^d比较大小的关系说明了什么:f(n)代表的是合并的复杂度,1<=a <= b,定性的分析,可以知道:
a < b^d
因为1<=a <= b,所以只要d>1,不管a,b是什么(不管怎么划分规模,也不管需要处理几个规模),总是第一种情况,时间复杂度是n^d。如果d=1呢,只要a < b(处理的比划分的少),那么还是第一种情况,时间复杂度也是n^d = na = b^d
因为1<=a <= b,所以如果a=b(划分多少处理多少),那么d只能为1才能是这种情况。—–常见的归并排序都是这样
而如果a < b,那d就只能<1才能是这种情况,一般很少见。a > b^d
非常少见,我还木有见过这样的算法,一开始我认为这种情况不可能,但在理论上它是存在的。因为1<=a <= b,所以要满足这个式子d必须<1 。从这也可以看出这三个参数之间的关系,事实上是划分的复杂度和合并的复杂度在争抢复杂度的控制权。
说了这么多,感觉越分析越复杂了吗,其实不是,把这些分析想清楚,对递推式的理解就更进一步了,有了上述分析,其实下面几个常见的递推式就包含了大多数的算法。T(n) = a * T(n/b) + f(n)的常见式子:
T(n) = 2 * T(n/2) + O(n)
时间复杂度n*log(n)。一般来说分治算法就是这样,分成2个子规模的问题,需要处理的也是2个,对这两个子规模合并又是线性的
a = b = 2, d = 1; a == b^d 由主定理得n*log(n)。只要a=b,d=1,就都是这个复杂度。T(n) = T(n/2) + O(n)
时间复杂度为n,线性的。a = 1,b = 2,d=1(分为2个子规模,但只对一个子规模处理,合并也是线性的)。a < b^d, 时间复杂度是n^d = n。其实只要a < b,d=1,都是这个。
感觉对一个定理解读了这么多,确实让它变得更加复杂了,但如果你做了上述思考,相信对这个式子认识也更加深刻了一点。当然,如果你觉得直接记住上面这个公式就可以了,可以无视以上解读。
六 可使用分治法求解的一些经典问题
- 二分搜索
- 大整数乘法
- Strassen矩阵乘法
- 棋盘覆盖
- 合并排序
- 快速排序
- 线性时间选择
- 最接近点对问题
- 循环赛日程表
- 汉诺塔
七 依据分治法设计程序时的思维过程
实际上就是类似于数学归纳法,找到解决本问题的求解方程公式,然后根据方程公式设计递归程序。
1. 一定是先找到最小问题规模时的求解方法
2. 然后考虑随着问题规模增大时的求解方法
3. 找到求解的递归函数式后(各种规模或因子),设计递归程序即可。
这篇博文基本复制的下面一篇文章:
http://www.cnblogs.com/steven_oyj/archive/2010/05/22/1741370.html