- 数据结构与算法(C语言版)
- 胡明 王红梅编著
- 2052字
- 2020-08-27 10:10:19
2.3 减治法
2.3.1 减治法的设计思想
减治法(reduceandconquermethod)在将原问题分解为若干个子问题后,利用了原问题的解与子问题的解之间的关系,这种关系通常表现为:
(1)原问题的解只存在于其中一个较小规模的子问题中;
(2)原问题的解与其中一个较小规模的解之间存在某种对应关系。
由于原问题的解与较小规模的子问题的解之间存在这种关系,所以,只需求解其中一个较小规模的子问题就可以得到原问题的解,无须对子问题的解进行合并。因此,严格地说,减治法应该是一种退化了的分治法。减治法主要有以下三种类型:
(1)减常量:算法每次迭代总是从实例规模中减去一个相同的常量,一般来说,这个常量等于1,如图2-4所示。在本书中,直接插入排序、拓扑排序等都是减1技术的应用实例。
图2-4 减1技术
(2)减常数因子:算法每次迭代总是从实例规模中减去一个相同的常数因子,一般来说,这个常数因子等于2(即减半),如图2-5所示。在本书中,折半查找、平衡二叉树的查找、B树的查找、堆调整等都是减半技术的应用实例。
图2-5 减半技术
(3)减可变规模:算法每次迭代时减去的规模都是不同的。在本书中,欧几里得算法、二叉排序树的查找等都是减可变规模的应用实例。
例如,对于给定的整数a和非负整数n,利用减治法计算an的值,如果n=1,可以简单地返回a的值;如果n是偶数并且n>1,可以把该问题的规模减半,即计算an/2的值。而且规模为n的解an和规模减半的解an/2之间具有明显的对应关系:an=(an/2)2。如果n是奇数并且n>1,可以先用偶指数的规则计算an-1,再把结果乘以a。所以,应用减治技术得到如下递推关系式:
显然,上述算法的时间复杂度是O(log2n)。注意该算法和2.2节介绍的分治法不同,分治法是对分解的子问题分别求解,再对子问题的解进行合并,而减治法只对一个子问题求解,并且不需要进行解的合并,因此,应用减治技术设计的算法通常具有很高的效率,一般来说其时间复杂度是O(log2n)。
2.3.2 算法设计实例——假币问题
【问题】 在n枚外观相同的硬币中有一枚是假币,并且已知假币较轻。可以通过一架天平来任意比较两组硬币,从而得知两组硬币的重量是否相同,或者哪一组更轻一些。假币问题是要求设计一个高效的算法来找出这枚假币。
【想法】 问题的解决是经过一系列比较和判断,最自然的想法就是一分为二,也就是把n枚硬币分成两组,每组有└n/2」枚硬币,如果n为奇数,就留下一枚硬币,然后把两组硬币分别放到天平的两端。如果两组硬币的重量相同,那么留下的硬币就是假币;否则,用同样的方法对较轻的那组硬币进行同样的处理,因为假币一定在较轻的那组里。由于每次用天平比较后,只需解决一个规模减半的问题,所以,它属于减治算法。该算法在最坏情况下的时间性能满足如下递推式:
根据1.4.3节通用分治递推式的定理,得到T(n)=O(log2n)。
实际上,减半不是一个最好的选择。考虑不是把硬币分成两组,而是分成三组,前两组有「n/3┐个硬币,其余的硬币作为第三组;将前两组硬币放到天平上,如果它们的重量相同,则假币一定在第三组中,用同样的方法对第三组进行处理;如果前两组的重量不同,则假币一定在较轻的那一组中,用同样的方法对较轻的那组硬币进行处理。显然这个算法存在递推式:
这个递推式的解是T(n)=O(log3n)。这个减治法是将原问题一分为三,从而获得了更少的比较次数。
【算法】 采用递归技术设计假币问题的算法,设函数Coin实现假币问题,算法用伪代码描述如下 :
算法:Coin(low,high,n)
输入:硬币所在数组的下标范围low和high,硬币的个数n
输出:假币在硬币集合中的序号
1.如果n等于1,则该硬币即为假币,输出对应的序号,算法结束;
2.计算3组的硬币个数num1、num2和num3;
3.add1=第1组硬币的重量和;add2=第2组硬币的重量和;
4.根据情况执行下述三种操作之一:
4.1 如果add1小于add2,则在第1组硬币中查找;
4.2 如果add1大于add2,则在第2组硬币中查找;
4.3 如果add1等于add2,则在第3组硬币中查找;
【程序】 设N枚硬币的重量存储在数组a[N]中,函数Coin实现假币问题的求解,参数low和high分别表示假币所在的数组下标范围,为避免传递数组参数,将a[N]设为全局变量。程序如下:
#include<stdio.h> //使用库函数printf constintN=8; //假设求解8枚硬币问题 inta[N]={2,2,1,2,2,2,2,2}; //真币的重量是2,假币的重量是1 intCoin(intlow,inthigh,intn); //函数声明 //以下是主函数
intmain() { inti=Coin(0,7,8); //初始调用,在a[0]~a[7]中查找假币 printf("第%d枚硬币是假币\n",i);//输出假币的序号 return0; //将0返回操作系统,表明程序正常结束 } //以下是其他函数定义 intCoin(intlow,inthigh,intn) //在a[low]~a[high]中查找假币 { inti,num1,num2,num3; //num1、num2和num3存储3组硬币的个数 intadd1=0,add2=0; //add1和add2存储前两组硬币的重量和 if(n==1) //递归结束的条件 returnlow+1; //返回的是序号,即下标加1 if(n% 3==0) //3组硬币的个数相同 num1=num2=n/3; else //前两组有「n/3┐组硬币 num1=num2=n/3+1; num3=n-num1-num2; for(i=0;i<num1;i++) //计算第1组硬币的重量和 add1=add1+a[low+i]; for(i=num1;i<num1+num2;i++) //计算第2组硬币的重量和 add2=add2+a[low+i]; if(add1<add2) //在第1组查找,下标范围是low~low+num1-1 returnCoin(low,low+num1-1,num1); elseif(add1>add2) //在第2组查找,下标范围low+num1~low+num1+num2-1 returnreturnCoin(low+num1,low+num1+num2-1,num2); else //在第3组查找,下标范围low+num1+num2~high Coin(low+num1+num2,high,num3); }