You are on page 1of 40

基本算法策略

迭代
递推法
倒推法
蛮力(枚举)

1
1.迭代算法
• 定义:一种不断用变量的旧值递推出新值
的解决问题的方法。(辗转法)
• 步骤:
– 确定迭代模型
– 建立迭代关系式
– 对迭代过程进行控制
递推法
【例1】兔子繁殖问题
问题描述:一对兔子从出生后第三个月开始,每月生一对小兔
子。小兔子到第三个月又开始生下一代小兔子。假若兔子
只生不死,一月份抱来一对刚出生的小兔子,问一年中每
个月各有多少只兔子。
问题分析:因一对兔子从出生后第三个月开始每月生一对小兔
子,则每月新下小兔子的对儿数(用斜体数字表示)显然由前
两个月的小兔子的对儿数决定。则繁殖过程如下:
一月 二月 三月 四月 五月 六月 ……
1 1 1+1=2 2+1=3 3+2=5 5+3=8 ……
数学建模:y1=y2=1,yn=yn-1+yn-2,n=3,4,5,……
算法1:
main( )
{ int i,a=1,b=1;
print(a,b);
for(i=1;i<=10;i++)
{ c=a+b;
print (c);
a=b;
b=c;
}
}
算法2:
表1 递推迭代表达式
1 2 3 4 5 6 7 8 9
a b c=a+b a=b+c b=a+c c=a+b a=b+c b=a+c ……

可以用“c=a+b; a=b+c; b=c+a;”做循环“不变式”。


算法2如下:
main( )
{ int i,a=1,b=1; 算法2,最后输出的
print(a,b); 并不是12项,而是
2+3*4共14项。
for(i=1; i<=4;i++)
{ c=a+b; a=b+c; b=c+a; print(a,b,c); }
}
算法3:
表2 递推迭代表达式
1 2 3 4 5 6 7 8 9
a b a=a+b b=a+b a=a+b b=a+b ……

可以用“a=a+b; b=a+b;”做循环“不变式”
算法3如下:
main( )
{ int i,a=1,b=1;
print(a,b);
for(i=1; i<=5;i++)
{ a=a+b; b=a+b; print(a,b); }
}
倒推法
倒推法:是对某些特殊问题所采用的违反通常习惯的,从后
向前推解问题的方法。如下面的例题,因不同方面的需求而采用
了倒推策略。

1.在不知前提条件的情况下,经过从后向前递推,从而求
解问题。即由结果倒过来推解它的前提条件。

2.由于存储的要求,而必须从后向前进行推算。

3.在对一些问题进行分析或建立数学模型时,从前向后分
析问题感到比较棘手,而采用倒推法,则问题容易理解和解决。
【例1】猴子吃桃问题
一只小猴子摘了若干桃子,每天吃现有桃的一半多一个,
到第10天时就只有一个桃子了,求原有多少个桃?

数学模型:每天的桃子数为: a10=1,
a9=(1+a10)*2, a8 =(1+a9)*2,……

递推公式为: ai =(1+ ai+1)*2 i = 9,8,7,6……1


