1、【分享】常用算法设计方法常用算法设计方法在网上找到这篇常用算法设计方法 ,虽然代码是 C 的,但是算法的原理都一样吧。常用算法设计方法要使计算机能完成人们预定的工作,首先必须为如何完成预定的工作设计一个算法,然后再根据算法编写程序。计算机程序要对问题的每个对象和处理规则给出正确详尽的描述,其中程序的数据结构和变量用来描述问题的对象,程序结构、函数和语句用来描述问题的算法。算法数据结构是程序的两个重要方面。算法是问题求解过程的精确描述,一个算法由有限条可完全机械地执行的、有确定结果的指令组成。指令正确地描述了要完成的任务和它们被执行的顺序。计算机按算法指令所描述的顺序执行算法的指令能在有限的步骤
2、内终止,或终止于给出问题的解,或终止于指出问题对此输入数据无解。通常求解一个问题可能会有多种算法可供选择,选择的主要标准是算法的正确性和可靠性,简单性和易理解性。其次是算法所需要的存储空间少和执行更快等。算法设计是一件非常困难的工作,经常采用的算法设计技术主要有迭代法、穷举搜索法、递推法、贪婪法、回溯法、分治法、动态规划法等等。另外,为了更简洁的形式设计和藐视算法,在算法设计时又常常采用递归技术,用递归描述算法。一、迭代法迭代法是用于求方程或方程组近似根的一种常用的算法设计方法。设方程为 f(x)=0,用某种数学方法导出等价的形式 x=g(x),然后按以下步骤执行:(1) 选一个方程的近似根,
3、赋给变量 x0;(2) 将 x0 的值保存于变量 x1,然后计算 g(x1),并将结果存于变量 x0;(3) 当 x0 与 x1 的差的绝对值还小于指定的精度要求时,重复步骤(2)的计算。若方程有根,并且用上述方法计算出来的近似根序列收敛,则按上述方法求得的 x0 就认为是方程的根。上述算法用 C 程序的形式表示为:【算法】迭代法求方程的根 x0=初始近似根;do x1=x0;x0=g(x1); /*按特定的方程计算新的近似根*/ while ( fabs(x0-x1)Epsilon);printf(“方程的近似根是%fn” ,x0);迭代算法也常用于求方程组的根,令X=(x0,x1,xn-1
4、)设方程组为:xi=gi(X) (I=0,1,n-1)则求方程组根的迭代算法可描述如下:【算法】迭代法求方程组的根 for (i=0;ix=初始近似根;do for (i=0;iy=x;for (i=0;ix=gi(X);for (delta=0.0,i=0;iif (fabs(y-x)delta) delta=fabs(y-x); while (deltaEpsilon);for (i=0;iprintf(“变量 x%d的近似根是 %f” ,I,x);printf(“n”);具体使用迭代法求根时应注意以下两种可能发生的情况:(1) 如果方程无解,算法求出的近似根序列就不会收敛,迭代过程会变成
5、死循环,因此在使用迭代算法前应先考察方程是否有解,并在程序中对迭代的次数给予限制;(2) 方程虽然有解,但迭代公式选择不当,或迭代的初始近似根选择不合理,也会导致迭代失败。二、穷举搜索法穷举搜索法是对可能是解的众多候选解按某种顺序进行逐一枚举和检验,并从众找出那些符合要求的候选解作为问题的解。【问题】 将 A、B、C、D、E、F 这六个变量排成如图所示的三角形,这六个变量分别取1,6上的整数,且均不相同。求使三角形三条边上的变量之和相等的全部解。如图就是一个解。程序引入变量 a、b、c、d、e、f,并让它们分别顺序取 1 至 6 的证书,在它们互不相同的条件下,测试由它们排成的如图所示的三角形
6、三条边上的变量之和是否相等,如相等即为一种满足要求的排列,把它们输出。当这些变量取尽所有的组合后,程序就可得到全部可能的解。细节见下面的程序。【程序 1】# includevoid main() int a,b,c,d,e,f;for (a=1;a0;j-)if (*ptj*ptj-1) break;if (j=0) break;for (i=VARIABLES-1;i=j;i-)if (*pt*pti-1) break;t=*ptj-1;* ptj-1 =* pt; *pt=t;for (i=VARIABLES-1;ij;i-,j+) t=*ptj; *ptj =* pt; *pt=t; 从
7、上述问题解决的方法中,最重要的因素就是确定某种方法来确定所有的候选解。下面再用一个示例来加以说明。【问题】 背包问题问题描述:有不同价值、不同重量的物品 n 件,求从这 n 件物品中选取一部分物品的选择方案,使选中物品的总重量不超过指定的限制重量,但选中物品的价值之和最大。设 n 个物品的重量和价值分别存储于数组 w 和 v 中,限制重量为 tw。考虑一个 n 元组(x0,x1,xn-1) ,其中 xi=0 表示第 i 个物品没有选取,而 xi=1 则表示第 i 个物品被选取。显然这个 n 元组等价于一个选择方案。用枚举法解决背包问题,需要枚举所有的选取方案,而根据上述方法,我们只要枚举所有的
8、 n 元组,就可以得到问题的解。显然,每个分量取值为 0 或 1 的 n 元组的个数共为 2n 个。而每个 n 元组其实对应了一个长度为 n 的二进制数,且这些二进制数的取值范围为 02n-1。因此,如果把 02n-1 分别转化为相应的二进制数,则可以得到我们所需要的 2n 个 n 元组。【算法】maxv=0;for (i=0;imaxv) maxv=temp_v;保存该 B 数组; -三、递推法递推法是利用问题本身所具有的一种递推关系求问题解的一种方法。设要求问题规模为 N的解,当 N=1 时,解或为已知,或能非常方便地得到解。能采用递推法构造算法的问题有重要的递推性质,即当得到问题规模为
9、i-1 的解后,由问题的递推性质,能从已求得的规模为 1,2,i-1 的一系列解,构造出问题规模为 I 的解。这样,程序可从 i=0 或 i=1出发,重复地,由已知至 i-1 规模的解,通过递推,获得规模为 i 的解,直至得到规模为N 的解。【问题】 阶乘计算问题描述:编写程序,对给定的 n(n100) ,计算并输出 k 的阶乘 k!(k=1,2,n)的全部有效数字。由于要求的整数可能大大超出一般整数的位数,程序用一维数组存储长整数,存储长整数数组的每个元素只存储长整数的一位数字。如有 m 位成整数 N 用数组 a 存储:N=am10m-1+am-110m-2+ +a2101+a1100并用
10、a0存储长整数 N 的位数 m,即 a0=m。按上述约定,数组的每个元素存储 k 的阶乘k!的一位数字,并从低位到高位依次存于数组的第二个元素、第三个元素。例如,5!=120,在数组中的存储形式为:3 0 2 1 首元素 3 表示长整数是一个 3 位数,接着是低位到高位依次是 0、2、1,表示成整数 120。计算阶乘 k!可采用对已求得的阶乘(k-1)!连续累加 k-1 次后求得。例如,已知 4!=24,计算 5!,可对原来的 24 累加 4 次 24 后得到 120。细节见以下程序。# include# include# define MAXN 1000void pnext(int a ,i
11、nt k) int *b,m=a0,i,j,r,carry;b=(int * ) malloc(sizeof(int)* (m+1);for ( i=1;i0;i-)printf(“%d”,a);printf(“nn”);void main() int aMAXN,n,k;printf(“Enter the number n: “);scanf(“%d”,a0=1;a1=1;write(a,1);for (k=2;k1 时) 。写成递归函数有:int fib(int n) if (n=0) return 0;if (n=1) return 1;if (n1) return fib(n-1)+f
12、ib(n-2);递归算法的执行过程分递推和回归两个阶段。在递推阶段,把较复杂的问题(规模为 n)的求解推到比原问题简单一些的问题(规模小于 n)的求解。例如上例中,求解 fib(n),把它推到求解 fib(n-1)和 fib(n-2)。也就是说,为计算 fib(n),必须先计算 fib(n-1)和fib(n- 2),而计算 fib(n-1)和 fib(n-2),又必须先计算 fib(n-3)和 fib(n-4)。依次类推,直至计算 fib(1)和 fib(0),分别能立即得到结果 1 和 0。在递推阶段,必须要有终止递归的情况。例如在函数 fib 中,当 n 为 1 和 0 的情况。在回归阶段
13、,当获得最简单情况的解后,逐级返回,依次得到稍复杂问题的解,例如得到fib(1)和 fib(0)后,返回得到 fib(2)的结果,在得到了 fib(n-1)和 fib(n-2)的结果后,返回得到 fib(n)的结果。在编写递归函数时要注意,函数中的局部变量和参数知识局限于当前调用层,当递推进入“简单问题”层时,原来层次上的参数和局部变量便被隐蔽起来。在一系列“简单问题”层,它们各有自己的参数和局部变量。由于递归引起一系列的函数调用,并且可能会有一系列的重复计算,递归算法的执行效率相对较低。当某个递归算法能较方便地转换成递推算法时,通常按递推算法编写程序。例如上例计算斐波那契数列的第 n 项的函
14、数 fib(n)应采用递推算法,即从斐波那契数列的前两项出发,逐次由前两项计算出下一项,直至计算出要求的第 n 项。【问题】 组合问题问题描述:找出从自然数 1、2、n 中任取 r 个数的所有组合。例如 n=5,r=3 的所有组合为: (1)5、4、3 (2)5、4、2 (3)5、4、1(4)5、3、2 (5)5、3、1 (6)5、2、1(7)4、3、2 (8)4、3、1 (9)4、2、1(10)3、2、1分析所列的 10 个组合,可以采用这样的递归思想来考虑求组合函数的算法。设函数为void comb(int m,int k)为找出从自然数 1、2、m 中任取 k 个数的所有组合。当组合的第
15、一个数字选定时,其后的数字是从余下的 m-1 个数中取 k-1 数的组合。这就将求 m 个数中取 k 个数的组合问题转化成求 m-1 个数中取 k-1 个数的组合问题。设函数引入工作数组 a 存放求出的组合的数字,约定函数将确定的 k 个数字组合的第一个数字放在 ak中,当一个组合求出后,才将 a 中的一个组合输出。第一个数可以是 m、m-1、k,函数将确定组合的第一个数字放入数组后,有两种可能的选择,因还未去顶组合的其余元素,继续递归去确定;或因已确定了组合的全部元素,输出这个组合。细节见以下程序中的函数 comb。【程序】# include# define MAXN 100int aMAX
16、N;void comb(int m,int k) int i,j;for (i=m;i=k;i-) ak=i;if (k1)comb(i-1,k-1);else for (j=a0;j0;j-)printf(“%4d”,aj);printf(“n”);void main() a0=3;comb(5,3);【问题】 背包问题问题描述:有不同价值、不同重量的物品 n 件,求从这 n 件物品中选取一部分物品的选择方案,使选中物品的总重量不超过指定的限制重量,但选中物品的价值之和最大。设 n 件物品的重量分别为 w0、w1、wn-1,物品的价值分别为 v0、v1、vn-1。采用递归寻找物品的选择方案。
17、设前面已有了多种选择的方案,并保留了其中总价值最大的方案于数组 option ,该方案的总价值存于变量 maxv。当前正在考察新方案,其物品选择情况保存于数组 cop 。假定当前方案已考虑了前 i-1 件物品,现在要考虑第 i 件物品;当前方案已包含的物品的重量之和为 tw;至此,若其余物品都选择是可能的话,本方案能达到的总价值的期望值为 tv。算法引入 tv 是当一旦当前方案的总价值的期望值也小于前面方案的总价值 maxv 时,继续考察当前方案变成无意义的工作,应终止当前方案,立即去考察下一个方案。因为当方案的总价值不比 maxv 大时,该方案不会被再考察,这同时保证函数后找到的方案一定会比
18、前面的方案更好。对于第 i 件物品的选择考虑有两种可能:(1) 考虑物品 i 被选择,这种可能性仅当包含它不会超过方案总重量限制时才是可行的。选中后,继续递归去考虑其余物品的选择。(2) 考虑物品 i 不被选择,这种可能性仅当不包含物品 i 也有可能会找到价值更大的方案的情况。按以上思想写出递归算法如下:try(物品 i,当前选择已达到的重量和,本方案可能达到的总价值 tv) /*考虑物品 i 包含在当前方案中的可能性*/if(包含物品 i 是可以接受的) 将物品 i 包含在当前方案中;if (itry(i+1,tw+物品 i 的重量,tv);else/*又一个完整方案,因为它比前面的方案好,
19、以它作为最佳方案*/以当前方案作为临时最佳方案保存;恢复物品 i 不包含状态;/*考虑物品 i 不包含在当前方案中的可能性*/if (不包含物品 i 仅是可男考虑的)if (itry(i+1,tw,tv-物品 i 的价值);else/*又一个完整方案,因它比前面的方案好,以它作为最佳方案*/以当前方案作为临时最佳方案保存;为了理解上述算法,特举以下实例。设有 4 件物品,它们的重量和价值见表:物品 0 1 2 3重量 5 3 2 1价值 4 4 3 1并设限制重量为 7。则按以上算法,下图表示找解过程。由图知,一旦找到一个解,算法就进一步找更好的佳。如能判定某个查找分支不会找到更好的解,算法不
20、会在该分支继续查找,而是立即终止该分支,并去考察下一个分支。按上述算法编写函数和程序如下:【程序】# include# define N 100double limitW,totV,maxV;int optionN,copN;struct double weight;double value;aN;int n;void find(int i,double tw,double tv) int k;/*考虑物品 i 包含在当前方案中的可能性*/if (tw+a.weightmaxV)if (ielse for (k=0;koptionk=copk;maxv=tv-a.value;void main
21、() int k;double w,v;printf(“输入物品种数n”);scanf(“%d”,printf(“输入各物品的重量和价值n”);for (totv=0.0,k=0;k scanf(“%1f%1f”,ak.weight=w;ak.value=v;totV+=V;printf(“输入限制重量n”);scanf(“%1f”,maxv=0.0;for (k=0;k find(0,0.0,totV);for (k=0;kif (optionk) printf(“%4d”,k+1);printf(“n 总价值为%.2fn”,maxv);作为对比,下面以同样的解题思想,考虑非递归的程序解。为
22、了提高找解速度,程序不是简单地逐一生成所有候选解,而是从每个物品对候选解的影响来形成值得进一步考虑的候选解,一个候选解是通过依次考察每个物品形成的。对物品 i 的考察有这样几种情况:当该物品被包含在候选解中依旧满足解的总重量的限制,该物品被包含在候选解中是应该继续考虑的;反之,该物品不应该包括在当前正在形成的候选解中。同样地,仅当物品不被包括在候选解中,还是有可能找到比目前临时最佳解更好的候选解时,才去考虑该物品不被包括在候选解中;反之,该物品不包括在当前候选解中的方案也不应继续考虑。对于任一值得继续考虑的方案,程序就去进一步考虑下一个物品。【程序】# include# define N 10
23、0double limitW;int copN;struct ele double weight;double value; aN;int k,n;struct int ;double tw;double tv;twvN;void next(int i,double tw,double tv) twv.=1;twv.tw=tw;twv.tv=tv;double find(struct ele *a,int n) int i,k,f;double maxv,tw,tv,totv;maxv=0;for (totv=0.0,k=0;ktotv+=ak.value;next(0,0.0,totv);i
24、=0;While (i=0) f=twv.;tw=twv.tw;tv=twv.tv;switch(f) case 1: twv.+;if (tw+a.weightmaxv)if (i next(i+1,tw,tv-a.value);i+;else maxv=tv-a.value;for (k=0;kcopk=twvk.!=0;break;return maxv;void main() double maxv;printf(“输入物品种数n”);scanf(“%d”,printf(“输入限制重量n”);scanf(“%1f”,printf(“输入各物品的重量和价值n”);for (k=0;ksc
25、anf(“%1f%1f”,maxv=find(a,n);printf(“n 选中的物品为n”);for (k=0;kif (optionk) printf(“%4d”,k+1);printf(“n 总价值为%.2fn”,maxv); -五、回溯法回溯法也称为试探法,该方法首先暂时放弃关于问题规模大小的限制,并将问题的候选解按某种顺序逐一枚举和检验。当发现当前候选解不可能是解时,就选择下一个候选解;倘若当前候选解除了还不满足问题规模要求外,满足所有其他要求时,继续扩大当前候选解的规模,并继续试探。如果当前候选解满足包括问题规模在内的所有要求时,该候选解就是问题的一个解。在回溯法中,放弃当前候选解
26、,寻找下一个候选解的过程称为回溯。扩大当前候选解的规模,以继续试探的过程称为向前试探。1、回溯法的一般描述可用回溯法求解的问题 P,通常要能表达为:对于已知的由 n 元组(x1,x2,xn)组成的一个状态空间 E=(x1,x2,xn)xiSi ,i=1,2,n,给定关于 n 元组中的一个分量的一个约束集 D,要求 E 中满足 D 的全部约束条件的所有 n 元组。其中 Si 是分量 xi 的定义域,且 |Si| 有限,i=1,2,n。我们称 E 中满足 D 的全部约束条件的任一 n 元组为问题 P 的一个解。解问题 P 的最朴素的方法就是枚举法,即对 E 中的所有 n 元组逐一地检测其是否满足
27、D 的全部约束,若满足,则为问题 P 的一个解。但显然,其计算量是相当大的。我们发现,对于许多问题,所给定的约束集 D 具有完备性,即 i 元组(x1,x2,xi)满足 D 中仅涉及到 x1,x2,xi 的所有约束意味着 j (jj。因此,对于约束集 D 具有完备性的问题 P,一旦检测断定某个 j 元组(x1,x2,xj)违反 D 中仅涉及x1,x2,xj 的一个约束,就可以肯定,以(x1,x2,xj)为前缀的任何 n 元组(x1,x2,xj,xj+1,xn)都不会是问题 P 的解,因而就不必去搜索它们、检测它们。回溯法正是针对这类问题,利用这类问题的上述性质而提出来的比枚举法效率更高的算法。
28、回溯法首先将问题 P 的 n 元组的状态空间 E 表示成一棵高为 n 的带权有序树 T,把在 E 中求问题 P 的所有解转化为在 T 中搜索问题 P 的所有解。树 T 类似于检索树,它可以这样构造:设 Si 中的元素可排成 xi(1) ,xi(2) ,xi(mi-1) ,|Si| =mi,i=1,2,n。从根开始,让 T 的第 I 层的每一个结点都有 mi 个儿子。这 mi 个儿子到它们的双亲的边,按从左到右的次序,分别带权 xi+1 (1) ,xi+1(2) ,xi+1(mi) ,i=0,1,2,n-1。照这种构造方式,E 中的一个 n 元组(x1,x2,xn)对应于 T 中的一个叶子结点,
29、T 的根到这个叶子结点的路径上依次的 n 条边的权分别为 x1,x2,xn,反之亦然。另外,对于任意的 0in-1,E 中 n 元组(x1,x2,xn)的一个前缀 I 元组(x1, x2,xi)对应于 T 中的一个非叶子结点,T 的根到这个非叶子结点的路径上依次的 I条边的权分别为 x1,x2,xi,反之亦然。特别,E 中的任意一个 n 元组的空前缀() ,对应于 T 的根。因而,在 E 中寻找问题 P 的一个解等价于在 T 中搜索一个叶子结点,要求从 T 的根到该叶子结点的路径上依次的 n 条边相应带的 n 个权 x1,x2,xn 满足约束集 D 的全部约束。在 T 中搜索所要求的叶子结点,
30、很自然的一种方式是从根出发,按深度优先的策略逐步深入,即依次搜索满足约束条件的前缀 1 元组(x1i) 、前缀 2 元组(x1,x2) 、,前缀 I 元组(x1,x2,xi) ,直到 i=n 为止。在回溯法中,上述引入的树被称为问题 P 的状态空间树;树 T 上任意一个结点被称为问题P 的状态结点;树 T 上的任意一个叶子结点被称为问题 P 的一个解状态结点;树 T 上满足约束集 D 的全部约束的任意一个叶子结点被称为问题 P 的一个回答状态结点,它对应于问题 P 的一个解。【问题】 组合问题问题描述:找出从自然数 1、2、n 中任取 r 个数的所有组合。例如 n=5,r=3 的所有组合为:(
31、1)1、2、3 (2)1、2、4 (3)1、2、5(4)1、3、4 (5)1、3、5 (6)1、4、5(7)2、3、4 (8)2、3、5 (9)2、4、5(10)3、4、5则该问题的状态空间为:E=(x1,x2,x3)xiS ,i=1,2,3 其中:S=1,2,3,4,5约束集为: x1显然该约束集具有完备性。2、回溯法的方法对于具有完备约束集 D 的一般问题 P 及其相应的状态空间树 T,利用 T 的层次结构和 D 的完备性,在 T 中搜索问题 P 的所有解的回溯法可以形象地描述为:从 T 的根出发,按深度优先的策略,系统地搜索以其为根的子树中可能包含着回答结点的所有状态结点,而跳过对肯定不
32、含回答结点的所有子树的搜索,以提高搜索效率。具体地说,当搜索按深度优先策略到达一个满足 D 中所有有关约束的状态结点时,即“激活”该状态结点,以便继续往深层搜索;否则跳过对以该状态结点为根的子树的搜索,而一边逐层地向该状态结点的祖先结点回溯,一边“杀死”其儿子结点已被搜索遍的祖先结点,直到遇到其儿子结点未被搜索遍的祖先结点,即转向其未被搜索的一个儿子结点继续搜索。在搜索过程中,只要所激活的状态结点又满足终结条件,那么它就是回答结点,应该把它输出或保存。由于在回溯法求解问题时,一般要求出问题的所有解,因此在得到回答结点后,同时也要进行回溯,以便得到问题的其他解,直至回溯到 T 的根且根的所有儿子
33、结点均已被搜索过为止。例如在组合问题中,从 T 的根出发深度优先遍历该树。当遍历到结点(1,2)时,虽然它满足约束条件,但还不是回答结点,则应继续深度遍历;当遍历到叶子结点(1,2,5)时,由于它已是一个回答结点,则保存(或输出)该结点,并回溯到其双亲结点,继续深度遍历;当遍历到结点(1,5)时,由于它已是叶子结点,但不满足约束条件,故也需回溯。3、回溯法的一般流程和技术在用回溯法求解有关问题的过程中,一般是一边建树,一边遍历该树。在回溯法中我们一般采用非递归方法。下面,我们给出回溯法的非递归算法的一般流程:在用回溯法求解问题,也即在遍历状态空间树的过程中,如果采用非递归方法,则我们一般要用到
34、栈的数据结构。这时,不仅可以用栈来表示正在遍历的树的结点,而且可以很方便地表示建立孩子结点和回溯过程。例如在组合问题中,我们用一个一维数组 Stack 表示栈。开始栈空,则表示了树的根结点。如果元素 1 进栈,则表示建立并遍历(1)结点;这时如果元素 2 进栈,则表示建立并遍历(1,2)结点;元素 3 再进栈,则表示建立并遍历(1,2,3)结点。这时可以判断它满足所有约束条件,是问题的一个解,输出(或保存) 。这时只要栈顶元素(3)出栈,即表示从结点(1,2,3)回溯到结点(1,2) 。【问题】 组合问题问题描述:找出从自然数 1,2,n 中任取 r 个数的所有组合。采用回溯法找问题的解,将找
35、到的组合以从小到大顺序存于 a0,a1,ar-1中,组合的元素满足以下性质:(1) ai+1a,后一个数字比前一个大;(2) a-i0;i+)if (m=primes) return 1;for (i=3;i*i=0;i+)if (!isprime(apos+aj)return 0;return 1;int extend(int pos) a+pos=selectnum(1);bapos=0;return pos;int change(int pos) int j;while (pos=0if (pos=0)void main() int i;for (i=1;ivno=i;for (j=box_h;j!=NULL;j=j-next)if (j-remainder=a) break;if (j=NULL) j=(HNODE *)malloc(sizeof(HNODE);j-remainder=box_volume-a;j-head=NULL;if (box_h=NULL) box_h=box_t=j;else box_t=boix_t-next=j;j-next=NULL;box_count+;else j-remainder-=a;for (q=j-next;q!=NULLq=q-link);if (q=NULL) p-link=j-head;j-head=p;