2019-2020年高中信息技术全国青少年奥林匹克联赛教案贪心法二

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

课题:贪心法 目标:
知识目标:贪心的原理递与贪心的实现 能力目标:贪心的原理
重点:贪心算法的应用 难点:贪心的理解 板书示意:
1) 贪心的引入(例
2) 贪心的应用(例 授课过程:
若在求解一个问题时,能根据每次所得到的局部最优解,推导出全局最优或最优目标。

那么,我们可以根据这个策略, 每次得到局部最优解答, 逐步而推导出问题,这种策略称为
贪心法。

下面我们看一些简单例题。

例24:在N 行M 列的正整数矩阵中,要求从每行中选出 1个数,使得选出的总共 N 个
数的和最大。

分析:要使总和最大,则每个数要尽可能大,自然应该选每行中最大的那个数。

因此, 我们设计出如下算法:
读入N, M,矩阵数据; Total := 0;
For I := 1 to N do begin {对 N 行进行选择}
选择第I 行最大的数,记为K ; Total := Total + K ; End;
输出最大总和Total ;
从上例中我们可以看出,和递推法相仿,贪心法也是从问题的某一个初始解出发, 向给
定的目标递推。

但不同的是,推进的每一步不是依据某一固定的递推式, 而是做一个局部的
最优选择,即贪心选择(在例中,这种贪心选择表现为选择一行中的最大整数) ,这样,不
断的将问题归纳为若干相似的子问题,最终产生出一个全局最优解。

特别注意的是是,局部贪心的选择是否可以得出全局最优是能否采用贪心法的关键所 在。

对于能否使用贪心策略,应从理论上予以证明。

下面我们看看另一个问题。

例25:部分背包问题
给定一个最大载重量为 M 的卡车和N 种食品,有食盐,白糖,大米等。

已知第 i 种食品 的最多拥有 W 公斤,其商品价值为 V 元/公斤,编程确定一个装货方案,使得装入卡车中的 所有物品总价值最大。

分析: 因为每一个物品都可以分割成单位块, 单位块的利益越大显然总收益越大, 所以 它局部最优满足全局
24)
25、例 26、例 27、例 28)
最优,可以用贪心法解答,方法如下:先将单位块收益按从大到小进行排列,然后用循环从单位块收益最大的取起,直到不能取为止便得到了最优解。

因此我们非常容易设计出如下算法:
问题初始化;{ 读入数据}
按V i 从大到小将商品排序;
I := 1;
repeat
if M = 0 then Break; { 如果卡车满载则跳出循环}
M := M - W i ;
if M >= 0 then 将第I 种商品全部装入卡车
else
将(M + W i)重量的物品I装入卡车;
I := I + 1; {选择下一种商品}
until (M <= 0) OR (I >= N)
在解决上述问题的过程中,首先根据题设条件,找到了贪心选择标准(V i) ,并依据这个
标准直接逐步去求最优解,这种解题策略被称为贪心法。

Program Exam25;
Const Finp='Input.Txt';
Fout='Output.Txt';
Var N,M :Longint;
S :Real;
P,W :Array[1..100] Of Integer;
Procedure Init;{ 输出}
Var I :Integer;
Begin
Assign(Input,Finp); Reset(Input);
Readln(M,N);
For I:=1 To N Do Readln(W[I],P[I]);
Close(Input);
End;
Procedure Sort(L,R:Integer); { 按收益值从大到小排序} Var I,J,Y :Integer;
X :Real;
Begin
I:=L; J:=R;
X:=P[(L+R) Div 2]/W[(L+R) Div 2];
Repeat
While (I<R)And(P[I]/W[I]>=X) Do Inc(I);
While (P[J]/W[J]<=X)And(J>L) Do Dec(J);
If I<=J Then
Begin
Y:=P[I]; P[I]:=P[J]; P[J]:=Y;
Y:=W[I]; W[I]:=W[J]; W[J]:=Y;
Inc(I); Dec(J);
End;
Until I>J;
If I<R Then Sort(I,R);
If L<J Then Sort(L,J);
End;
Procedure Work;
Var I :Integer;
Begin
Sort(1,N);
For I:=1 To N Do
If M>=W[I] Then { 如果全部可取,则全取}
Begin
S:=S+P[I]; M:=M-W[I];
End
Else { 否则取一部分}
Begin
S:=S+M*(P[I]/W[I]); Break;
End;
End;
Procedure Out; { 输出}
Begin
Assign(Output,Fout); Rewrite(Output);
Writeln(S:0:0);
Close(Output);
End;
Begin { 主程序}
Init;
Work;
Out;
End.
因此,利用贪心策略解题,需要解决两个问题:首先,确定问题是否能用贪心策略求解;一般来说,适用于贪心策略求解的问题具有以下特点:
① 可通过局部的贪心选择来达到问题的全局最优解。