算法如下 :
main( )
{ int i,s;
s=1;
for (i=9 ;i>=1;i=i-1)
s=(s+1)*2
print (s);
}
【例2】 输出如图4-1的杨辉三角形(限
1
定用一个一维数组完成)。 1 1
数学模型:上下层规律较明显,中间的数 1 2 1
等于上行左上、右上两数之和。 1 3 3 1
1 4 6 4 1
问题分析:题目中要求用一个一维数组即 ……………
完成。数组空间一定是由下标从小到大 图4-1 杨辉三角形
利用的,这样其实杨辉三角形是按下图
4-2形式存储的。若求n层,则数组最多
存储n个数据。
算法设计: 1
1 1
1 2 1
A[1] = A[i]=1 1 3 3 1
A[j] = A[j] + A[j-1] j=i-1,i-2,……,2 1 4 6 4 1
i行 i-1行 i-1行 ……………
图4-2 杨辉三角形存储格式
算法如下:
main( )
{int n,i,j,a[100];
input(n);
print(“1”); print(“换行符”);
a[1]=a[2]=1;
print(a[1],a[2]); print(“换行符”);
for (i=3;i<=n;i=i+1)
{a[1]=a[i]=1;
for (j=i-1,j>1,j=j-1)
a[j]=a[j]+a[j-1];
for (j=1;j<=i;j=j+1) print(a[j]);
print(“换行符”);
}
}
【例3】穿越沙漠问题
用一辆吉普车穿越1000公里的沙漠。吉普车的总装油量为
500加仑,耗油率为1加仑/公里。由于沙漠中没有油库,
必须先用这辆车在沙漠中建立临时油库。该吉普车以最少
的耗油量穿越沙漠,应在什么地方建油库,以及各处的贮
油量。
问题分析:
1)先看一简单问题:有一位探险家用5天的时间徒步
横穿A、B两村,两村间是荒无人烟的沙漠,如果一
个人只能担负3天的食物和水,那么这个探险家至
少雇几个人才能顺利通过沙漠。
A城雇用一人与探险家同带3天食物同行一天,然后被雇
人带一天食物返回,并留一天食物给探险家,这样探险
家正好有3天的食物继续前行,并于第三天打电话雇B城
人带3天食物出发,第四天会面他们会面,探险家得到一
天的食物赴B城。如图4-3主要表示了被雇用二人的行程。

A B
图1 被雇用二人的行程

2)贮油点问题要求要以最少的耗油量穿越沙漠,即到达终
点时,沙漠中的各临时油库和车的装油量均为0。这样只
能从终点开始向前倒着推解贮油点和贮油量。
数学模型:根据耗油量最少目标的分析,下面从后向前分段
讨论。
第一段长度为500公里且第一个加油点贮油为500加仑。
第二段中为了贮备油,吉普车在这段的行程必须有往返。
下面讨论怎样走效率高:
1)首先不计方向这段应走奇数次(保证最后向前走)。
2)每次向前行进时吉普车是满载。
3)要能贮存够下一加油点的贮油量,以及建立下一加油点
路上的油耗。

……
下图是满足以上条件的最佳方案,第二段共走3次:第一、
二次来回耗油2/3贮油1/3,第三次耗油1/3贮油2/3,所以
第二个加油点贮油为1000加仑。由于每公里耗油率为1加
仑,则此段长度为500/3公里。
第三段与第二段思路相同。共走5次:第一、二次来回耗油
2/5贮油3/5,第三、四次来回耗油2/5贮油3/5,第五次耗
油1/5贮油4/5,第三个加油点贮油为1500加仑。此段长度
为500/5。 ……

