分而治之算法应用.docx
- 文档编号:23349799
- 上传时间:2023-05-16
- 格式:DOCX
- 页数:27
- 大小:33.09KB
分而治之算法应用.docx
《分而治之算法应用.docx》由会员分享,可在线阅读,更多相关《分而治之算法应用.docx(27页珍藏版)》请在冰豆网上搜索。
分而治之算法应用
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
//残缺方格位于本象限
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 //残缺方格位于本象限 TileBoard(tr+s,tc,dr,dc,s); else{//把三格板t放在右上角 Board[tr+s][tc+s-1]=t; //覆盖其余部分 TileBoard(tr+s,tc,tr+s,tc+s-1,s);} //覆盖右下象限 if(dr>=tr+s&&dc>=tc+s) //残缺方格位于本象限 TileBoard(tr+s,tc+s,dr,dc,s); else{//把三格板t放在左上角 Board[tr+s][tc+s]=t; //覆盖其余部分 TileBoard(tr+s,tc+s,tr+s,tc+s,s);} } voidOutputBoard(intsize) { for(inti=0;i for(intj=0;j cout< cout< } } 可以用迭代的方法来计算这个表达式(见例2-20),可得t(k)=(4k)=(所需的三格板的数目)。 由于必须花费至少 (1)的时间来放置每一块三格表,因此不可能得到一个比分而治之算法更快的算法。 2.2.2归并排序 可以运用分而治之方法来解决排序问题,该问题是将n个元素排成非递减顺序。 分而治之方法通常用以下的步骤来进行排序算法: 若n为1,算法终止;否则,将这一元素集合分割成两个或更多个子集合,对每一个子集合分别排序,然后将排好序的子集合归并为一个集合。 假设仅将n个元素的集合分成两个子集合。 现在需要确定如何进行子集合的划分。 一种可能性就是把前面n-1个元素放到第一个子集中(称为A),最后一个元素放到第二个子集里(称为B)。 按照这种方式对A递归地进行排序。 由于B仅含一个元素,所以它已经排序完毕,在A排完序后,只需要用程序2-10中的函数insert将A和B合并起来。 把这种排序算法与InsertionSort(见程序2-15)进行比较,可以发现这种排序算法实际上就是插入排序的递归算法。 该算法的复杂性为O(n2)。 把n个元素划分成两个子集合的另一种方法是将含有最大值的元素放入B,剩下的放入A中。 然后A被递归排序。 为了合并排序后的A和B,只需要将B添加到A中即可。 假如用函数Max(见程序1-31)来找出最大元素,这种排序算法实际上就是SelectionSort(见程序2-7)的递归算法。 假如用冒泡过程(见程序2-8)来寻找最大元素并把它移到最右边的位置,这种排序算法就是BubbleSort(见程序2-9)的递归算法。 这两种递归排序算法的复杂性均为(n2)。 若一旦发现A已经被排好序就终止对A进行递归分割,则算法的复杂性为O(n2)(见例2-16和2-17)。 上述分割方案将n个元素分成两个极不平衡的集合A和B。 A有n-1个元素,而B仅含一个元素。 下面来看一看采用平衡分割法会发生什么情况: A集合中含有n/k个元素,B中包含其余的元素。 递归地使用分而治之方法对A和B进行排序。 然后采用一个被称之为归并(merge)的过程,将已排好序的A和B合并成一个集合。 例2-5考虑8个元素,值分别为[10,4,6,3,8,2,5,7]。 如果选定k=2,则[10,4,6,3]和[8,2,5,7]将被分别独立地排序。 结果分别为[3,4,6,10]和[2,5,7,8]。 从两个序列的头部开始归并这两个已排序的序列。 元素2比3更小,被移到结果序列;3与5进行比较,3被移入结果序列;4与5比较,4被放入结果序列;5和6比较,.。 如果选择k=4,则序列[10,4]和[6,3,8,2,5,7]将被排序。 排序结果分别为[4,10]和[2,3,5,6,7,8]。 当这两个排好序的序列被归并后,即可得所需要的排序序列。 图2-6给出了分而治之排序算法的伪代码。 算法中子集合的数目为2,A中含有n/k个元素。 template voidsort(TE,intn) {//对E中的n个元素进行排序,k为全局变量 if(n>=k){ i=n/k; j=n-i; 令A包含E中的前i个元素 令B包含E中余下的j个元素 sort(A,i); sort(B,j); merge(A,B,E,i,j,);//把A和B合并到E } else使用插入排序算法对E进行排序 } 图14-6分而治之排序算法的伪代码 从对归并过程的简略描述中,可以明显地看出归并n个元素所需要的时间为O(n)。 设t(n)为分而治之排序算法(如图14-6所示)在最坏情况下所需花费的时间,则有以下递推公式: 其中c和d为常数。 当n/k≈n-n/k时,t(n)的值最小。 因此当k=2时,也就是说,当两个子集合所包含的元素个数近似相等时,t(n)最小,即当所划分的子集合大小接近时,分而治之算法通常具有最佳性能。 可以用迭代方法来计算这一递推方式,结果为t(n)=(nlogn)。 虽然这个结果是在n为2的幂时得到的,但对于所有的n,这一结果也是有效的,因为t(n)是n的非递减函数。 t(n)=(nlogn)给出了归并排序的最好和最坏情况下的复杂性。 由于最好和最坏情况下的复杂性是一样的,因此归并排序的平均复杂性为t(n)=(nlogn)。 图2-6中k=2的排序方法被称为归并排序(mergesort),或更精确地说是二路归并排序(two-waymergesort)。 下面根据图14-6中k=2的情况(归并排序)来编写对n个元素进行排序的C++函数。 一种最简单的方法就是将元素存储在链表中(即作为类chain的成员(程序3-8))。 在这种情况下,通过移到第n/2个节点并打断此链,可将E分成两个大致相等的链表。 归并过程应能将两个已排序的链表归并在一起。 如果希望把所得到C++程序与堆排序和插入排序进行性能比较,那么就不能使用链表来实现归并排序,因为后两种排序方法中都没有使用链表。 为了能与前面讨论过的排序函数作比较,归并排序函数必须用一个数组a来存储元素集合E,并在a中返回排序后的元素序列。 为此按照下述过程来对图14-6的伪代码进行细化: 当集合E被化分成两个子集合时,可以不必把两个子集合的元素分别复制到A和B中,只需简单地在集合E中保持两个子集合的左右边界即可。 接下来对a中的初始序列进行排序,并将所得到的排序序列归并到一个新数组b中,最后将它们复制到a中。 图14-6的改进版见图14-7。 template MergeSort(Ta[],intleft,intright) {//对a[left: right]中的元素进行排序 if(left inti=(left+right)/2;//中心位置 MergeSort(a,left,i); MergeSort(a,i+1,right); Merge(a,b,left,i,right);//从a合并到b Copy(b,a,left,right);//结果放回a } } 图14-7分而治之排序算法的改进 可以从很多方面来改进图14-7的性能,例如,可以容易地消除递归。 如果仔细地检查图14-7中的程序,就会发现其中的递归只是简单地重复分割元素序列,直到序列的长度变成1为止。 当序列的长度变为1时即可进行归并操作,这个过程可以用n为2的幂来很好地描述。 长度为1的序列被归并为长度为2的有序序列;长度为2的序列接着被归并为长度为4的有序序列;这个过程不断地重复直到归并为长度为n的序列。 图14-8给出n=8时的归并(和复制)过程,方括号表示一个已排序序列的首和尾。 初始序列[8][4][5][6][2][1][7][3] 归并到b[48][56][12][37] 复制到a[48][56][12][37] 归并到b[4568][1237] 复制到a[4568][1237] 归并到b[12345678] 复制到a[12345678] 图14-8归并排序的例子 另一种二路归并排序算法是这样的: 首先将每两个相邻的大小为1的子序列归并,然后对上一次归并所得到的大小为2的子序列进行相邻归并,如此反复,直至最后归并到一个序列,归并过程完成。 通过轮流地将元素从a归并到b并从b归并到a,可以虚拟地消除复制过程。 二路归并排序算法见程序14-3。 程序14-3二路归并排序 template voidMergeSort(Ta[],intn) {//使用归并排序算法对a[0: n-1]进行排序 T*b=newT[n]; ints=1;//段的大小 while(s MergePass(a,b,s,n);//从a归并到b s+=s; MergePass(b,a,s,n);//从b归并到a s+=s; } } 为了完成排序代码,首先需要完成函数MergePass。 函数MergePass(见程序14-4)仅用来确定欲归并子序列的左端和右端,实际的归并工作由函数Merge(见程序14-5)来完成。 函数Merge要求针对类型T定义一个操作符<=。 如果需要排序的数据类型是用户自定义类型,则必须重载操作符<=。 这种设计方法允许我们按元素的任一个域进行排序。 重载操作符<=的目的是用来比较需要排序的域。 程序14-4MergePass函数 template voidMergePass(Tx[],Ty[],ints,intn) {//归并大小为s的相邻段 inti=0; while(i<=n-2*s){ //归并两个大小为s的相邻段 Merge(x,y,i,i+s-1,i+2*s-1); i=i+2*s; } //剩下不足2个元素 if(i+s elsefor(intj=i;j<=n-1;j++) //把最后一段复制到y y[j]=x[j]; } 程序14-5Merge函数 template voidMerge(Tc[],Td[],intl,intm,intr) {//把c[l: m]]和c[m: r]归并到d[l: r]. inti=l,//第一段的游标 j=m+1,//第二段的游标 k=l;//结果的游标 //只要在段中存在i和j,则不断进行归并 while((i<=m)&&(j<=r)) if(c[i]<=c[j])d[k++]=c[i++]; elsed[k++]=c[j++]; //考虑余下的部分 if(i>m)for(intq=j;q<=r;q++) d[k++]=c[q]; elsefor(intq=i;q<=m;q++) d[k++]=c[q]; } 自然归并排序(naturalmergesort)是基本归并排序(见程序14-3)的一种变化。 它首先对输入序列中已经存在的有序子序列进行归并。 例如,元素序列[4,8,3,7,1,5,6,2]中包含有序的子序列[4,8],[3,7],[1,5,6]和[2],这些子序列是按从左至右的顺序对元素表进行扫描而产生的,若位置i的元素比位置i+1的元素大,则从位置i进行分割。 对于上面这个元素序列,可找到四个子序列,子序列1和子序列2归并可得[3,4,7,8],子序列3和子序列4归并可得[1,2,5,6],最后,归并这两个子序列得到[1,2,3,4,5,6,7,8]。 因此,对于上述元素序列,仅仅使用了两趟归并,而程序14-3从大小为1的子序列开始,需使用三趟归并。 作为一个极端的例子,假设输入的元素序列已经排好序并有n个元素,自然归并排序法将准确地识别该序列不必进行归并排序,但程序14-3仍需要进行[log2n]趟归并。 因此自然归并排序将在(n)的时间内完成排序。 而程序14-3将花费(nlogn)的时间。 2.2.3快速排序 分而治之方法还可以用于实现另一种完全不同的排序方法,这种排序法称为快速排序(quicksort)。 在这种方法中,n个元素被分成三段(组): 左段left,右段right和中段middle。 中段仅包含一个元素。 左段中各元素都小于等于中段元素,右段中各元素都大于等于中段元素。 因此left和right中的元素可以独立排序,并且不必对left和right的排序结果进行合并。 middle中的元素被称为支点(pivot)。 图14-9中给出了快速排序的伪代码。 //使用快速排序方法对a[0: n-1]排序 从a[0: n-1]中选择一个元素作为middle,该元素为支点 把余下的元素分割为两段left和right,使得left中的元素都小于等于支点,而right中的元素都大于等于支点 递归地使用快速排序方法对left进行排序 递归地使用快速排序方法对right进行排序 所得结果为left+middle+right 图14-9快速排序的伪代码 考察元素序列[4,8,3,7,1,5,6,2]。 假设选择元素6作为支点,则6位于middle;4,3,1,5,2位于left;8,7位于right。 当left排好序后,所得结果为1,2,3,4,5;当right排好序后,所得结果为7,8。 把right中的元素放在支点元素之后,left中的元素放在支点元素之前,即可得到最终的结果[1,2,3,4,5,6,7,8]。 把元素序列划分为left、middle和right可以就地进行(见程序14-6)。 在程序14-6中,支点总是取位置1中的元素。 也可以采用其他选择方式来提高排序性能,本章稍后部分将给出这样一种选择。 程序14-6快速排序 template voidQuickSort(T*a,intn) {//对a[0: n-1]进行快速排序 {//要求a[n]必需有最大关键值 quickSort(a,0,n-1); template voidquickSort(Ta[],intl,intr) {//排序a[l: r],a[r+1]有大值 if(l>=r)return; inti=l,//从左至右的游标 j=r+1;//从右到左的游标 Tpivot=a[l]; //把左侧>=pivot的元素与右侧<=pivot的元素进行交换 while(true){ do{//在左侧寻找>=pivot的元素 i=i+1; }while(a[i] do{//在右侧寻找<=pivot的元素 j=j-1; }while(a[j]>pivot); if(i>=j)break;//未发现交换对象 Swap(a[i],a[j]); } //设置pivot a[l]=a[j]; a[j]=pivot; quickSort(a,l,j-1);//对左段排序 quickSort(a,j+1,r);//对右段排序 } 若把程序14-6中do-while条件内的<号和>号分别修改为<=和>=,程序14-6仍然正确。 实验结果表明使用程序14-6的快速排序代码可以得到比较好的平均性能。 为了消除程序中的递归,必须引入堆栈。 不过,消除最后一个递归调用不须使用堆栈。 消除递归调用的工作留作练习(练习13)。 程序14-6所需要的递归栈空间为O(n)。 若使用堆栈来模拟递归,则可以把这个空间减少为O(logn)。 在模拟过程中,首先对left和right中较小者进行排序,把较大者的边界放入堆栈中。 在最坏情况下left总是为空,快速排序所需的计算时间为(n2)。 在最好情况下,left和right中的元素数目大致相同,快速排序的复杂性为(nlogn)。 令人吃惊的是,快速排序的平均复杂性也是(nlogn)。 定理2-1快速排序的平均复杂性为(nlogn)。 证明用t(n)代表对含有n个元素的数组进行排序的平均时间。 当n≤1时,t(n)≤d,d为某一常数。 当n<1时,用s表示左段所含元素的个数。 由于在中段中有一个支点元素,因此右段中元素的个数为n-s-1。 所以左段和右段的平均排序时间分别为t(s),t(n-s-1)。 分割数组中元素所需要的时间用cn表示,其中c是一个常数。 因为s有同等机会取0~n-1中的任何一个值. 如对(2-8)式中的n使用归纳法,可得到t(n)≤knlogen,其中n>1且k=2(c+d),e~2.718为自然对数的基底。 在归纳开始时首先验证n=2时公式的正确性。 根据公式(14 如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。 copyright@ 2008-2022 冰点文档网站版权所有 经营许可证编号:鄂ICP备2022015515号-1