运用贪心策略解题,一般来说需要一步步的进行多次的贪
心选择。

在经过一次贪心选择之后,原问题将变成一个相似的,但规模更小的问题,而后的每一步都是当前看似最佳的选择,且每一个选择都仅做一次。

② 原问题的最优解包含子问题的最优解,即问题具有最优子结构的性质。

在背包问题中,第一次选择单位质量
最大的货物,它是第一个子问题的最优解,第二次选择剩
下的货物中单位重量价值最大的货物,同样是第二个子问题的最优解,依次类推。

其次,如何选择一个贪心标准?正确的贪心标准可以得到问题的最优解,在确定采用贪心策略解决问题时,不能随意的判断贪心标准是否正确,尤其不要被表面上看似正确的贪心
标准所迷惑。

在得出贪心标准之后应给予严格的数学证明。

下面来看看0-1背包问题。

给定一个最大载重量为M的卡车和N种动物。

已知第i种动物的重量为W,其最大价值
为V,设定M W, V均为整数,编程确定一个装货方案,使得装入卡车中的所有动物总价值最大。

分析:对于N种动物,要么被装,要么不装,也就是说在满足卡车载重的条件下,如何选择动物,使得动物价值最大的问题。

即确定一组X1, X2,…,Xn, Xi € {0,1} f(x)=max(刀X*V i)其中,刀(X i*W)w W
从直观上来看,我们可以按照上例一样选择那些价值大,而重量轻的动物。

也就是可以按价值质量比(V i/W)的大小来进行选择。

可以看出,每做一次选择,都是从剩下的动物中选择那些V i/W最大的,这种局部最优的选择是否能满足全局最优呢?我们来看看一个简单的例子:
设N=3,卡车最大载重量是100,三种动物A B C的重量分别是40, 50, 70,其对应
的总价值分别是80、100、150。

情况A:按照上述思路,三种动物的V/W分别为2,2,2.14。

显然,我们首先选择动物C, 得到价值150,然后任意选择A或B,由于卡车最大载重为100,因此卡车不能装载其他动
物。

情况B:不按上述约束条件,直接选择A和B。

可以得到价值80+100=180,卡车装载的
重量为40+50=90。

没有超过卡车的实际载重,因此也是一种可行解,显然,这种解比上一种解要优化。

问题出现在什么地方呢?我们看看图2-18
|空載30空載山|
載重九价值150
价值190
情况A 情况B
图23卡车装载货物情况分析
从图23中明显可以看出,情况A,卡车的空载率比情况B高。

也就是说,上面的分析,只考虑了货物的价值质量比,而没有考虑到卡车的运营效率,因此,局部的最优化,不能导
致全局的最优化。

因此,贪心不能简单进行,而需要全面的考虑,最后得到证明。

例26排队打水问题
有N个人排队到R个水龙头去打水,他们装满水桶的时间为T1, T2,…,Tn为整数且各不
相等,应如何安排他们的打水顺序才能使他们花费的时间最少?
分析:由于排队时,越靠前面的计算的次数越多,显然越小的排在越前面得出的结果越小(可以用数学方法简单证
明,这里就不再赘述),所以这道题可以用贪心法解答,基本步骤:
(1) 将输入的时间按从小到大排序;
(2) 将排序后的时间按顺序依次放入每个水龙头的队列中
(3) 统计,输出答案。

参考程序:
Program
Exam26;
Const Finp='Input.Txt';
Fout='Output.Txt';
Var A :Array[1..100] Of Integer;
:Array[1..100] Of Longint;
S
N,M :Integer;
Min :Longint;
Procedure Init; { 读入数据}
Var I :Integer;
Begin
Assign(Input,Finp); Reset(Input);
Readln(N,M);
For I:=1 To N Do Read(A[I]);
Close(Input);
End;
Procedure Sort(L,R:Integer); { 将时间从小到大排序}
Var I,J,X,Y :Integer;
Begin
I:=L; J:=R; X:=A[(L+R) Div 2];
Repeat
While (A[I]<=X)And(I<R) Do Inc(I);
While (A[J]>=X)And(J>L) Do Dec(J);
If I<=J Then
Begin
Y:=A[I]; A[I]:=A[J]; A[J]:=Y;
Inc(I); Dec(J);
End;
Until I>J;
If L<J Then Sort(L,J);
If R>I Then Sort(I,R);
End;
Procedure Work;
Var I,J,K :Integer;
Begin
Fillchar(S,Sizeof(S),0);
J:=0; Min:=0;
For I:=1 To N Do { 用贪心法求解}
Begi n
In c(J);
If J=M+1 Then J:=1;
S[J]:=S[J]+A[I];
Mi n:=Mi n+S[J];
En d;
Assign(Output,Fout); Rewrite(Output); { 输出解答}
Writel n(Min); Close(Output);
End;
Begin { 主程序}
In it;
Sort(1,N);
Work;
End.
例27:旅行家的预算(NOI99分区联赛第3题)
一个旅行家想驾驶汽车以最少的费用从一个城市到另一个城市(假设出发时油箱时空
的)。