500/5公里
<———
500/3公里 ———>
<——— <———
500公里 第一 ———> 第二 ———> 第三
终点<——贮油点(500)<——贮油点(1000)<———贮油点(1500)……
图2 贮油点及贮油量示意
综上分析,从终点开始分别间隔 500,500/3,500/5,
500/7,……(公里)设立贮油点,直到总距离超过1000公
里。每个贮油点的油量为500,1000,1500,……。
算法设计:由模型知道此问题并不必用倒推算法解决(只是
分析过程用的是倒推法),只需通过累加算法就能解决。
通过倒着累加储油点间的距离,并计算各储油点的储油量,
直到总距离超过1000公里,求距离出发点最近一个储油点
的位置及储油量即可。
变量说明:dis表示距终点的距离,1000- dis则表示距起点
的距离,k表示贮油点从后到前的序号。
desert( )
{ int dis,k,oil,k;
dis=500;k=1;oil=500;
do{
print(“storepoint”,k,”distance”,1000-
dis,”oilquantity”,oil);
k=k+1;
dis=dis+500/(2*k-1);
oil= 500*k;
}while ( dis<1000)
oil=500*(k-1)+(1000-dis)*(2*k-1);
print(“storepoint”,k,”distance”,0,”oilquantity”,oil);
}
2 蛮力法
蛮力法是基于计算机运算速度快这一特性,在解决问题
时采取的一种“懒惰”的策略。这种策略不经过(或者说是经
过很少的)思考,把问题的所有情况或所有过程交给计算机
去一一尝试,从中找出问题的解。
蛮力策略的应用很广,具体表现形式各异,数据结构课
程中学习的:选择排序、冒泡排序、插入排序、顺序查找、
朴素的字符串匹配等,都是蛮力策略具体应用。
比较常用还有枚举法、盲目搜索算法等。
枚举法
枚举( enumerate)法(穷举法)是蛮力策略的一种表现
形式,也是一种使用非常普遍的思维方法。它是根据问题中
的条件将可能的情况一一列举出来,逐一尝试从中找出满足
问题条件的解。但有时一一列举出的情况数目很大,如果超
过了我们所能忍受的范围,则需要进一步考虑,排除一些明
显不合理的情况,尽可能减少问题可能解的列举数目。
用枚举法解决问题,通常可以从两个方面进行算法设计:
1)找出枚举范围:分析问题所涉及的各种情况。
2)找出约束条件:分析问题的解需要满足的条件,并
用逻辑表达式表示。
【例1】百钱百鸡问题。中国古代数学家张丘建在他的《算经》
中提出了著名的“百钱百鸡问题”:鸡翁一,值钱五;鸡母一
,值钱三;鸡雏三,值钱一;百钱买百鸡,翁、母、雏各几何?
算法设计1:
通过对问题的理解,读者可能会想到列出两个三元一次方程,
去解这个不定解方程,就能找出问题的解。这确实是一种办法
,但这里我们要用“懒惰”的枚举策略进行算法设计:
设x,y,z分别为公鸡、母鸡、小鸡的数量。
尝试范围:由题意给定共100钱要买百鸡,若全买公鸡最多
买100/5=20只,显然x的取值范围1~20之间;同理,y的取值范
围在1~33之间,z的取值范围在1~100之间。
约束条件: x+y+z=100 且 5*x+3*y+z/3=100
算法1如下:
main( )
{ int x,y,z;
for(x=1;x<=20;x=x+1)
for(y=1;y<=33;y=y+1)
for(z=1;z<=100;z=z+1)
if(100=x+y+z and 100=5*x+3*y+z/3)
{print(the cock number is",x);
print(the hen number is", y);
print(the chick number is "z);}
}
算法分析:以上算法需要枚举尝试20*34*100=68000次。
算法的效率显然太低。

算法设计2:
在公鸡(x)、母鸡(y)的数量确定后,小鸡的数量z就固
定为100-x-y,无需再进行枚举了。
此时约束条件只有一个: 5*x+3*y+z/3=100
main( )
{ int x,y,z;
for(x=1;x<=20;x=x+1)
for(y=1;y<=33;y=y+1)
{ z=100-x-y;
if(z mod 3=0 and 5*x+3*y+z/3=100)
{print(the cock number is",x);
print(the hen number is", y);
print(the chick number is "z);}
}
}
算法分析:以上算法只需要枚举尝试20*33=660次。实现时约
束条件又限定Z能被3整除时,才会判断“5*x+3*y+z/3=100”。这
样省去了z不整除3时的算术计算和条件判断,进一步提高了算法
的效率。
【例2】解数字迷: A B C A B
× A
D D D D D D
算法设计1:按乘法枚举
1)枚举范围为:
A:3-9(A=1,2时积不会得到六位数),B:0-9,
C:0-9 六位数表示为A*10000+B*1000+C*100+A*10+B,
共尝试800次。
2)约束条件为:
每次尝试,先求六位数与A的积,再测试积的各位是否相
同,若相同则找到了问题的解。
测试积的各位是否相同比较简单的方法是,从低位开始,
每次都取数据的个位,然后整除10,使高位的数字不断变
成个位,并逐一比较。
算法1如下:
main( )
{ int A,B,C,D,E,E1,F,G1,G2,i;
for(A=3; A<=9; A++)
for(B=0; B<=9; B++)
for(C=0; C<=9; C++)
{ F=A*10000+B*1000+C*100+A*10+B;
E=F*A; E1=E; G1=E1 mod 10;
for(i=1; i<=5; i++)
{ G2=G1; E1=E1/10; G1= E1 mod 10;
if(G1<>G2 ) break; }
if(i=6) print( F,”*”,A,”=”,E);
}
}
算法说明1:算法中对枚举出的每一个五位数与A相乘,结果
存储在变量E中。然后,测试得到的六位数E是否各个位的数
字都相同。鉴于要输出结果,所以不要改变计算结果,而另
设变量E1,用于测试运算。

算法分析1:以上算法的尝试范围是A:3——9,B:0——9,
C:0——9 。共尝试800次,每次,不是一个好的算法。

算法设计2:将算式变形为除法:DDDDDD/A=ABCAB。此时
只需枚举A:3-9 D:1-9,共尝试7*9=63次。每次
尝试,测试商的万位、十位与除数是否相同,千位与个位
是否相同,都相同时为解。
main()
{int A,B,C,D,E,F;
for(A=3;A<=9;A++)
for(D=1;D<=9;D++)
{ E = D*100000+D*10000+D*1000+D*100+D*10+D;
if(E mod A=0) F=E/A;
if(F/10000=A and (F mod 100)/10=A)
if(F/1000==F mod 10)
print( F,”*”,A,”=”,E);
}
}
【例3】狱吏问题
某国王对囚犯进行大赦,让一狱吏n次通过一排锁着的n
间牢房,每通过一次,按所定规则转动n间牢房中的某些门
锁, 每转动一次, 原来锁着的被打开, 原来打开的被锁上;
通过n次后,门锁开着的,牢房中的犯人放出,否则犯人不
得获释。
转动门锁的规则是这样的,第一次通过牢房,要转动每
一把门锁,即把全部锁打开;第二次通过牢房时,从第二间
开始转动,每隔一间转动一次;第k次通过牢房,从第k间开
始转动,每隔k-1 间转动一次;问通过n次后,那些牢房的
锁仍然是打开的?
算法设计1:
1)用n个空间的一维数组a[n],每个元素记录一个锁的状
态,1为被锁上,0为被打开。
2)用数学运算方便模拟开关锁的技巧,对i号锁的一次开
关锁可以转化为算术运算:a[i]=1-a[i]。
3)第一次转动的是1,2,3,……,n号牢房;
第二次转动的是2,4,6,……号牢房;
第三次转动的是3,6,9,……号牢房;
……第i次转动的是i,2i,3i,4i,……号牢房,是起点
为i,公差为i的等差数列。
4)不做其它的优化,用蛮力法通过循环模拟狱吏的开关
锁过程,最后当第i号牢房对应的数组元素a[i]为0时,
该牢房的囚犯得到大赦。算法1如下:
main1( )
{int *a,i,j,n;
input(n);
a=calloc(n+1,sizeof(int));
for (i=1; i<=n;i++)
a[i]=1;
for (i=1; i<=n;i++)
for (j=i; j<=n;j=j+i)
a[i]=1-a[i];
for (i=1; i<=n;i++)
if (a[i]=0) print(i,”is free.”);
}
算法分析:以一次开关锁计算,算法的时间复杂度为
n(1+1/2+1/3+……+1/n)=O(nlogn)。
问题分析:转动门锁的规则可以有另一种理解,第一次转动
的是编号为1的倍数的牢房;第二次转动的是编号为2的倍
数的牢房;第三次转动的是编号为3的倍数的牢房;……则
狱吏问题是一个关于因子个数的问题。令d(n)为自然数n
的因子个数,这里不计重复的因子,如4的因子为1,2,4
共三个因子,而非1,2,2,4。则d(n)有的为奇数,有的
为偶数,见下表:
表3 编号与因数个数的关系
n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ……
d(n) 1 2 2 3 2 4 2 4 3 4 2 6 2 4 4 5 ……

数学模型1:上表中的d(n)有的为奇数,有的为偶数,由于
牢房的门开始是关着的,这样编号为i的牢房,所含1~i
之间的不重复因子个数为奇数时,牢房最后是打开的,反
之,牢房最后是关闭的。
算法设计2:
1)算法应该是求出每个牢房编号的不重复的因子个数,当它
为奇数时,这里边的囚犯得到大赦。
2)一个数的因子是没有规律的,只能从1~n枚举尝试。算法
2如下:
main2( )
{int s,i,j,n;
input(n);
for (i=1; i<=n;i++)
{ s=1;
for (j=2; j<=i;j=j++)
if (i mod j=0) s=s+1;
if (s mod 2 =1) print(i,”is free.”); }
}
算法分析:
狱吏开关锁的主要操作是a[i]=1- a[i];共执行了
n*(1+1/2+1/3+……+1/n)次,时间近似为复杂度为O
(n log n)。
使用了n个空间的一维数组。算法2没有使用辅助空间,
但由于求一个编号的因子个数也很复杂,其主要操作
是判断i mod j是否为0,共执行了n*(1+2+3+……+n)
次,时间复杂度为O(n2)。

