分治法.docx
《分治法.docx》由会员分享,可在线阅读,更多相关《分治法.docx(36页珍藏版)》请在冰豆网上搜索。
分治法
常用算法大全-分而治之算法
君主和殖民者们所成功运用的分而治之策略也可以运用到高效率的计算机算法的设计过程中。
本章将首先介绍怎样在算法设计领域应用这一古老的策略,然后将利用这一策略解决如下问题:
最小最大问题、矩阵乘法、残缺棋盘、排序、选择和计算一个几何问题——找出二维空间中距离最近的两个点。
本章给出了用来分析分而治之算法复杂性的数学方法,并通过推导最小最大问题和排序问题的复杂性下限来证明分而治之算法对于求解这两种问题是最优的(因为算法的复杂性与下限一致)。
2.1算法思想
分而治之方法与软件设计的模块化方法非常相似。
为了解决一个大的问题,可以:
1)把它分成两个或多个更小的问题;2)分别解决每个小问题;3)把各小问题的解答组合起来,即可得到原问题的解答。
小问题通常与原问题相似,可以递归地使用分而治之策略来解决。
例2-1[找出伪币]给你一个装有16个硬币的袋子。
16个硬币中有一个是伪造的,并且那个伪造的硬币比真的硬币要轻一些。
你的任务是找出这个伪造的硬币。
为了帮助你完成这一任务,将提供一台可用来比较两组硬币重量的仪器,利用这台仪器,可以知道两组硬币的重量是否相同。
比较硬币1与硬币2的重量。
假如硬币1比硬币2轻,则硬币1是伪造的;假如硬币2比硬币1轻,则硬币2是伪造的。
这样就完成了任务。
假如两硬币重量相等,则比较硬币3和硬币4。
同样,假如有一个硬币轻一些,则寻找伪币的任务完成。
假如两硬币重量相等,则继续比较硬币5和硬币6。
按照这种方式,可以最多通过8次比较来判断伪币的存在并找出这一伪币。
另外一种方法就是利用分而治之方法。
假如把16硬币的例子看成一个大的问题。
第一步,把这一问题分成两个小问题。
随机选择8个硬币作为第一组称为A组,剩下的8个硬币作为第二组称为B组。
这样,就把16个硬币的问题分成两个8硬币的问题来解决。
第二步,判断A和B组中是否有伪币。
可以利用仪器来比较A组硬币和B组硬币的重量。
假如两组硬币重量相等,则可以判断伪币不存在。
假如两组硬币重量不相等,则存在伪币,并且可以判断它位于较轻的那一组硬币中。
最后,在第三步中,用第二步的结果得出原先16个硬币问题的答案。
若仅仅判断硬币是否存在,则第三步非常简单。
无论A组还是B组中有伪币,都可以推断这16个硬币中存在伪币。
因此,仅仅通过一次重量的比较,就可以判断伪币是否存在。
现在假设需要识别出这一伪币。
把两个或三个硬币的情况作为不可再分的小问题。
注意如果只有一个硬币,那么不能判断出它是否就是伪币。
在一个小问题中,通过将一个硬币分别与其他两个硬币比较,最多比较两次就可以找到伪币。
这样,16硬币的问题就被分为两个8硬币(A组和B组)的问题。
通过比较这两组硬币的重量,可以判断伪币是否存在。
如果没有伪币,则算法终止。
否则,继续划分这两组硬币来寻找伪币。
假设B是轻的那一组,因此再把它分成两组,每组有4个硬币。
称其中一组为B1,另一组为B2。
比较这两组,肯定有一组轻一些。
如果B1轻,则伪币在B1中,再将B1又分成两组,每组有两个硬币,称其中一组为B1a,另一组为B1b。
比较这两组,可以得到一个较轻的组。
由于这个组只有两个硬币,因此不必再细分。
比较组中两个硬币的重量,可以立即知道哪一个硬币轻一些。
较轻的硬币就是所要找的伪币。
例2-2[金块问题]有一个老板有一袋金块。
每个月将有两名雇员会因其优异的表现分别被奖励一个金块。
按规矩,排名第一的雇员将得到袋中最重的金块,排名第二的雇员将得到袋中最轻的金块。
根据这种方式,除非有新的金块加入袋中,否则第一名雇员所得到的金块总是比第二名雇员所得到的金块重。
如果有新的金块周期性的加入袋中,则每个月都必须找出最轻和最重的金块。
假设有一台比较重量的仪器,我们希望用最少的比较次数找出最轻和最重的金块。
假设袋中有n个金块。
可以用函数Max(程序1-31)通过n-1次比较找到最重的金块。
找到最重的金块后,可以从余下的n-1个金块中用类似的方法通过n-2次比较找出最轻的金块。
这样,比较的总次数为2n-3。
程序2-26和2-27是另外两种方法,前者需要进行2n-2次比较,后者最多需要进行2n-2次比较。
下面用分而治之方法对这个问题进行求解。
当n很小时,比如说,n≤2,识别出最重和最轻的金块,一次比较就足够了。
当n较大时(n>2),第一步,把这袋金块平分成两个小袋A和B。
第二步,分别找出在A和B中最重和最轻的金块。
设A中最重和最轻的金块分别为HA与LA,以此类推,B中最重和最轻的金块分别为HB和LB。
第三步,通过比较HA和HB,可以找到所有金块中最重的;通过比较LA和LB,可以找到所有金块中最轻的。
在第二步中,若n>2,则递归地应用分而治之方法。
假设n=8。
这个袋子被平分为各有4个金块的两个袋子A和B。
为了在A中找出最重和最轻的金块,A中的4个金块被分成两组A1和A2。
每一组有两个金块,可以用一次比较在A中找出较重的金块HA1和较轻的金块LA1。
经过另外一次比较,又能找出HA2和LA2。
现在通过比较HA1和HA2,能找出HA;通过LA1和LA2的比较找出LA。
这样,通过4次比较可以找到HA和LA。
同样需要另外4次比较来确定HB和LB。
通过比较HA和HB(LA和LB),就能找出所有金块中最重和最轻的。
因此,当n=8时,这种分而治之的方法需要10次比较。
如果使用程序1-31,则需要13次比较。
如果使用程序2-26和2-27,则最多需要14次比较。
设c(n)为使用分而治之方法所需要的比较次数。
为了简便,假设n是2的幂。
当n=2时,c(n)=1。
对于较大的n,c(n)=2c(n/2)+2。
当n是2的幂时,使用迭代方法(见例2-20)可知
c(n)=3n/2-2。
在本例中,使用分而治之方法比逐个比较的方法少用了25%的比较次数。
例2-3[矩阵乘法]两个n×n阶的矩阵A与B的乘积是另一个n×n阶矩阵C,C可表示为假如每一个C(i,j)都用此公式计算,则计算C所需要的操作次数为n3m+n2(n-1)a,其中m表示一次乘法,a表示一次加法或减法。
为了得到两个矩阵相乘的分而治之算法,需要:
1)定义一个小问题,并指明小问题是如何进行乘法运算的;2)确定如何把一个大的问题划分成较小的问题,并指明如何对这些较小的问题进行乘法运算;3)最后指出如何根据小问题的结果得到大问题的结果。
为了使讨论简便,假设n是2的幂(也就是说,n是1,2,4,8,16,.)。
首先,假设n=1时是一个小问题,n>1时为一个大问题。
后面将根据需要随时修改这个假设。
对于1×1阶的小矩阵,可以通过将两矩阵中的两个元素直接相乘而得到结果。
考察一个n>1的大问题。
可以将这样的矩阵分成4个n/2×n/2阶的矩阵A1,A2,A3,和A4。
当n大于1且n是2的幂时,n/2也是2的幂。
因此较小矩阵也满足前面对矩阵大小的假设。
矩阵Bi和Ci的定义与此类似.
根据上述公式,经过8次n/2×n/2阶矩阵乘法和4次n/2×n/2阶矩阵的加法,就可以计算出A与B的乘积。
因此,这些公式能帮助我们实现分而治之算法。
在算法的第二步,将递归使用分而治之算法把8个小矩阵再细分(见程序2-19)。
算法的复杂性为(n3),此复杂性与程序2-24直接使用公式(2-1)所得到的复杂性是一样的。
事实上,由于矩阵分割和再组合所花费的额外开销,使用分而治之算法得出结果的时间将比用程序2-24还要长。
为了得到更快的算法,需要简化矩阵分割和再组合这两个步骤。
一种方案是使用Strassen方法得到7个小矩阵。
这7个小矩阵为矩阵D,E,.,J,矩阵D到J可以通过7次矩阵乘法,6次矩阵加法,和4次矩阵减法计算得出。
前述的4个小矩阵可以由矩阵D到J通过6次矩阵加法和两次矩阵减法得出.
用上述方案来解决n=2的矩阵乘法。
将某矩阵A和B相乘得结果C,如下所示:
因为n>1,所以将A、B两矩阵分别划分为4个小矩阵,每个矩阵为1×1阶,仅包含一个元素。
1×1阶矩阵的乘法为小问题,因此可以直接进行运算。
利用计算D~J的公式,得:
D=1(6-8)=-2
E=4(7-5)=8
F=(3+4)5=35
G=(1+2)8=24
H=(3-1)(5+6)=22
I=(2-4)(7+8)=-30
J=(1+4)(5+8)=65
根据以上结果可得:
对于上面这个2×2的例子,使用分而治之算法需要7次乘法和18次加/减法运算。
而直接使用公式(2-1),则需要8次乘法和7次加/减法。
要想使分而治之算法更快一些,则一次乘法所花费的时间必须比11次加/减法的时间要长。
假定Strassen矩阵分割方案仅用于n≥8的矩阵乘法,而对于n<8的矩阵乘法则直接利用公式(2-1)进行计算。
则n=8时,8×8矩阵相乘需要7次4×4矩阵乘法和18次4×4矩阵加/减法。
每次矩阵乘法需花费64m+48a次操作,每次矩阵加法或减法需花费16a次操作。
因此总的操作次数为7(64m+48a)+18(16a)=448m+624a。
而使用直接计算方法,则需要512m+448a次操作。
要使Strassen方法比直接计算方法快,至少要求512-448次乘法的开销比624-448次加/减法的开销大。
或者说一次乘法的开销应该大于近似2.75次加/减法的开销。
假定n<16的矩阵是一个“小”问题,Strassen的分解方案仅仅用于n≥16的情况,对于n<16的矩阵相乘,直接利用公式(2-1)。
则当n=16时使用分而治之算法需要7(512m+448a)+18(64a)=3584m+4288a次操作。
直接计算时需要4096m+3840a次操作。
若一次乘法的开销与一次加/减法的开销相同,则Strassen方法需要7872次操作及用于问题分解的额外时间,而直接计算方法则需要7936次操作加上程序中执行for循环以及其他语句所花费的时间。
即使直接计算方法所需要的操作次数比Strassen方法少,但由于直接计算方法需要更多的额外开销,因此它也不见得会比Strassen方法快。
n的值越大,Strassen方法与直接计算方法所用的操作次数的差异就越大,因此对于足够大的n,Strassen方法将更快。
设t(n)表示使用Strassen分而治之方法所需的时间。
因为大的矩阵会被递归地分割成小矩阵直到每个矩阵的大小小于或等于k(k至少为8,也许更大,具体值由计算机的性能决定).用迭代方法计算,可得t(n)=(nlog27)。
因为log27≈2.81,所以与直接计算方法的复杂性(n3)相比,分而治之矩阵乘法算法有较大的改进。
注意事项
分而治之方法很自然地导致了递归算法的使用。
在许多例子里,这些递归算法在递归程序中得到了很好的运用。
实际上,在许多情况下,所有为了得到一个非递归程序的企图都会导致采用一个模拟递归栈。
不过在有些情况下,不使用这样的递归栈而采用一个非递归程序来完成分而治之算法也是可能的,并且在这种方式下,程序得到结果的速度会比递归方式更快。
解决金块问题的分而治之算法(例2-2)和归并排序方法(2.3节)就可以不利用递归而通过一个非递归程序来更快地完成。
例2-4[金块问题]用例2-2的算法寻找8个金块中最轻和最重金块的工作可以用二叉树来表示。
这棵树的叶子分别表示8个金块(a,b,.,h),每个阴影节点表示一个包含其子树中所有叶子的问题。
因此,根节点A表示寻找8个金块中最轻、最重金块的问题,而节点B表示找出a,b,c和d这4个金块中最轻和最重金块的问题。
算法从根节点开始。
由根节点表示的8金块问题被划分成由节点B和C所表示的两个4金块问题。
在B节点,4金块问题被划分成由D和E所表示的2金块问题。
可通过比较金块a和b哪一个较重来解决D节点所表示的2金块问题。
在解决了D和E所表示的问题之后,可以通过比较D和E中所找到的轻金块和重金块来解决B表示的问题。
接着在F,G和C上重复这一过程,最后解决问题A。
可以将递归的分而治之算法划分成以下的步骤:
1)从图2-2中的二叉树由根至叶的过程中把一个大问题划分成许多个小问题,小问题的大小为1或2。
2)比较每个大小为2的问题中的金块,确定哪一个较重和哪一个较轻。
在节点D、E、F和G上完成这种比较。
大小为1的问题中只有一个金块,它既是最轻的金块也是最重的金块。
3)对较轻的金块进行比较以确定哪一个金块最轻,对较重的金块进行比较以确定哪一个金块最重。
对于节点A到C执行这种比较。
根据上述步骤,可以得出程序14-1的非递归代码。
该程序用于寻找到数组w[0:
n-1]中的最小数和最大数,若n<1,则程序返回false,否则返回true。
当n≥1时,程序14-1给Min和Max置初值以使w[Min]是最小的重量,w[Max]为最大的重量。
首先处理n≤1的情况。
若n>1且为奇数,第一个重量w[0]将成为最小值和最大值的候选值,因此将有偶数个重量值w[1:
n-1]参与for循环。
当n是偶数时,首先将两个重量值放在for循环外进行比较,较小和较大的重量值分别置为Min和Max,因此也有偶数个重量值w[2:
n-1]参与for循环。
在for循环中,外层if通过比较确定(w[i],w[i+1])中的较大和较小者。
此工作与前面提到的分而治之算法步骤中的2)相对应,而内层的if负责找出较小重量值和较大重量值中的最小值和
最大值,这个工作对应于3)。
for循环将每一对重量值中较小值和较大值分别与当前的最小值w[Min]和最大值w[Max]进行比较,根据比较结果来修改Min和Max(如果必要)。
下面进行复杂性分析。
注意到当n为偶数时,在for循环外部将执行一次比较而在for循环内部执行3(n/2-1)次比较,比较的总次数为3n/2-2。
当n为奇数时,for循环外部没有执行比较,而内部执行了3(n-1)/2次比较。
因此无论n为奇数或偶数,当n>0时,比较的总次数为「3n/2ù-2次。
程序14-1找出最小值和最大值的非递归程序
template
boolMinMax(Tw[],intn,T&Min,T&Max)
{//寻找w[0:
n-1]中的最小和最大值
//如果少于一个元素,则返回false
//特殊情形:
n<=1
if(n<1)returnfalse;
if(n==1){Min=Max=0;
returntrue;}
//对Min和Max进行初始化
ints;//循环起点
if(n%2){//n为奇数
Min=Max=0;
s=1;}
else{//n为偶数,比较第一对
if(w[0]>w[1]){
Min=1;
Max=0;}
else{Min=0;
Max=1;}
s=2;}
//比较余下的数对
for(inti=s;i//寻找w[i]和w[i+1]中的较大者
//然后将较大者与w[Max]进行比较
//将较小者与w[Min]进行比较
if(w[i]>w[i+1]){
if(w[i]>w[Max])Max=i;
if(w[i+1]else{
if(w[i+1]>w[Max])Max=i+1;
if(w[i]}
returntrue;
}
练习
1.将例14-1的分而治之算法扩充到n>1个硬币的情形。
需要进行多少次重量的比较?
2.考虑例14-1的伪币问题。
假设把条件“伪币比真币轻”改为“伪币与真币的重量不同”,同样假定袋中有n个硬币。
1)给出相应分而治之算法的形式化描述,该算法可输出信息“不存在伪币”或找出伪币。
算法应递归地将大的问题划分成两个较小的问题。
需要多少次比较才能找到伪币(如果存在伪币)?
2)重复1),但把大问题划分为三个较小问题。
3.1)编写一个C++程序,实现例14-2中寻找n个元素中最大值和最小值的两种方案。
使用递归来完成分而治之方案。
2)程序2-26和2-27是另外两个寻找n个元素中最大值和最小值的代码。
试分别计算出每段程序所需要的最少和最大比较次数。
3)在n分别等于100,1000或10000的情况下,比较1)、2)中的程序和程序14-1的运行时间。
对于程序2-27,使用平均时间和最坏情况下的时间。
1)中的程序和程序2-26应具有相同的平均时间和最坏情况下的时间。
4)注意到如果比较操作的开销不是很高,分而治之算法在最坏情况下不会比其他算法优越,为什么?
它的平均时间优于程序2-27吗?
为什么?
4.证明直接运用公式(14-2)~(14-5)得出结果的矩阵乘法的分而治之算法的复杂性为(n3)。
因此相应的分而治之程序将比程序2-24要慢。
5.用迭代的方法来证明公式(14-6)的递归值为(nlog27)。
*6.编写Strassen矩阵乘法程序。
利用不同的k值(见公式(14-6))进行实验,以确定k为何值时程序性能最佳。
比较程序及程序2-24的运行时间。
可取n为2的幂来进行比较。
7.当n不是2的幂时,可以通过增加矩阵的行和列来得到一个大小为2的幂的矩阵。
假设使用最少的行数和列数将矩阵扩充为m阶矩阵,其中m为2的幂。
1)求m/n。
2)可使用哪些矩阵项组成新的行和列,以使新矩阵A'和B'相乘时,原来的矩阵A和B相乘的结果会出现在C'的左上角?
3)使用Strassen方法计算A'*B'所需要的时间为(m2.81)。
给出以n为变量的运行时间表达式。
2.2应用
2.2.1残缺棋盘
残缺棋盘(defectivechessboard)是一个有2k×2k个方格的棋盘,其中恰有一个方格残缺。
图2-3给出k≤2时各种可能的残缺棋盘,其中残缺的方格用阴影表示。
注意当k=0时,仅存在一种可能的残缺棋盘(如图14-3a所示)。
事实上,对于任意k,恰好存在22k种不同的残缺棋盘。
残缺棋盘的问题要求用三格板(triominoes)覆盖残缺棋盘(如图14-4所示)。
在此覆盖中,两个三格板不能重叠,三格板不能覆盖残缺方格,但必须覆盖其他所有的方格。
在这种限制条件下,所需要的三格板总数为(22k-1)/3。
可以验证(22k-1)/3是一个整数。
k为0的残缺棋盘很容易被覆盖,因为它没有非残缺的方格,用于覆盖的三格板的数目为0。
当k=1时,正好存在3个非残缺的方格,并且这三个方格可用图14-4中的某一方向的三格板来覆盖。
用分而治之方法可以很好地解决残缺棋盘问题。
这一方法可将覆盖2k×2k残缺棋盘的问题转化为覆盖较小残缺棋盘的问题。
2k×2k棋盘一个很自然的划分方法就是将它划分为如图14-5a所示的4个2k-1×2k-1棋盘。
注意到当完成这种划分后,4个小棋盘中仅仅有一个棋盘存在残缺方格(因为原来的2k×2k棋盘仅仅有一个残缺方格)。
首先覆盖其中包含残缺方格的2k-1×2k-1残缺棋盘,然后把剩下的3个小棋盘转变为残缺棋盘,为此将一个三格板放在由这3个小棋盘形成的角上,如图14-5b所示,其中原2k×2k棋盘中的残缺方格落入左上角的2k-1×2k-1棋盘。
可以采用这种分割技术递归地覆盖2k×2k残缺棋盘。
当棋盘的大小减为1×1时,递归过程终止。
此时1×1的棋盘中仅仅包含一个方格且此方格残缺,所以无需放置三格板。
可以将上述分而治之算法编写成一个递归的C++函数TileBoard(见程序14-2)。
该函数定义了一个全局的二维整数数组变量Board来表示棋盘。
Board[0][0]表示棋盘中左上角的方格。
该函数还定义了一个全局整数变量tile,其初始值为0。
函数的输入参数如下:
?
tr棋盘中左上角方格所在行。
?
tc棋盘中左上角方格所在列。
?
dr残缺方块所在行。
?
dl残缺方块所在列。
?
size棋盘的行数或列数。
TileBoard函数的调用格式为TileBoard(0,0,dr,dc,size),其中size=2k。
覆盖残缺棋盘所需要的三格板数目为(size2-1)/3。
函数TileBoard用整数1到(size2-1)/3来表示这些三格板,并用三格板的标号来标记被该三格板覆盖的非残缺方格。
令t(k)为函数TileBoard覆盖一个2k×2k残缺棋盘所需要的时间。
当k=0时,size等于1,覆盖它将花费常数时间d。
当k>0时,将进行4次递归的函数调用,这些调用需花费的时间为4t(k-1)。
除了这些时间外,if条件测试和覆盖3个非残缺方格也需要时间,假设用常数c表示这些额外时间。
可以得到以下递归表达式:
程序14-2覆盖残缺棋盘
voidTileBoard(inttr,inttc,intdr,intdc,intsize)
{//覆盖残缺棋盘
if(size==1)return;
intt=tile++,//所使用的三格板的数目
s=size/2;//象限大小
//覆盖左上象限
if(dr
//残缺方格位于本象限
TileBoard(tr,tc,dr,dc,s);
else{//本象限中没有残缺方格
//把三格板t放在右下角
Board[tr+s-1][tc+s-1]=t;
//覆盖其余部分
TileBoard(tr,tc,tr+s-1,tc+s-1,s);}
//覆盖右上象限
if(dr
|
=tc+s)//残缺方格位于本象限
TileBoard(tr,tc+s,dr,dc,s);
else{//本象限中没有残缺方格
//把三格板t放在左下角
Board[tr+s-1][tc+s]=t;
//覆盖其余部分
TileBoard(tr,tc+s,tr+s-1,tc+s,s);}
//覆盖左下象限
if(dr>=tr+s&&dc//残缺方格位于本象
展开阅读全文
相关搜索