给定两个城市之间的距离D1、汽车油箱的容量C (以升为单位)、每升汽油能行驶的距
离D2、出发点每升汽油价格P和沿途加油站数N(N可以为零),油站i离出发点的距离Di、
每升汽油价格Pi (i=1 , 2,……,N)o
计算结果四舍五入至小数点后两位。

如果无法到达目的地,则输出" No Solution ”。

样例:
In put
D1=275.6 C=11.9 D2=27.4 P=2.8 N=2
Output
26.95 (该数据表示最小费用)
分析:需要考虑如下问题:
1) 出发前汽车的油箱是空的,故汽车必须在起点( 1号站)处加油。

加多少油?
2) 汽车行程到第几站开始加油,加多少油?
可以看出,原问题需要解决的是在哪些油站加油和加多少油的问题。

对于某个油站,汽车加油后到达下一加油站,可以归结为原问题的子问题。

因此,原问题关键在于如何确定下
一个加油站。

通过分析,我们可以选择这样的贪心标准:
对于加油站I,下一个加油站J可能第一个是比油站I油价便宜的油站,若不能到达这样的油站,则至少需要到达下一个油站后,继续进行考虑。

对于第一种情况,则油箱需要(d (j ) -d (i )) /m加仑汽油。

对于第二种情况,则需将油箱加满。

贪心算法证明如下:
设定如下变量:
Value[i] :第i 个加油站的油价;
Over[i] :在第i 站时的剩油;Way[i] :起点到油站i 的距离;X[I] :X 记录问题的最优解,X[I] 记录油站I 的实际加油量。

首先,X[1]丰 0, Over[1]=0。

假设第I站加的X[l] —直开到第K站。

则有,X[l]..x[k-1] 都为0,而X[K]丰0。

①若Value[I]>Value[k] ,则按贪心方案,第I 站应加油为
T=(Way[k]-Way[I] )/M-Over[I] 。

若T<X[I] ,则汽车无法从起点到达第k 个加油站;与假设矛盾。

若T>X[I],则预示着,汽车开到油站K,仍然有油剩余。

假设剩余W加仑汽油,则须费用Value[I]*W , 如果W 加仑汽油在油站K 加, 则须费用Value[K]*W , 显然Value[K]*W<Value[I]*W 。

②若Value[I]<Value[k] ,则按贪心规则,须加油为
T=C-Over[I] (即加满油)。

若T<X[I] ,则表示在第I 站的加油量会超过汽车的实际载油量,显然是不可能的。

若T>X[I] , 则表示在第I 站的不加满油, 而将一部分油留待第K 站加, 而Value[I]<Value[k] ,所以这样费用更高。