数学模型2:仔细观察表3,发现当且仅当n为完全平方
数时,d(n)为奇数;这是因为n的因子是成对出现的,也
即当n=a*b且a≠b时,必有两个因子a,b; 只有n为完
全平方数,也即当n=a2时, 才会出现d(n)为奇数的情形。

算法设计3:这时只需要找出小于n的平方数即可。
main3( )
{int s,i,j,n;
input(n);
for (i=1;i<=n;i++)
if(i*i<=n) print(i,”is free.”);
else break;
}
由此算法我们应当注意:在对运行效率要求较高的大规模的
数据处理问题时,必须多动脑筋找出效率较高的数学模型及其
对应的算法。
信息数字化

34
【例1】警察局抓了a,b,c,d四名偷窃嫌疑犯,其中只有一
人是小偷。审问中
a说:“我不是小偷。”
b说:“c是小偷。”
c说:“小偷肯定是d。”
d说:“c在冤枉人。”
现在已经知道四个人中三人说的是真话,一人说的是假话,
问到底谁是小偷?

问题分析:将a,b,c,d将四个人进行编号,号码分别
为1,2,3,4。则问题可用枚举尝试法来解决。
算法设计:用变量x存放小偷的编号,则x的取值范围从1取
到4,就假设了他们中的某人是小偷的所有情况。四个人
所说的话就可以分别写成:
a说的话:x<>1
b说的话:x=3
c说的话:x=4
d说的话:x<>4或not(x=4)

