writeln(x,':
3x=',y2);
end.
当X=8则输出结果:
8:
3X=2.000000011E+00
当X=27则输出结果:
27:
3X=3.0000000018E+00
【例3】过河卒(NOIP2002初中组第四题)
【问题描述】棋盘上A点有一个过河卒,需要走到目标B点。
卒行走的规则:
可以向下、或者向右。
同时在棋盘上的任一点有一个对方的马(如C点).该马所在的点和所有跳跃一步可达的点称为对方马的拄制点(如下图中的c点和P1,P2,…,P8)。
卒不能通过对方马的控制点.棋盘用坐标表示,A点(0,0)、B点(n,m)(n,m为不超过20的整数).同样马的位置坐标是需要给出的C(x,y).C≠AC≠B。
现在从键盘输入n,m.,要你计算出卒从A点能够到达B点的路径的条数。
【问题分析】跳马一般是在学习回溯或搜索等算法的时候.很多书上也有类似的题目,一些比赛中也经常出现这一问题的变形(如NOIPl997初中组第三题)。
有些同学一看到这种类型的题目就去盲目搜索,但事实证明:
当n,m=15就会超时。
其实,对本题稍加分析就能发现,要到达棋盘上的一个点,只能从左边过来(我们称之为左点)或是从上面过来(我们称之为上点)。
根据加法原理.到达某一点的路径数目,就等于到达其相邻的上点和左点的路径数目之和.因此我们可以使用逐列(或逐行)递推的方法来求出起点到终点的路径数目。
障碍点(马的控制点)也完全适用,只要将到达该点的路径数目设置为0即可。
假设用F[i,j]表示到达点(i,j)的路径数目,用g[i,j]表示点(i,j)是否是对方马的控制点g[i,j]=0表示不是对方马的控制点,g[i,j]=1表示是对方马的控制点。
则,我们可以得到如下的递推关系式:
F[0,0]=1
F[i,j]=0{g[x,y]=1]
F[i,0]=F[i-1,0]{i>0,g[x,y]=0}
F[0,j]=F[0,j-1]{j>0,g[x,y]=0}
F[i,j]=F[i-1,j]+F[i,j-1]{i>0,j>0.G[x,y]=0}
上述递推关系式的边界为:
F[0,0]=l。
考虑到最大情况下;n=20,m=20.路径条数可能会超出长整数范围,所以要使用int64类型计数或高精度运算。
【参考程序】
programp2_1(input,output);
const
dx:
array[1..8]ofShortint=(-2,-1,1,2,2,1,-1,-2);
dy:
array[1..8]ofShortint=(1,2,2,1,-1,-2,-2,-1);
var
n,m,x,y,i,j:
Byte;
g:
array[0..20,0..20]ofByte;
f:
array[0..20,0..20]ofint64;
begin
Readln(n,m,x,y);
Fillchar(g,Sizeof(g),0);
g[x,y]:
=1;
fori:
=1to8do
if(x+dx[i]>=0)and(x+dx[i]<=n)and
(y+dy[i]>=0)and(y+dy[i]<=m)then
g[x+dx[i],y+dy[i]]:
=1;
f[0,0]:
=1;
fori:
=1tondo
ifg[i,0]=0thenf[i,0]:
=f[i-1,0];
fori:
=1tomdo
ifg[0,i]=0thenf[0,i]:
=f[0,i-1];
fori:
=1tondo
forj:
=1tomdo
ifg[i,j]=0thenf[i,j]:
=f[i-1,j]+f[i,j-1];
writeln(f[n,m])
end.
解决递推类型问题有三个重点:
一是如何建立正确的递推关系式.二是递推关系有何性质,三是递推关系式如何求解。
递推按照我们推导问题的方向,常分为顺推法和倒推法。
所谓顺推法,就是由问题的边界条件(初始状态,已知的、隐含的、推导出的)出发,通过递推关系式依次从前往后递推出问题的解;所谓倒推法.就是在不知道问题的初始状态(边界条件)下,从问题的最终解(目标状态或某个中间状态.已知的或者经过简单推理得到的)出发.反过来推导出问题的初始状态。
下面分别举例给予说明。
【例4】储油点
【问题描述】一辆卡车欲穿过1000km的沙漠.卡车耗油为1L/km,卡车总栽油能力为500L,显然卡车装一次油是过不了沙漠的。
因此司机必须设法在沿途建立几个贮油点.使卡车能顺利穿越沙漠,试问司机如何建立这些贮油点?
每一贮油点应存多少油,才能使卡车以消耗最少汽油的代价通过沙漠(结果保留小数点后两位)?
编程计算及打印建立的贮油点序号.各贮油点距沙漠边沿出发的距离以及存油量。
No.distance(km)oil(L)
1××××××
2××××××
3××××××
【问题分析】
如果从起点出发,则我们无法确定第1个贮油点的位置及贮油量。
既然顺推不行,我们就换个方向,采用倒推法试试看;先从终点出发倒推最后一个储油点的位置及储油量,然后再把最后一个储油点作为终点,倒推倒数第2个储油点的位置及储油量,……。
设dis[i]表示第i个贮油点至终点(i=0)的距离,oil[i]表示第一个贮油点的贮油量。
从终点向始点倒推,逐一求出每个贮油点的位置及存油量的示意图如下。
从贮油点i向贮油点i+1倒推的策略是,卡车在点i和点i+l间往返若干次。
卡车每次返回i+1处时正好耗尽500L汽油,而每次从i+l处出发时又必须装足500L汽油。
两点之间的距离必须满足在耗油最少的条件下使i点贮足500iL汽油的要求(0≤i≤n-1)。
具体地讲,第一个贮油点i=1应距终点i=0处500km且在该处贮藏500L汽油,这样才能保证卡车能由i=1处到达终点i=0处;这就是说:
dis[1]=500;oil[1]=500。
为了在i=1处贮藏500L汽油,卡车至少从i=2处开两趟满载油的车至i=1处。
所以i=2处至少存贮2*500L汽油,即oil[2]=500*2=1000。
另外,再加上从i=1返回至i=2处的一趟空载,合计往返3次。
三次往返路程的耗油量按最省要求只能为500L。
,即d1,2=500/3km。
dis[2]=dis[1]+d1,2=dis[1]+500/3
为了在i=2处贮存1000L汽油,卡车至少从i=3处开三趟满载油的车至i=2处。
所以i=3处至少存贮3*500L汽油,即oil[3]=500*3=1500。
加上i=2至i=3处的二趟返程空车,合计5次。
路途耗油量亦应500L,即d2,3=500/5km。
dis[3]=dis[2]+d2,3=dis[2]+500/5
依此类推,为了在i=k处贮藏k*500L,汽油,卡车至少从i=k+1处开k+1趟满载车至i=k处,即oil[k+1]=(k+1)*500=oil[k]+500,加上从i=k返回i=k+1的k趟返程空车,合计2k+1次。
这2k+1次总耗油量按最省要求为500L,即dk,k+1=500/(2k+1)km。
dis[k+1]=dis[k]+dk,k+1=dis[k]+500/(2k+1)
最后,i=n至始点的距离为1000-dis[n],oil[n]=500*n。
为了在i=n处取得500*nL汽油,卡车至少从始点开n+1次满载车至i=n,加上从i=n返回始点的n趟返程空车.合计2n+1次,2n+1趟的总耗油量应正好为(1000-dis[n])×(2n+1).即始点藏油为oil[n]+(1000-dis[n])×(2n+1)。
【算法分析2】
由于问题要求卡车以最少的油耗通过沙漠,因此,我们可以得到,车在到达终点的时候应该刚好把油全部用完,也就是说在终点的时候油为0。
这样的话我们就知道了问题的最终状态。
而问题又要我们求的是最终状态之前的所有状态的情况,因此,本题只能从最终状态出发,反推前面的状态,即我们所说的倒推法。
同时又由于油都是要从起点运送过去的,因此储油站应该尽可能的靠近起点,这样才能省油。
那么如何来倒推并找到递推公式呢?
根据我们上面的分析,既然储油站要尽可能的靠近起点且最终车内要无油,因此,离终点最近的那个储油站就应该是在汽车装满油的情况下所能走到的最远距离,即离开终点500公里。
而这个点应该储油的量就是500升。
为了能在这个点储油500升,卡车从上一个点出发肯定不能一次就完成运油任务(因为卡车本身要耗油,所以它从倒数第二个储油站加满油出发到倒数第一个储油站的时候,车上肯定不会有500升油,因此还必须返回在运一次),于是,我们可以知道,卡车在这个区间内至少要跑一个半来回,而为了使储油站尽量的分开,因此,这一个半来回应该也耗掉500升油。
也就是说,倒数第二个储油站应该储油500+500=1000升,距离倒数第一个储油站为500/3公里。
同理,我们可以继续往前推导直到起点。
因此我们可以知道,从终点开始数,第k个储油站应满足:
储油:
oil[k]=oil[k-1]+500=500*k
距终点的位置:
dis[k]=dis[k-l]+500/(2*k-1)
而起点的位置不一定恰好就在上述某个储油站上,因此,在运算的时候需要运算到第n+1个储油站使得dis[n+1]>=1000,然后计算起点应该的储油量oil[n]+(1000-dis[n])*(2n+1)。
算法框架如下;
k:
=1;d:
=500;{从i=1处开始向始点倒推)
dis[1]:
=500;oil[l]:
=500;
repeat
k:
=k+l;d:
=d+500/(2*k-1);
dis[k]:
=d;
oil[k]:
=oil[k-1]+500;
untild>1000;
dis[k];=1000;{置始点至终点的距离值)
dl:
=1000-dis[k-1];{求贮油点k处至始点的距离}
oil[k]:
=dl*(2*k十1)+oil[k-1];{求始点藏油量}
fori:
=0tokdo输出第i个贮油点的距离为1000-dis[k-i],藏油量为oil[k-i];
【例5】Catalan数
在一个凸n边形中,通过不相交于n边形内部的对角线,把n边形拆分成若干三角形,不同的拆分数目用hn表之,hn即为Catalan数。
例如五边形有如下五种拆分方案(图3),故h5=5。
求对于一个任意的凸n边形相应的hn。
【问题分析】
如果纯粹从h3=1,h4=2,h5=5,….慢慢去归纳,恐怕很难找到问题的递推关系式.更不要说找到问题的本质了。
所以我们换个思维角度.就像解方程中的降幂思想一样.从一般情况出发去“降n”。
因为多边形的任意一条边必定属于某一个三角形,所以我们以某一条边为准.以这条边的两个顶点为起点,再去找任意一个多边形的顶点,来构成一个三角形,用这个三角形把一个凸多边形剖分成两个凸多边形。
因为凸多边形的任意一点都可以引出n-3条对角线,但这两点引出的两组对角线并不是任意的,因为要剖分成若干个三角形.且“对角线要互不相交”。
如上图所示,我们以P1Pn这条边为基准边.再找Pk(2≤k≤n-1)来构成三角形,则原凸n边形被剖解成了△P1PkPn和两个凸多边形,其中一个是由P1,P2,…Pk构成的凸k边形,另一个是由Pk,Pk+1…Pn构成的凸n-k+1边形.根据乘法原理,选择Pk这个顶点的分解方案为hk*hn-k+1种。
而k可以选2到n-1.所以.再根据加法原理,得出总的方案数应该为:
n-1
hn=∑hk*hn-k+1
k=2
注意.就这个递推关系式而言,临界值应该设为h2=1.而不是h3=1,否则递推关系就不能得到正确解,这与原问题的实际情况可能不符(即两边形),其实这只是理解上的差异。
用这个递推式求出的数列:
h1,h2,h3……,就是著名的catalan数列,它会经常出现在组合计数的问题中。
还要注意的是,这个数列同样很大.当n=22时,hn就超过了长整数范围,所以.如果n很大就需要用高精度运算去计算。
参考程序
programp2_2(input,output);
constmax=21;
varc:
array[2..max]oflongint;
n,i,k:
integer;
total:
longint;
begin
write('inputn=');readln(n);
c[2]:
=1;
fori:
=3tondo
begin
c[i]:
=0;
fork:
=2toi-1doc[i]:
=c[i]+c[k]*c[i-k+1];
end;
writeln('catalan=',c[n]);
end.
【例6】极值问题
【问题描述】已知m,n为整数,且满足下列两个条件:
①m,n∈{1,2,…,k),即1≤m,n≤k
②(n2-mn-m2)2=1
你的任务是:
编程由键盘输入正整数k(1≤k≤109),求一组满足上述两个条件的m、n,并且使m2+n2的值最大。
例如,从键盘输入k=1995,则输出:
m=987,n=1597。
【问题分析】这是一道典型的数学题。
如果我们就从条件②出发,用求根公式,加上限制条件去解方程的话.从数学意义上讲是一定可以得出正确的解。
但是我们疏漏了一个重要的条件1≤k≤l09。
可以验证,如果k值超过105。
上述算法决不可能在竞赛限定的时间内出解.更不要说l09了。
所以要提高算法效率,必须对问题进行一些推理和变换,使问题更直观,同时挖掘出问题的本质。
首先,我们对表达式(n2-mn-m2)2=1作如下数学变换:
(n2-mn-m2)2
=(m2+nm-n2)2
=[(n+m)2-n(n+m)-n2]2
=[(n')2-m'n'-(m')2]2
其中;n'=m+n,m'=n。
虽然从形式上看,表达式并没有什么变化,但从上述数学变换式可以看出:
如果m和n为一组满足条件①和条件②的解.那么m'和n'也是一组满足条件①和条件②的解,这样我们就可以用“迭代法”求解。
所以,我们令m=1,n=l,发现满足条件①和条件②,即是问题的一组小解。
因此,我们可以将所有满足条件①和条件②的m和n按递增顺序排列出来.即1.1,2,3,5,8,……,大家发现这正是一个Fibonacci数列(这也正是问题的本质)数列中小于k的最大两个相邻数即为试题所要求的一组m和n。
所以算法就很简单了,描述如下:
输入k;
m:
=l;n