经典算法C语言.docx
《经典算法C语言.docx》由会员分享,可在线阅读,更多相关《经典算法C语言.docx(22页珍藏版)》请在冰豆网上搜索。
经典算法C语言
知道为什么计算机能高效准确的帮助我们解决复杂的问题吗?
你可能会回答因为它是电脑呀,但实际上真正的原因是因为我们给它注入了我们人类的解决问题的思路,否则它也是什么事情也不能做的哟。
在前面我们学习了给计算机注入我们思路的工具-C语言,下面我们就来学习一些高效的解决问题的思路……
第6章常用算法
【教学提示】 从给定的问题入手,先想想其解决思路,再试着将思路用书面的形式表述出来,然后用C语言让计算机求解该问题,最后想想在现实生活当中还有什么问题可以用此类思路来解决,从而总结出此思路可解决问题的特性。
【教学要求】 培养学生逻辑思维能力和推导能力,掌握高效准确的解决较为复杂问题的思路。
【主要内容】 迭代法,穷举搜索法,递推法,递归法,回溯法,贪婪法的解决问题的思路,C语言实现的源代码以及思考。
6.1 算法概述
要使计算机能完成人们预定的工作,首先必须为如何完成预定的工作设计一个算法,然后再根据算法编写程序指令。
计算机程序要对问题的每个对象和处理规则给出正确详尽的描述,其中程序的数据结构和变量用来描述问题的对象,程序结构、函数和语句用来描述问题的算法。
算法数据结构是程序的两个重要方面。
算法是问题求解过程的精确描述,一个算法由有限条可完全机械地执行的、有确定结果的指令组成。
指令正确地描述了要完成的任务和它们被执行的顺序。
计算机按算法指令所描述的顺序执行算法的指令能在有限的步骤内终止,或终止于给出问题的解,或终止于指出问题对此输入数据无解。
通常求解一个问题可能会有多种算法可供选择,选择的主要标准是算法的正确性和可靠性,简单性和易理解性。
其次是算法所需要的存储空间少和执行更快等。
算法设计是一件非常困难的工作,经常采用的算法设计技术主要有迭代法、穷举搜索法、递推法、贪婪法、回溯法、分治法、动态规划法等等。
另外,为了更简洁的形式设计和藐视算法,在算法设计时又常常采用递归技术,用递归描述算法。
6.2迭代法
迭代法是用于求方程或方程组近似根的一种常用的算法设计方法。
设方程为f(x)=0,可采用某种数学方法导出其等价的形式x=g(x),然后按以下步骤执行:
(1)选一个方程的近似根,赋给变量x0;
(2)将x0的值保存于变量x1,然后计算g(x1),并将结果存于变量x0;
(3)当x0与x1的差的绝对值还小于指定的精度要求时,重复步骤
(2)的计算。
否则认为x0为原方程的近似根。
若方程有根,并且用上述方法计算出来的近似根序列收敛,则按上述方法求得的x0就认为是方程的根。
上述算法用C程序的形式表示为:
【算法】迭代法求方程的根
{x0=初始近似根;
do{
x1=x0;
x0=g(x1);/*按特定的方程计算新的近似根*/
}while(fabs(x0-x1)>Epsilon);
printf(“方程的近似根是%f\n”,x0);
}
【例6-1】用迭代法求
-2
-4x-7=0在[3,4]的解,要求精度为0.00001。
问题分析按照上面的解题思路,我们先将方程转化为等价的形式x=2+4/x+7/
。
从题目上看,要求方程在区间3到4的解,则我们不妨可令初始值x0为3.1,然后按照迭代法的思路编制程序如下:
main()
{
doublex0,x1,epsilon=1e-5;
x0=3.1;
do{
x1=x0;
x0=2+4/x1+7/(x1*x1);/*按特定的方程计算新的近似根*/
}while(fabs(x0-x1)>epsilon);
printf(“近似根是%.5f”,x1);
}
求的近似值为:
3.63199。
想一想 迭代公式采用x=(
-2
-7)/4行吗?
不妨试一试。
另外迭代算法也常用于求方程组的根,设方程组为:
xi=gi(X)(I=0,1,…,n-1)
令初始值为:
X=(x0,x1,…,xn-1)
则求方程组近似根的迭代算法可描述如下:
【算法】迭代法求方程组的根
{for(i=0;i<=n-1;i++) x[i]=初始近似根;
do{
for(i=0;i<=n-1;i++) y[i]=x[i];
for(i=0;i<=n-1;i++) x[i]=gi(X);
for(delta=0.0,i=0;i<=n-1;i++)
if(fabs(y[i]-x[i])>delta)
delta=fabs(y[i]-x[i]);
}while(delta>Epsilon);
for(i=0;i<=n-1;i++)
printf(“变量x[%d]的近似根是%f”,i,x[i]);
printf(“\n”);
}
提醒 具体使用迭代法求根时应注意以下两种可能发生的情况:
●如果方程无解,算法求出的近似根序列就不会收敛,迭代过程会变成死循环,因此在使用迭代算法前应先考察方程是否有解,并在程序中对迭代的次数给予限制;
●方程虽然有解,但迭代公式选择不当,或迭代的初始近似根选择不合理,也会导致迭代失败。
6.3穷举搜索法
穷举搜索法是对可能是解的众多候选解按某种顺序进行逐一枚举和检验,并从众找出那些符合要求的候选解作为问题的解的方法。
【例6-2】找出n个自然数(1,2,3,…,n)中r个数的组合。
例如,当n=5,r=3时,所有组合为:
543
542
541
532
531
521
432
431
421
321
问题分析求n个数中r个数的组合,其中每r个数中,数不能相同。
另外,任何两组组合的数,所包含的数也不应相同。
例如,5、4、3与3、4、5。
为此,约定前一个数应大于后一个数。
将上述两条不允许为条件,当r=3时,可用三重循环进行搜索
程序:
main()
{
constintn=5,r=3;
inti,j,k,t;
t=0;
for(i=n;i>=r;i--)
for(j=i-1;j>=r-1;j--)
for(k=j-1;k>=1;j--)
{
t=t+1;
printf(“%4d%4d%4d”,i,j,k);
}
}
想一想 假如有n=9,r=5此问题的求解会发生什么变化?
知识点播 用穷举搜索法解决问题一般都有一个问题,当r变化时,循环重数改变,这就影响了这一问题的解的通用性,即没有一般性。
但是,很多情况下穷举搜索法还是常用的。
提醒 用穷举搜索法解决问题,最重要的因素就是确定某种方法来确定所有的候选解。
下面用一个示例来加以说明。
【问题】背包问题
问题描述有不同价值、不同重量的物品n件,求从这n件物品中选取一部分物品的选择方案,使选中物品的总重量不超过指定的限制重量,但选中物品的价值之和最大。
问题分析设n个物品的重量和价值分别存储于数组w[]和v[]中,限制重量为tw。
考虑一个n元组(x0,x1,…,xn-1),其中xi=0表示第i个物品没有选取,而xi=1则表示第i个物品被选取。
显然这个n元组等价于一个选择方案。
用枚举法解决背包问题,需要枚举所有的选取方案,而根据上述方法,我们只要枚举所有的n元组,就可以得到问题的解。
显然,每个分量取值为0或1的n元组的个数共为2n个。
而每个n元组其实对应了一个长度为n的二进制数,且这些二进制数的取值范围为0~2n-1。
因此,如果把0~2n-1分别转化为相应的二进制数,则可以得到我们所需要的2n个n元组。
【算法】
maxv=0;
for(i=0;i<2n;i++)
{
B[0..n-1]=0;
把i转化为二进制数,存储于数组B中;
temp_w=0;
temp_v=0;
for(j=0;j
{
if(B[j]==1)
{
temp_w=temp_w+w[j];
temp_v=temp_v+v[j];
}
if((temp_w<=tw)&&(temp_v>maxv))
{
maxv=temp_v;
保存该B数组;
}
}
}
试一试读者自行将此问题的解决思路用C语言实现。
6.4递推法
递推法的解决问题是指在前面已知的一个或几个结果的基础上推导出下一个结果,从而推导出最终解的解决方法。
【例6-3】有一数列,第一个数是2,第二个数是3,第三个是前两个数之和,以后每个数都是其前两个数之和,要求输出该数列的前N(10)项。
问题分析显然这是一个典型的递推问题,从第三个数开始,每个数都是其前两个数之和,即要想知道第N个数,必须先知道第N-2个和第N-1个数,然后才能求第N个数。
这样的问题可以利用循环结构,从已知数开始,循环计算,直到计算出第N个数。
此例可以先把已知两个数分别放置到变量A和B中,显示A和B,再计算C=A+B得到第三个数,输出第三个数C;要得到第四个数,应该把C和B相加,但是为了减少变量的使用和简化程序,我们可以把B的值移到A中(A中原来的数据已经无用),让出B变量,再把C中的数据移到B中,再计算C=A+B,输出C的值,得到第四个数;再把B的值移到A中,……,如此循环,可以得到数列的N个数。
基本算法与程序实现:
#defineN10
main()
{
inti,A=2,B=3,C;
C=A+B;
printf(“%4d\n%4d\n”,A,B);
for(i=3;i<=N;i++)
{
printf(“%4d\n”,C);
A=B;B=C;
C=A+B;
}
}
6.5递归法
递归法是将问题递推到比原问题更简单的问题上求解,然后再回归到原问题上的一种解决问题的方法,是“递推”和“回归”两个部分的简称。
知识点播 递推:
就是为得到问题的解,将它推到比原问题简单的问题的求解。
我们先看看下面的数学中的递归定义例子:
比如:
为求N!
,先求(N-1)!
,因为(N-1)!
更接近以知解0!
=1;
再看看下面的过程:
3!
=3*2!
;2!
=2*1!
;1!
=1*0!
;0!
=1;
提醒
●递推应有终止之时。
例如,N!
,当N=0时,0!
=1为递推的终止条件,在此条件下,问题的解是明确的,缺少终止条件算法会失败。
●简单问题表示离递推终止条件更接近的问题。
简单问题与原问题其解的算法是一致的,其差别主要反映在参数值上。
例如,N!
与(N-1)!
其参数差1。
参数的变化,使问题递推到有明确解的问题上。
知识点播 回归:
指简单问题得到解后,回归到原问题的解上来。
例如:
当计算完(N-1)!
后,回归计算N*(N-1)!
,即得N!
。
注意:
回归是递推的返回过程。
比如:
0!
=1;1!
=1*0!
;2!
=2*1!
;3!
=3*2!
;
【例6-4】N!
的递归函数定义。
源程序参考:
longf(inti)
{
if(i==0)
return1;
else
returni*(i-1);
}
试一试
Fibonacci数定义如下:
F(0)=0;
F
(1)=1;
F(N)=F(N-1)+F(N-2)(N>1时)
6.6回溯法
回溯法是一个既带有系统性又带有跳跃性的的搜索算法。
知识点播回溯法的基本思想是在包含问题的所有解的解空间树中,按照深度优先的策略,从根结点出发搜索解空间树。
算法搜索至解空间树的任一结点时,总是先判断该结点是否肯定不包含问题的解。
如果肯定不包含,则跳过对以该结点为根的子树的系统搜索,逐层向其祖先结点回溯。
否则,进入该子树,继续按深度优先的策略进行搜索。
回溯法在用来求问题的所有解时,要回溯到根,且根结点的所有子树都已被搜索遍才结束。
而回溯法在用来求问题的任一解时,只要搜索到问题的一个解就可以结束。
这种以深度优先的方式系统地搜索问题的解的算法称为回溯法,它适用于解一些组合数较大的问题。
算法分析
1、问题的解空间:
应用回溯法解问题时,首先应明确定义问题的解空间。
问题的解空间应到少包含问题的一个(最优)解。
2、运用回溯法解题通常包含以下三个步骤:
(1)针对所给问题,定义问题的解空间;
(2)确定易于搜索的解空间结构;
(3)以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索;
3、递归回溯:
由于回溯法是对解空间的深度优先搜索,因此在一般情况下可用递归函数来实现。
回溯法基本算法如下:
try(inti)
{
if(i>n)输出结果
else
forj=下界to上界
{
x[i]=h[j];
if可行{满足限界函数和约束条件}
{置值;try(i+1);}
}
}
说明:
i是递归深度;
n是深度控制,即解空间树的的高度;
可行性判断有两方面的内容:
不满约束条件则剪去相应子树;若限界函数越界,也剪去相应子树;两者均满足则进入下一层。
【例6-5】采用回溯法找出从自然数1,2,…,n中任取r个数的所有组合。
问题描述设n=5,r=3,并将找到的组合以从小到大顺序存于a[0],a[1],…,a[r-1]中,组合的元素满足以下性质:
(1)a[i+1]>a[i],后一个数字比前一个大;
(2)a[i]-i<=n-r+1。
问题分析首先放弃组合数个数为r的条件,候选组合从只有一个数字1开始。
因该候选解满足除问题规模之外的全部条件,扩大其规模,并使其满足上述条件
(1),候选组合改为1,2。
继续这一过程,得到候选组合1,2,3。
该候选解满足包括问题规模在内的全部条件,因而是一个解。
在该解的基础上,选下一个候选解,因a[2]上的3调整为4,以及以后调整为5都满足问题的全部要求,得到解1,2,4和1,2,5。
由于对5不能再作调整,就要从a[2]回溯到a[1],这时,a[1]=2,可以调整为3,并向前试探,得到解1,3,4。
重复上述向前试探和向后回溯,直至要从a[0]再回溯时,说明已经找完问题的全部解。
按上述思想写成程序如下:
【程序】
#defineMAXN100
inta[MAXN];
voidcomb(intm,intr)
{inti,j,k;
clrscr();
for(i=0;i<=r-1;i++)
a[i]=i+1;
i=r-1;
j=i;
do{
if(a[i]-i<=m-r+1)
{
for(k=i+1;k<=r-1;k++)
a[k]=a[k-1]+1;
for(k=0;k<=r-1;k++)
printf("%4d",a[k]);
printf("\n");
i=r-1;
a[i]++;
continue;
}
else
{i=j;
if(i==0)
return;
a[--i]++;
j=i;
}
}while
(1);
}
main()
{comb(7,5);
}
想一想 回溯法与递归法在解决问题的思路上是不是相同的,他们有何区别和联系?
6.7贪婪法
贪婪法是一种不追求最优解,只希望得到较为满意解的方法。
贪婪法一般可以快速得到满意的解,因为它省去了为找最优解要穷尽所有可能而必须耗费的大量时间。
贪婪法常以当前情况为基础作最优选择,而不考虑各种可能的整体情况,所以贪婪法不要回溯。
知识点播平时购物找钱时,为使找回的零钱的硬币数最少,不考虑找零钱的所有各种方案,而是从最大面值的币种开始,按递减的顺序考虑各币种,先尽量用大面值的币种,当不足大面值币种的金额时才去考虑下一种较小面值的币种。
这就是在使用贪婪法。
这种方法在这里总是最优,是因为银行对其发行的硬币种类和硬币面值的巧妙安排。
如只有面值分别为1、5和11单位的硬币,而希望找回总额为15单位的硬币。
按贪婪算法,应找1个11单位面值的硬币和4个1单位面值的硬币,共找回5个硬币。
但最优的解应是3个5单位面值的硬币。
【例6-6】装箱问题
问题描述装箱问题可简述如下:
设有编号为0、1、…、n-1的n种物品,体积分别为v0、v1、…、vn-1。
将这n种物品装到容量都为V的若干箱子里。
约定这n种物品的体积均不超过V,即对于0≤i<n,有0<vi≤V。
不同的装箱方案所需要的箱子数目可能不同。
装箱问题要求使装尽这n种物品的箱子数要少。
问题分析若考察将n种物品的集合分划成n个或小于n个物品的所有子集,最优解就可以找到。
但所有可能划分的总数太大。
对适当大的n,找出所有可能的划分要花费的时间是无法承受的。
为此,对装箱问题采用非常简单的近似算法,即贪婪法。
该算法依次将物品放到它第一个能放进去的箱子中,该算法虽不能保证找到最优解,但还是能找到非常好的解。
不失一般性,设n件物品的体积是按从大到小排好序的,即有v0≥v1≥…≥vn-1。
如不满足上述要求,只要先对这n件物品按它们的体积从大到小排序,然后按排序结果对物品重新编号即可。
算法描述:
{输入箱子的容积;
输入物品种数n;
按体积从大到小顺序,输入各物品的体积;
预置已用箱子链为空;
预置已用箱子计数器box_count为0;
for(i=0;i{
从已用的第一只箱子开始顺序寻找能放入物品i的箱子j;
if(已用箱子都不能再放物品i)
{另用一个箱子,并将物品i放入该箱子;
box_count++;
}
else
将物品i放入箱子j;
}
}
上述算法能求出需要的箱子数box_count,并能求出各箱子所装物品。
下述程序为上述算法的C语言描述。
程序描述每只箱子所装物品用链表来表示,链表首结点指针存于一个结构中,结构记录尚剩余的空间量和该箱子所装物品链表的首指针。
另将全部箱子的信息也构成链表。
以下是按以上算法编写的程序。
【程序】
#include
#include
typedefstructele
{
intvno;/*存放箱子的编号*/
structele*link;/*链表指针*/
}ELE;
typedefstructhnode
{
intremainder;/*记录尚剩余的空间量*/
ELE*head;/*该箱子所装物品链表的首指针*/
structhnode*next;/*指向下一个装有物品的箱子*/
}HNODE;
voidmain()
{intn,i,box_count,box_volume,*a;
HNODE*box_h,*box_t,*j;
ELE*p,*q;
printf("输入箱子容积\n");
scanf("%d",&box_volume);
printf("输入物品种数\n");
scanf("%d",&n);
a=(int*)malloc(sizeof(int)*n);
printf("请按体积从大到小顺序输入各物品的体积:
");
for(i=0;iscanf("%d",&a[i]);
box_h=box_t=NULL;/*预置已用箱子链为空*/
box_count=0;
for(i=0;i{
p=(ELE*)malloc(sizeof(ELE));
p->vno=i;
for(j=box_h;j!
=NULL;j=j->next)
if(j->remainder>=a[i])break;
if(j==NULL)/*已用箱子都不能再放物品i*/
{/*另用一个箱子,并将物品i放入该箱子*/
j=(HNODE*)malloc(sizeof(HNODE));
j->remainder=box_volume-a[i];
j->head=NULL;
if(box_h==NULL)box_h=box_t=j;
elsebox_t=box_t->next=j;
j->next=NULL;
box_count++;
}
elsej->remainder-=a[i];/*将物品i放入箱子j*/
for(q=j->head;q!
=NULL&&q->link!
=NULL;q=q->link);
if(q==NULL)/*新启用的箱子*/
{
p->link=j->head;
j->head=p;
}
else
{p->link=NULL;
q->link=p;
}
}
printf("共使用了%d只箱子",box_count);
printf("各箱子装物品情况如下:
");
for(j=box_h,i=1;j!
=NULL;j=j->next,i++)
{
printf("第%2d只箱子还剩余容积%4d,所装物品有;\n",i,j->remainder);
for(p=j->head;p!
=NULL;p=p->link)
printf("%4d",p->vno+1);
printf("\n");
}
}
设有6种物品,它们的体积分别为:
60、45、35、20、20和20单位体积,箱子的容积为100个单位体积。
按上述算法的程序计算,需三只箱子,各箱子所装物品分别为:
第一只箱子装物品1、3;第二只箱子装物品2、4、5;第三只箱子装物品6。
想一想 上述问题的最优解是什么?
贪婪法寻求的问题的解是不是原问题的最优解。
习题6
1.程序填空(请根据指定程序的功能,在空格处将程序补充完整)
从n种不同总量,不同价值的物品中选取一部分物品,要求在不超过限定重量limw的前提下,使被选取的物品的总价值最大。
这里约定limw是不超过n种物品的总量的总和,也没有一种物品的总量超过limw,并且各物品的价值都大于0。
程序中,n种物品被顺序编号为0、1、2、3、...、n-1。
#include
#defineN3
doublelimw;
intopts[N];/*临时存储最佳选择方案,当opts[i]为1时,物品i在解中*/
structelem{
doubleweight;
doublevalue;
}a[N];/*物品重量和价值信息*/
intk,n;
struct{
intflag;/*物品的状态:
0,不选;1,考虑;2,曾被选中*/
doubletw;/*已达到的重量*/
doubletv;/*期望的总价值*/
}twv[N];/*当前候选解总各物品的考虑状态及候选解的状态*/
voidmain()
{
doublemaxv,find(structelem*,int);
printf(“Enternumberofmater.”);
scanf(“%d”,&n)
printf(“E