注意:在x的枚举过程中,当这四个逻辑式的值相加等于3时,
即表示“四个人中三人说的是真话,一人说的是假话”。
main( )
{ int x;
for(x=1;x<=4;x++)
if((x<>1)+(x=3)+(x=4)+(x<>4)==3)
print(chr(64+x),“is a thief .”);
}
运行结果:
c is a thief .
【例2】三位老师对某次数学竞赛进行了预测。他们的预测
如下:
甲说:学生A得第一名,学生B得第三名。
乙说:学生C得第一名,学生D得第四名。
丙说:学生D得第二名,学生A得第三名。
竞赛结果表明,他们都说对了一半,说错了一半,并且无并
列名次,试编程输出A、B、C、D各自的名次。

问题分析:用数1,2,3,4分别代表学生a,b,c,d获得
的名次。问题就可以利用三重循环把所有的情况列举出来。
算法设计:
1)用a,b,c,d 代表四个同学,其存储的值代表他们的名
次。
设置第一层计数循环a的范围从1到4;
设置第二层计数循环b的范围从1到4;
设置内计数循环c的范围从1到来4;
由于无并列名次,名次的和为1+2+3+4=10,由此可计算
出d的名次值为10-a-b-c。
2)问题的已知内容,可以表示成以下几个条件式:
(1) (a=1)+(b=3)=1
(2) (c=1)+(d=4)=1
(3) (d=2)+(a=3)=1
若三个条件均满足,则输出结果,若不满足,继续循环
搜索,直至循环正常结束。
main( )
{int a,b,c,d;
for( a=1;a<=4;a=a+1)
for( b=1;b<=4;b=b+1)
if (a<>b)
for( c=1;c<=4;c=c+1)
if (c<>a and c<>b)
{ d=10-a-b-c;
if (d<>a and d<>b and d<>c )
if(((a=1)+(b=3))=1 and ((c=1)+(d=4))=1 and
((d=2)+(a=3))=1)
print( “a,b,c,d=”,a,b,c,d);
}
}

You might also like