综合上述分析,可以得出如下算法:
I := 1 { 汽车出发设定为第1 个加油站}
L := C*D2 ;{ 油箱装满油能行驶的距离}
repeat
在L 距离以内,向后找第一个油价比I 站便宜的加油站J ;
if J 存在then
if I 站剩余油能到达J then
计算到达J 站的剩油
else
在I 站购买油,使汽车恰好能到达J 站
else
在I 站加满油;
I := J ;{ 汽车到达J 站}
until 汽车到达终点;
程序如下:
program NOI99L_3;
const
Inp = ‘ input.txt
Outp = ‘output.txt
MaxN = 10001; Zero = 1e-16; type
Rectype = record Value: Real;{ 最大油站数}
{ 误差值}
{ 油站的数据结构} { 油
价}
{ 距起点的距离 } { 汽车到达该站时的剩油 } { 油站指针 }
Way: Real; Over: Real; end;
RecPtr = ARectype;
var
Oil: array [1 .. MaxN] of RecPtr; D1, C, D2,
N: Integer; Cost: Real; MaxWay,
{ 记录所有油站 }
{ 起点到终点之间的距离 } { 汽车油箱的容量 } { 每升汽油能行驶的距离 } { 油站数 } { 最小油费 }
{ 满油时汽车最大的行驶距离 }
function init: Boolean; var { 初始化,并判无解 }
I: Integer; begin
Read (D1, C, D2); New(Oil[1]); Oil[1]A.Way := 0; Read(Oil[1]A.Value,n); MaxWay := D2 * C; for I := 2 to n do begin { New(Oil[I]);
{ 处理初始值和起始油站 }
读入后 N-1 个油站信息 }
Readln(Oil[I]A.Way, Oil[I]A.Value); O il[I]A.ov er:=0; end; Inc(n); New(Oi l[n]); Oil[n]A.Way := D1; Oil[
{ 将终点也看成一个加油站 }
if (Oil[I]A.Way init:= False; Exit;
end;
{ 判是否无解 }
-Oil[l - 1]~Way > MaxWay) then begin
init := True; end;
procedure Buy(l: lnteger; Miles: Real);; { 在 l 加油站购买 Miles/D2 加仑汽油 } begin
{ 将买汽油所需的费用加到 Cost 变量中 } end;
procedure Solve; var
l, J: lnteger; S: Real; begin l := 1; repeat
S := 0.0;
{在MaxWay 范围以内,找第一个油价比 I 站便宜的加油站 J}
while (S <= MaxWay+zero) and (J <= N
- 1)
and (Oil[『.Value <= Oil[J]A .Value) do
begin
{ 汽车在起点 } Inc(J);
S := S + Oil[J]A.Way
-Oil[J - 1]人.Way;
Writel n(Cost:0 :2); end else
Writeln( ‘ No Solution ' ); {输出无解}
Close(I nput); Close(Output); en d.
例28:两机器加工问题
有n 个部件需在 A , B 机器上加工,每个工件都必须经过先 A 后B 两道工序。

已知:部件i 在A B 机器上的加工时间分别为 a i , b i 。

问:如何安排n 个工件的加工顺序,才能使得总加工时间最短? 输入示例:
N = 5
end;
if S <= MaxWay+zero then { 如果找到 J 站或可以直达终点 }
{ 如果剩油足够到达 J 站,则无需购油,并计算到达 J 站时汽车的剩油 } if (Oil[I]A.Over + Zero >=Oil[J]A.Way — Oil[l F.Way) the n
Oil[J]A.Over:=Oil[l]A.Over — Oil[J]A.Way+Oil[l]A.Way
else begin
{ 在 l 站购买恰好能到达 J 站的油量 }
Buy(l,Oil[J]A.Way Oil[J]A.Over := 0.0; end
-Oil[l]A.Way - Oil[l]A.Over);
else begin Buy(I, MaxWay
J := I + 1;
Oil[JF.Over:= MaxWay end; I := J; until I = N; end;
{ 附近无比 l 站便宜的加油站 J}
Oil[l]A.Over);
{在 I 站加满油} { 行驶到下一站 }
-(Oil[J]A.Way - Oil[l]A .Way);
{ 汽车直达 J 站 }
{ 汽车到达终点 }
begin Cost := 0; Assign(Input, Inp); Reset(Input); Assign(Output, Outp); Rewrite(Output); if init then begin Solve;
{ 主程序 }
{ 如果有解 }
{求解} {输出最少费用}
34 (最少时间) 1 5 4 2 3
(最优加工顺序)
分析:
本题求一个加工顺序使得加工总时间最短,要使时间最短,则就是让机器的空闲时间最
短。

一旦A 机器开始加工,则 A 机器将会不停的进行作业,关键是 B 机器在加工过程中,有 可能要等待A 机器。

很明显第一个部件在 A 机器上加工时,B 机器必须等待,最后一个部件 在B 机器上加工,A 机器也在等待 B 机器的完工。

可以大胆猜想,要使总的空闲的最少,就要把在A 机器上加工时间最短的部件最先加工, 这样使得B 机器能以最快的速度开始加工; 把在B 机器上加工时间最短的部件放在最后加工。

这样使得A 机器能尽快的等待 B 机器完工。

于是我们可以设计出这样的贪心法:
设 M=mi n{a i , b i }
将M 按照从小到大的顺序排序。

然后从第1个开始处理,若M=a ,则将它排在从头开始
的已经作业后面,若 M=b i ,则将它排在从尾开始的作业前面。

例如:N=5 (a 1, a 2, a s , a 4, a 5) =(3,5, 8, 7, 10)
(b 1, b 2, b 3, b 4,
b 5) = (6, 2, 1, 4, 9)
贝9( m ,
nm ,币,m , m ) = (3, 2 ,1, 4, 9)
排序之后为 (ms ,
m m ,nt , m ;)
处理m :: m=b 3 -m 排在后面; 加入m 之后的加工顺序为(
3);
处理m :T m=b 2 -m >排在后
面;
加入m >之后的加工顺序为(
2, 3); 处理m :v m=a 1 • m 排在前面;
加入m 之后的加工顺序为(
1
,
2, 3); 处理m :T m=b 4 -m 排在后面; 加入m 之后的加工顺序为( 1,
,4, 2, 3); 处理m :: m=b 5
• m
排在后面;
加入m 之后的加工顺序为(
1
,
5, 4,
2, 3);
则最优加工顺序就是(
1, 5, 4, 2, 3),最短时间为 34o 显然这疋最优解。

冋题是这种贪心策略是否正确呢?还需证明。

证明过程如下:
设S={J l , J 2,……,J n },为待加工部件的作业排序,若 A 机器开始加工S 中的部件时, B 机器还在加工其它部件,
t 时刻后再可利用,在这样的条件下,加工
S 中任务所需的最短
时间 T ( S, t ) = min{a i +T(S-{J i },b i +max{t-a i ,0})} 其中,J i € S 。

图24机器加工作业示意图
从图24可以看出,(a )为作业I 等待机器B 的情况,(b )为机器B 等待作业I 在机器A 上完成的情形。

假设最佳的方案中,先加工作业
J i ,然后加工作业J j ,则有:
T(S,t)=a i +T(S-{J i },b i +Max{t-a i ,0})
=a i
+a j +T(S-{J i ,J j },b j +max{b i +max{t-a i ,0}-a j ,0})
=a
i
+a j +T(S-{J i ,J j },T ij )
T ij =b j +max{b i +max{t-a i ,0}-a j ,0} =b j +b i -a j +max{max{t-a i ,0},a j -b i } =b i +b j -a j +max{t-a i ,a j -b i ,0} =b i +b j -a i -a j +max{t,a i ,a i +a-b i }
t b i
b j 「a j 「a j ,若 max{t ,a i ,ai+aj-bi}=t
=5 +6 -aj , 若 max{t,a i ,a i +a j -b i }=a i
bj ,
若 max{t,a i ,a i +a j -b i }=a i +a j -b i
若将作业J i 和作业J j 的加工顺序,则有:
T ' (S,t)=a i +a+T(S-(J i ,J j ),T ji ),其中
T ji =b i +b j -a i -a j +max{t,a j ,a i +a -b j } 按假设,因为T<=T ',所以有:
max{t,ai+aj-bi,ai}<=max{t,ai+aj-bj,aj}
.................... ①
于是有:
a i +a +max{-
b i ,-a j }<=a i +a +max{-b j ,-a i }

Min{b j ,a i }<=min{b i ,a j }
.................... ②
②式便是Johnson 公式。

也就是说②式成立的条件下, 任务J i 安排在任务J j 之前加工可
以得到最优解。

也就是说在 A 机器上加工时间短的任务应优先, 而在B 机器上加工时间短的
任务应排在后面。

因此,论证了开始设计的贪心算法是正确的。

算法流程如下:
for I := 1 to N do {求 M 数组}
if A[I] < B[I] then
a
i
為机器
--------
I
I
I
t :
6
E 机器一「才—'
ai-t (a ) t = 3i j max{ t- 0} =0
啊玉口巧貝-—
-J
7X v L 5 S -
1
1
1 t
1
j n ct □ • --- 七
i I® 码 1 F A-
-K 机 1TCJ
F bi-i- (t —Si )
e)
max{ t-a 0}
M[I] := A[I] else M[I] := B[I] ; 将M 从小到大排序;
S := 1; T := N;
{ 首位指针初始化 } for I := 1 to N do if 对于第 I 小的工序 J ,若 A[J] < B[J] then begin Order[S] := J;
{ 将工序 J 插在加工序列的前面 }
S := S + 1; end else begin Order[T] := J; { 将工序 J 插在加工序列的后面 }
T := T - 1; end;
程序如下: program Machine; const
Inp = 'input.txt'; Outp = 'output.txt'; MaxN = 100; var
{ 最多部件数 }
N, Min: Integer; A, B, M, O, {O 用来记录从小到大排序后部件的编号 } Order: array [1 .. MaxN] of Integer; {Order 用来记录加工顺序 }
I: Integer;
begin
Assign(Input, Inp); Reset(Input); Readln(N);
for I := 1 to N do Read(A[I]); Readln;
for I := 1 to N do Read(B[I]); Close(Input); end;
procedure Main; var
I, J, Z, S, T, T1, T2: Integer; begin FillChar(M, Sizeof(M), 0);
{求 M 数组的值}
for I := 1 to N do
if A[I] < B[I] then M[I] := A[I] else M[I] := B[I]; for I := 1 to N do O[I] := I; for I := 1 to N - 1 do
{ 从小到大排序 }
for J := I + 1 to N do
if M[O[I]] > M[O[J]] then begin Z := O[I]; O[I] :=O[J]; O[J] := Z;
procedure Init; var
{ 读入数据 }
end;
FillChar(Order, Sizeof(Order), 0);
S := 1; T := N;
for I := 1 to N do
if M[O[I]] = A[O[I]] then begin
{ 若A[O[I]]<B[O[I]] ,则插在加工序列的前面}
Order[S] := O[I];
S := S + 1;
end else begin
{ 若B[O[I]]仝A[O[I]],则插在加工序列的后面}
Order[T] := O[I];
T := T - 1;
end;
{ 计算最少加工时间}
T1 := 0; T2 := 0;
for I := 1 to N do begin
T1 := T1 + A[Order[I]];
if T2 < T1 then T2 := T1;
T2 := T2 + B[Order[I]];
end;
Min := T2;
end;
procedure Out; {打印输出}
var I: Integer;
begin
Assign(Output, Outp); Rewrite(Output);
Writeln(Min); {输出最少时间}
for I := 1 to N do {输出最佳加工序列} Write(Order[I], ' ');
Writeln;
Close(Output);
end;
Begin
End.
2019-2020年高中信息技术全国青少年奥林匹克联赛教案
递归与回溯法
课题:递归与回溯 目标:
知识目标:递归概念与利用递归进行回溯 能力目标:回溯算法的应用
重点:回溯算法 难点:回溯算法的理解 板书示意:
1) 递归的理解 2)
利用递归回溯解决实际问题(例 14、例15、例16、例17、例18)
3)
利用回溯算法解决排列问题(例
19)
授课过程:
什么是递归?先看大家都熟悉的一个民间故事: 从前有座山,山上有座庙,庙里有一个 老和尚在给小和尚讲故事,故事里说,从前有座山,
山上有座庙,庙里有一个老和尚在给小
和尚讲故事,故事里说……。

象这样,一个对象部分地由它自己组成, 或者是按它自己定义,
我们称之是递归。

例如,我们可以这样定义 N!, N!=N*(N-1)!,因此求N!转化为求(N-1)!。

这就是一个
递归的描述。

因此,可以编写如下递归程序:
program Factorial; var
N: In teger; T: Longint;
fun cti on Fac(N: In teger): Longint; begin
if N = 0 then Fac := 1 else Fac := N * Fac(N - 1) en d; begin
Write('N = '); Readl n( N); T := Fac(N); Writel n('N! = ',T); en d.
In it; Main; Out;
{输入} {主过程}
{输出}
图13展示了 N=3的执行过程。

由上述程序可以看出,递归是一个反复执行直到递归终
N=3 T^Fac(3) *
第一层;N=3^Fac (3)=3+Fac -------------
第二层:N=l^Fac
(0) *
M=0—►Fac (0)=1^
图13递归调用示例图
止的过程。

设一个未知函数f ,用其自身构成的已知函数 g 来定义:
为了定义f(n),必须先定义f(n-1),为了定义f(n-1),又必须先定义f(n-2),…, 上述这种用自身的简单情况来定义自己的方式称为递归定义。

递归有如下特点:
① 它直接或间接的调用了自己。

② 一定要有递归终止的条件,这个条件通常称为边界条件。

与递推一样,每一个递推都有其边界条件。

但不同的是,递推是由边界条件出发,通过 递推式求f(n)的值,从边界到求解的全过程十分清楚;而递归则是从函数自身出发来达到 边界条件,在通过边界条件的递归调用过程中, 系统用堆栈把每次调用的中间结果
(局部变
量和返回地址)保存起来,直至求出递归边界值 f(0)=a 。

然后返回调用函数。

返回的过程 中,中间结果相继出栈恢复,
f(1)=g(1,a) f(2)=g(2,f(1))
…… 直至求出
f(n )=g( n,f(n-1))。

由此可见,递归算法的效率往往很低,费时和费内存空间。

但是递归也有其长处,它能 使一个蕴含递归关系且结构复杂的程序简洁精炼, 增加可读性。

特别是在难于找到从边界到
解的全过程的情况下,
如果把问题进一步,其结果仍维持原问题的关系,
则采用递归算法编
间接递归一一即
P 包含另一过程 D ,而D 又调用P ;
程比较合适。

递归算法适用的一般场合为: ①数据的定义形式按递归定义。

如裴波那契数列的定义:
'1(n =0) 2(n =1 ) 斗
+ fn ^( n 兰
第二层=N=2—^Fac (2)-2+Fac ⑴
f (n
0 n,f( n-1)) a
直接递归一一递归过程 P 直接自己调用自己;
递归按其调用方式分
对应的递归程序为
fun cti on fib(n: In teger): In teger; begi n if n = 0 then fib := 1
{递归边界}
else if n = 1 the n fib := 2 {递归边界}
else
这类递归问题可转化为递推算法,递归边界为递推的边界条件。

例如上例转化为递推算
法即为
fun cti on fib(n: In teger): In teger;
begi n
f[0]:= 1; f[1] := 2; {递推边界}
for I :=
=2 to n do f[I]:= =f[I - 1] + f[I
- 2];
fib := f(n); end;
② 数据之间的关系(即数据结构)按递归定义。

如树的遍历,图的搜索等。

③ 问题解法按递归算法实现。

例如回溯法等。

对于②和③,可以用堆栈结构将其转换为非递归算法,以提高算法的效率以及减少内存 空间的浪费。

下面以经典的N 皇后问题为例,看看递归法是怎样实现的,以及比较递归算法和非递归 算法效率上的差别。

例15: N 皇后问题
在N*N 的棋盘上放置 N 个皇后而彼此不受攻击(即在棋盘的任一行,任一列和任一对角
fib := fib(n en d; -2) + fib(n - 1);
{递
1 2 3 4 5 6 7 8
12 3 4 5 6 7 8
图14八皇后的两组解
线上不能放置 2 个皇后),编程求解所有的摆放方法。

分析:
由于皇后的摆放位置不能通过某种公式来确定, 因此对于每个皇后的摆放位置都要进行 试探和纠正,这就是“回溯”的思想。


N 个皇后未放置完成前,摆放第 I 个皇后和第 I+1
个皇后的试探方法是相同的,因此完全可以采用递归的方法来处理。

下面是放置第 I 个皇后的的递归算法:
Procedure Try (I:integer ) ;
{ 搜索第 I 行皇后的位置 } var
j:integer; begin
if I=n+1 then 输出方案; for j:=1 to n do
if 皇后能放在第 I 行第 J 列的位置 then begin
放置第 I 个皇后;
对放置皇后的位置进行标记; Try (I+1 ) 对放置皇后的位置释放标记;
End;
End;
N 皇后问题的递归算法的程序如下: program N_Queens; const
MaxN = 100; var
A:array [1..MaxN] of Boolean; C:array [1
- MaxN..MaxN- 1] of Boolean;{
左下到右上斜线被控制标记
}
procedure Out; var
I: Integer; begin
Inc(Total); Write(Total: 3, ‘ : ' );
for I := 1 to N do Write(X[I]: 3); Writeln; end;
procedure Try(I: Integer); { 搜索第 I 个皇后的可行位置 }
{ 最多皇后数 }
{ 竖线被控制标记 }
B:array [2..MaxN * 2] of Boolean; { 左上到右下斜线被控制标记 }
X: array [1 .. MaxN] of Integer; Total: Longint; N: Integer; { 记录皇后的解 }
{解的总数 }
{输出方案 }
var J: Integer;
begin
if I = N + 1 then Out; {N for J := 1 to N do
if A[J] and B[J + I] and C[J X[I] := J; A[J] := False; B[J + I] := False; C[J - I] :=
False; Try(I + 1); A[J] := True; B[J + I] := True; C[J - I] := True;
end;
end;
begin
Write( ‘ Queens Numbers = ‘); Readln(N);
FillChar(A, Sizeof(A), True); FillChar(B, Sizeof(B), True); FillChar(C, Sizeof(C), True); Try(1);
Writeln( ‘Total = ‘, Total); end.
个皇后都放置完毕,则输出解 }
-I] then begin
{ 搜索下一皇后的位置 }
N 皇后问题的非递归算法的程序: program N_Queens; const MaxN = 100;
var
A:array [1..MaxN] of Boolean; B:array [2..MaxN * 2] of Boolean;
{ 最多皇后数 }
{ 竖线被控制标记 }
{ 左上到右下斜线被控制标记 }
for I := 1 to N do Write(X[I]: 3); Writeln; end;
procedure Main; var
K: Integer; begin
X[1] := 0; K := 1; while K > 0 do begin if X[K] <> 0 then begin A[X[K]] := True; B[X[K] + K] := True;
C[X[K] — K] := True;
end;
Inc(X[K]);
while(X[K]<=N)and not(A[X[K]]and B[X[K]+K]and C[X[K]
- K])do
Inc(X[K]);
{ 寻找一个可以放置的位置 }
if X[K] <= N then if K = N then Out else begin
A[X[K]] := False; B[X[K] + K] := False;
C[X[K] - K] := False;
Inc(K); X[K] := 0; {继续放置下一个皇后 }
end else Dec(K); {回溯 }
end; end; begin Write(
‘ Queens Number = ‘);
Readln(N);
FillChar(A, Sizeof(A), True); FillChar(B, Sizeof(B), True); FillChar(C, SizeofI, True);
C:array [1 - MaxN..MaxN- 1] of Boolean;{
左下到右上斜线被控制标记
}
X: array [1 .. MaxN] of Integer;
{ 记录皇后的解 } Total: Longint; {解的总数 }
N: Integer;
{皇后个数 procedure Out; var {输出方案 }
I: Integer; begin Inc(Total); Write(Total: 3,
: ' );
Main;
Writeln( ‘Total = ‘, Total);
end.
使用递归可以使蕴含复杂关系的问题,结构变得简洁精炼。

看看下面的例题。

例16: 新汉诺(hanoi )塔问题
设有n 各大小不等的中空圆盘,按从小到大的顺序从 迭套在三根立柱上,立柱的编号分别为
A 、
B C,这个状态称之为初始状态。

问题要求找到
一种步数最少的移动方案,使得从初始状态转变为目标状态。

移动时有如下要求:
① 一次只移动一个盘;
② 不允许把大盘移到小盘上边;
输入:输入文件第1行是状态中圆盘总数;第 2~4行是分别是初始状态中 A 、B 、C 柱上 的圆盘个数和从上到下每个圆盘的编号;
第5~7行是分别是目标状态 A B 、C 柱上的圆盘个
数和从上到下每个圆盘的编号。

输出:每行写一步的移动方案,格式为:
Move I 圆盘 form P 柱 to Q 柱。

最后输出最少步数。

输入样例(如图):
6 3 1 2 3 2 4 5 1 6 0
6 1 2 3 4 5 6 0
样例所描述的状态如图 15所示。

初始状态
图15样例图
输出样例: 分析:
要从初始状态移动到目标状态,就是把每个圆盘分别移动到自己的目标状态。

而问题的 关键一步就是:首先考虑把编号最大的圆盘移动到自己的目标状态, 而不是最小的,因为编
号最大的圆盘移到目标位置之后就可以不再移动了, 而在编号最大的圆盘未移到目标位置之
前,编号小的圆盘可能还要移动,
编号最大的圆盘一旦固定, 对以后的移动将不会造成影响。

根据上面的分析可设计如下过程
1到n 编号。

将这n 个圆盘任意的
A
| ±
B
m 2 | 1 3

5 1 6
1
c
目标状态
Move(K, W);
表示把编号K的圆盘从当前所在柱移动到W柱的过程。

下面对样例进行分析。

则口它已4 F rom E to C .Move 3 From B to A -Move 5 Firom A t o B .
wove 1 Front A t □ C»Move 1 From C to B .Move 1 Fretn C to A.
Move 2 From A to 5 .Move 2 From C to A«Move 2 Fir om C t o B .
Wove 1 F rom C to E .Move 1 From B 七口A»Move 1 F ITOKI A to E -
3口p亡3 F rom A 七匸i C .Move 6 From C to B ,Move 3 Fazoim C t o A.
wove 1 From B to A»Nov ft 1 From A to B .MOV ft 1 Ft Oln B t O C .
Move 2 From B to C 9Move 2 From A to C <Move 2 From B to A,
Move 1 F r orn A 七匚i C・Mofve 1 From B 七口C .Move 1 Fxrowi C to A -
血口7亡 5 F rom B 七匚i 真.Move 3 From A 七口B .Morire 4 From C t o B .
wove 1 From C to B*Movt 1 From C to A.Move 1 From A to B .
Move 2 Firom C 七 o A P Move 2 From C to B.Move 2 From A t O C
Wove 1 F rora B 七口A.Move 1 From A 七口B .Monre 1 Froiti B to C .
Wove 3 F rom C to E .Move 4 From A to C .Move 3 Firorm A t o B .
Move 1 From A 匸口C »Move 1 Fizcw B to C .Move 1 Froin C to A.
Move 2 F rom A to E.Move 2 From B to A <Move 2 Froiri C t o B
Wave 1 F r oin C to B .Move 1 From C 七口A...Move 1 Frrom A to B .
Move 4 F rom C 七口A.Monre 3 From B to C .56
Move 1 FcOzn B to A»Move 1 FlZCtD A to B .
Move 2 F roxti B to C.Move 2 From A to C *
Move 1 F rom A 七 a C .Move 1 From B 七口C .
将6号盘移动到B柱,在此之前一定将经历如图16所示的状态
ABC
m
2
3 1
4
5吕
图16样例移动过程
要移动6号盘首先要把1~5号盘全部移开,也就是说,既不能移动到6号盘的初始立柱上,也不能移动到6号盘的目标立柱上。

显然这里要将它们移动到A柱。

然后再将6号盘移到位。

此时状态如图17所示。

图17样例移动过程
同时我们注意到:把1~5盘移动到目标的过程和将6号盘移动到B柱的过程,从形式上是一样的,只是盘的编号不同而已。

显然这是个递归过程,可以采用递归法实现。

算法设计如下:
procedure Move(K, W) ;
{编号K的圆盘从当前所在柱移动到W柱}
begin
if K 号盘已经在W立柱上then Exit ; {递归边界}
for I := K - 1 downto 1 do
Move(l, 过渡立柱); {将编号小于K的盘都移到过渡立柱上去}输出当前移动方案;
将K号盘移到W立柱上;
lnc(Step); {累计步数}
end;。

相关文档
最新文档