1k likes | 1.15k Vues
例 3 : 输入一串以 ‘ ! ’ 结束的字符,按逆序输出。. program p4 (input , output) ; procedure rever ; var c : char ; begin read(c) ; if c<>'!' then rever; write(c) ; end ; begin { 主程序 } rever ; end . 运行: 输入 hey! 输出 !yeh 。.
E N D
例3:输入一串以‘!’结束的字符,按逆序输出。例3:输入一串以‘!’结束的字符,按逆序输出。 program p4(input,output); procedure rever; var c:char; begin read(c); if c<>'!' then rever; write(c); end; begin {主程序} rever; end. 运行: 输入 hey! 输出 !yeh。 程序中,c 是过程rever的局部变量。每一次递归调用,都要为局部变量重新分配单元,因此各层的变量c实际上是不同的量,它们分别用于保存执行本层调用时的输入值。
hey! c=‘!’ procedure rever; var c:char; begin read(c); if c<>'!' then rever; write(c); end; c=‘y’ procedure rever; var c:char; begin read(c); if c<>'!' then rever; write(c); end; c=‘e’ procedure rever; var c:char; begin read(c); if c<>'!' then rever; write(c); end; c=‘h’ procedure rever; var c:char; begin read(c); if c<>'!' then rever; write(c); end;
程序中的递归过程图解如下: procedure num(x: integer) begin if x=5 then a:=10 else begin num(x+1); a:=a+2 end end; 整个递归过程可视为由往返双向“运动”组成,先是逐层递进,逐层打开新的“篇章”,(有可能无具体计算值)当最终递进达到边界,执行完本“层”的语句,才由最末一“层”逐次返回到上“层”,每次返回均带回新的计算值,直至回到第一次由主程序调用的地方,完成对原问题的处理。
计算机求解的过程 • 在状态空间寻找机内解, 可以看成是从初始状态出发,搜索目标状态(解所在的状态)的过程。 状态空间 初始状态 搜索 目标状态 • 搜索的过程可描述为:S0S1…Sn,其中S0为初态,Sn为终态。或者说ψ(S0)且φ(Sn),这里ψ称为初始条件,φ称为终止条件。
求解是状态空间的搜索 求解的过程可以描述为对状态空间的搜索 S0 S0 其中S0为初始状态,不妨设Sni为 终止状态 … S11 S12 S1k … … … … … … …… …… Snm Sn1 Sni Sni • 问题求解就是通过搜索,寻找出一条从初始状态S0到终止状态Sni的路径。
几种搜索方法 状态空间的搜索实际上是一种树/DAG(Directed Acyclic Graph )的搜索,常用的方法有: • 广度优先搜索 • 深度优先搜索 • 启发式搜索 从初始状态开始,逐层地进行搜索。 从初始状态开始,逐个分枝地进行搜索。 从初始状态开始,每次选择最有可能达到终止状态的结点进行搜索。
三种搜索的优劣之处 • 一般来说,三种搜索方法各有优劣之处: • 广度优先搜索和深度优先搜索优点:一定能找到解;缺点:时间复杂性大。 • 启发式搜索优点:一般来说能较快地找到解,即其时间复杂性小;缺点:需要设计一个评价函数,并且评价函数的优劣决定了启发式搜索的优劣。
回溯法 • 当需要找出问题的解集,或者要求回答什么解是满足某些约束条件的最佳解时,往往要使用回溯法。 • 回溯法的基本做法是搜索,或是一种组织得井井有条的,能避免不必要搜索的穷举式搜索法。这种方法适用于解一些组合数相当大的问题。 • 在问题的解空间树中,回溯法按深度优先策略,从根结点出发搜索解空间树。 • 算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解。 • 如果肯定不包含,则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯;否则,进入该子树,继续按深度优先策略搜索。
n=3时的0-1背包问题用完全二叉树表示的解空间 回溯法的算法框架5.1.1 问题的解空间 • 应用回溯法解问题时,首先应明确定义问题的解空间。问题的解空间应至少包含问题的一个(最优)解。通常将解空间组织成树或图的形式。 • 问题的解向量:回溯法希望一个问题的解,能够表示成一个n元式(x1,x2,…,xn)的形式。 • 显约束:对分量xi的取值限定 • 隐约束:为满足问题的解,而对不同分量之间施加的约束。 • 解空间:对于问题的一个实例,解向量满足显式约束条件的所有多元组,构成了该实例的一个解空间。 • 例如,对于有n种可选物品的0-1背包问题,其解空间由长度为n的0-1向量组成。
回溯法的基本思想 • 扩展结点:一个正在产生儿子的结点 • 活结点:一个自身已生成但其儿子还没有全部生成的节点 • 死结点:一个所有儿子已经产生的结点 • 深度优先的问题状态生成法:如果对一个扩展结点R,一旦产生了它的一个儿子C,就把C当做新的扩展结点。在完成对子树C(以C为根的子树)的穷尽搜索之后,将R重新变成扩展结点,继续生成R的下一个儿子(如果存在) • 宽度优先的问题状态生成法:在一个扩展结点变成死结点之前,它一直是扩展结点。 • 回溯法:为了避免生成那些不可能产生最佳解的问题状态,要不断地利用限界函数(bounding function)来处死那些实际上不可能产生所需解的活结点,以减少问题的计算量。具有限界函数的深度优先生成法称为回溯法。
基本思想: • 确定了解空间的组织结构后,回溯法就从开始结点(根结点)出发,以深度优先的方式搜索整个解空间。 • 开始结点就成为一个活结点,同时也成为当前的扩展结点。 • 在当前扩展结点,搜索向纵深方向移至一个新结点。这个新结点就成为一个新的活结点,并成为当前扩展结点。 • 如果在当前的扩展结点处不能再向纵深方向移动,则当前扩展结点就成为死结点。 • 此时,应往回移动(回溯)至最近的一个活结点处,并使这个活结点成为当前的扩展结点。 • 回溯法即以这种工作方式递归地在解空间中搜索,直至找到所要求的解或解空间中已没有活结点时为止。
可重复的全排列 假设是由1-3组成的3位数 program expl; var i,j,k:integer; begin for i:=1 to 3 do for j:=1 to 3 do for k:=1 to 3 do writeln(i,' ',j,' ',k); end.
如果是5位呢? 更多位呢? 不定位呢?
试一试,如果n位,n<6,如何改动? program expl_dg; var a:array[1..10] of integer; procedure print; var i:integer; begin for i:=1 to 3 do write(a[i],' '); writeln; end; procedure work(x:integer); var i:integer; begin if x>3 then begin print ;exit;end; for i:=1 to 3 do begin a[x]:=i; work(x+1); end; end; begin work(1); end. 如果要求输出不重复的全排列呢?
program expl_dg; var a:array[1..10] of integer; n:integer; procedure print; var i:integer; begin for i:=1 to n do write(a[i],' '); writeln; end; function try(x,i:integer):boolean; var j:integer; begin try:=true; for j:=1 to x-1 do if a[j]=i then begin try:=false;break;end; end; procedure work(x:integer); var i:integer; begin if x>n then begin print;exit;end; for i:=1 to n do if try(x,i)=true then begin a[x]:=i; work(x+1); end; end; begin readln(n); work(1); end. 剪枝
关于回溯法搜索的递归程序框架全排列算法的递归解法为例做个介绍:关于回溯法搜索的递归程序框架全排列算法的递归解法为例做个介绍: const n=5;var a:array [1..n] of integer; {记录搜索的解} e:array [1..n] of boolean; {标记数组用于记录i是否已经用过} {初始状态都是false} procedure print; var i:integer; begin for i:=1 to n do write(a[i],' '); end;procedure search(deep:integer); {deep是搜索深度} var i:integer; begin if deep>n then begin print; {输出结果} exit; {退出本层,回溯} end; for i:=1 to n do if e[i]=false then begin {如果i没有用过,限定条件} e[i]:=true; {标记为用过i} a[deep]:=i; {记录该位置的元素为i} search(deep+1); {搜索deep+1个位置上的元素} e[i]:=false; {回溯之后取消i元素的已用标记} end; end; 在很多题目中,"限定条件"需要用邻接矩阵等数据表示,这个"限定条件"就是输出结果中需要满足的条件,具体的可以根据题目来修改。
数字排列问题的递归实现 procedure try(k:integer); var i :integer; begin if( )then begin print; exit; end; for i:=1 to n do begin ( ); if( )then try(k+1) end end; begin readln(n); try(1); end. var i,k,n:integer; x:array[1..9] of integer; function place(k:integer):boolean; var i:integer; begin place:=true; for i:=1 to k-1 do if( )then begin place:=false; break end ; end; procedure print; var i:integer; begin for i:=1 to n do write(x[i],' '); writeln; end; k>n x[i]=x[k] x[k]:=i place(k)
四色问题 • 有形如下列图形的地图,图中每一块区域代表一个省份,现请你用红(1)、兰(2)、黄(3)、绿(4)四种颜色给这些省份填上颜色,要求每一省份用一种颜色,且任意两个相邻省份的颜色不能相同,请给出一种符合条件的填色方案。地图用无向图的形式给出,每个省份代表图上的一个顶点,边代表两个省份是相邻的。 返回到题目列表
四色问题 5 6 7 4 2 3 1 • 输入70 1 0 0 0 0 11 0 1 1 1 1 10 1 0 1 0 0 00 1 1 0 1 0 00 1 0 1 0 1 00 1 0 0 1 0 11 1 0 0 0 1 0 • 输出1 2 1 3 1 3 4 返回到题目列表
四色问题 • 要得到一种可行的填色方案,比较直接的做法就是一个省一个省的填色,在填色的过程中去检查是否和周围省份的颜色存在冲突。 • 从第一个省份开始,对每个省份依次尝试四种颜色,判断当然选择的颜色是否和相邻的省份相同。 • 若都不相同,则将此省份填上当前的颜色。 • 若存在相同,则取下一种颜色继续尝试。 • 若四种颜色都不能填上,则退回上一个省份并选择下一种颜色继续尝试。 • 当最后一个省份也被填色后就找到一组可行解。 返回到题目列表
四色问题 n块,m代表当前在填第几块 procedure search(m: integer); // 递归搜索第m个省份 begin if m > n then // 是否所有省份都已经填色 begin write(s[1]); // 已全部填色,输出结果,终止程序 for j := 2 to n do write(' ', s[j]); writeln; halt end else // 还未全部填色 for j := 1 to 4 do // 依次用四种颜色对当前省份填色 begin if 可以 then {同相邻的块没有色彩冲突} begin // 没有冲突 s[m] := j; // 记录当前省份颜色 search(m + 1); // 递归检查下一个省份 end; end; end;
迷宫问题 以一个 m*n的长方阵表示迷宫,0和1 分别表示迷宫中的通路和障碍(如下图所示)。 设计一个程序,对任意设定的迷宫,如果只能按上下左右四个方向移动,求出一条从入口到出口的通路。 或得出没有通路的结论。 在方阵中,允许运动的方向为上下左右,因此,对于每个状态,都有四个方向可以扩展搜索,为了简化程序,我们可以设计增量数组描述不同的运动方向。另外,为了回避边沿位置移动方向的限制(如上边沿的位置不能再向上移动等),可以对方阵进行扩充,增加一圈且有障碍。如图所示。
const maxm=10; maxn=10; const dx:array[1..4] of integer=(-1,1,0,0); { 上下左右方向 } dy:array[1..4] of integer=(0,0,-1,1); type address=record x,y:integer; end; var a:array[0..maxm+1,0..maxn+1] of integer; step:array[1..maxm*maxn] of address; m,n,x0,y0,x1,y1:integer; procedure init; var i,j:integer; begin readln(m,n); for i:=0 to m+1 do for j:=0 to n+1 do a[i,j]:=1; for i:=1 to m do for j:=1 to n do read(a[i,j]); readln(x0,y0); readln(x1,y1); step[1].x:=x0; step[1].y:=y0; end; procedure print(steps:integer); var i:integer; begin for i:=1 to steps-1 do write('(',step[i].x,',',step[i].y,')-->'); writeln('(',step[steps].x,',',step[steps].y,')'); halt; end; procedure try(i:integer); var j,x,y:integer; begin for j:=1 to 4 do begin x:=step[i-1].x+dx[j]; y:=step[i-1].y+dy[j]; if a[x,y]=0 then begin step[i].x:=x; step[i].y:=y; a[x,y]:=-1; if (x=x1) and (y=y1) then print(i) else try(i+1); a[x,y]:=0; end; end; end; begin init; a[x0,y0]:=1;try(2); end.
N皇后问题 [问题描述] 在n×n的国际象棋盘上,放置n个皇后,使任何一个皇后都不能吃掉另一个,要使任何一个皇后都不能吃掉另一个,需满足的条件是:同一行、同一列、同一对角线上只能有一个皇后。求放置方法. 如:n=4时,有以下2种放置方法. 输出: 2 4 1 3 3 1 4 2
n皇后问题 返回到题目列表
n皇后问题的解空间就应该是1~n全排列的一部分。n皇后问题的解空间就应该是1~n全排列的一部分。 • 解空间的长度是n。 • 解空间的组织形式是一棵n叉树,一个可行的解就是从根节点到叶子节点的一条路径。 • 控制策略则是当前皇后与前面所有的皇后都不同列和不同对角线。 返回到题目列表
var x:array[1..n] of integer; a:array[1..n] of boolean; {列控制标志:true:可以放,false:不能放} b:array[2..2*n] of boolean; {左上右下方斜线控制标志,true:可以放,false:不能放} c:array[1-n..n-1]of boolean; {左下右上方斜线控制标志,true:可以放,false:不能放} 初始时: fillchar(x,sizeof(x),0); fillchar(a,sizeof(a),true); fillchar(b,sizeof(b),true); fillchar(c,sizeof(c),true);
procedure try(i:integer); var j:integer; begin if i=n+1 then print else for j:=1 to n do if a[j] and b[i+j] and c[i-j] then begin x[i]:=j; a[j]:=false; {列控制标志} b[i+j]:=false; {左上右下方斜线控制标志} c[i-j]:=false; {左下右上方斜线控制标志} try(i+1); {如果不能递归进行,既a[j] and b[i+j] and c[i-j]=false: 无法放置i+1个皇后,说明当前皇后i放置不正确,要回溯,消除标志} a[j]:=true; b[i+j]:=true; c[i-j]:=true end; end; begin try(1) end.
皇后序号 四皇后问题的递归实现 procedure try(k:integer); var i:integer; begin if( ) then begin print; ( ) end; for i:=( )do begin ( ); if place(k) then( ); end; end ;begintry(1);{摆放第一个皇后} end. const n=4;var i,j,k:integer; x:array[1..n] of integer; {保存第i个皇后的列号}function place(k:integer):boolean; var i:integer; begin place:=true; for i:=1 to k-1 do if(x[i]=x[k])or(abs(x[i]-x[k])=abs(i-k)) then place:=false; end;procedure print; var i:integer; begin for i:=1 to n do write(x[i]:4); writeln; end; k=n+1 因为从第i个皇后到第i+1个皇后的摆放方法是相同的,所以可以用递归的方法. exit 1 to n x[k]:=i try(k+1) 摆放下一个皇后
跳马问题 • 在n×m棋盘上有一中国象棋中的马: • 马走日字; • 马只能往右走。 • 请你找出一条可行路径,使得马可以从棋盘的左下角(1,1)走到右上角(n,m)。 返回到题目列表
跳马问题 • 输入:9 5 • 输出:(1,1)->(3,2)->(5,1)->(6,3)->(7,1)->(8,3)->(9,5) 返回到题目列表
跳马问题 • 马走日字,当马一开始在黄点时,它下一步可以到达的点有以下的八个,但由于题目规定了只能往右走的限制,所以它只能走到四个绿色点之一。
A 跳马问题 • 当马一开始位于左下角的时候,根据规则,它只有两条线路可以选择(另外两条超出棋盘的范围),我们无法预知该走哪条,故任意选择一条,到达A1。 A2 A1
跳马问题 • 当到达A1点后,又有三条线路可以选择,于是再任意选择一条,到达B1。 • 从B1再出发,又有两条线路可以选择,先选一条,到达C1。 B3 B2 C2 A2 C1 A1 B1 A
跳马问题 • 从C1出发,可以有三条路径,选择D1。但到了D1以后,我们无路可走且D1也不是最终目标点,因此,选择D1是错误的,我们退回C1重新选择D2。同样D2也是错误的。再回到C1选择D3。D3只可以到E1,但E1也是错误的。返回D3后,没有其他选择,说明D3也是错误的,再回到C1。此时C1不再有其他选择,故C1也是错误的,退回B1,选择C2进行尝试。 B3 D3 B2 C2 D2 A2 C1 E1 A1 B1 A D1
跳马问题 • 从C2出发,有四条路径可以选择,选择D4,从D4出发又有两条路径,选择E1错误,返回D4选择E2,从E2出发有两条路径,先选择F1错误,返回E2选择B,而B恰好是我们要到达的目标点,至此,一条路径查找成功。 B B3 B2 C2 E2 A2 A1 E1 B1 A D4 F1
跳马问题 • 从上面的分析我们可以得知: • 在无法确定走哪条线路的时候,任选一条线路进行尝试; • 当从某点出发,所有可能到达的点都不能到达终点时,说明此点是一个死节点,必须回溯到上一个点,并重新选择一条新的线路进行尝试。
为了描述路径,我们最直接的方法就是记录路径上所有点的坐标。但比较繁琐。为了描述路径,我们最直接的方法就是记录路径上所有点的坐标。但比较繁琐。 • 如果我对马可以走到的四个点都编上号(方向),那么我们很容易得到每一个方向上两个坐标和原位置的关系。 • 同时,四个方向都有了编号,那么在选择路径的时候也就可以不采用任选的方式了,只需按照编号依次尝试就可以了。
4 3 2 1 若马所处的位置为(x,y),则其下一步可以到达的四个位置分别是(x+1, y-2),(x+2, y-1),(x+2, y+1),(x+1, y+2)。 在使用增量数组后,只要知道方向t,下一步的位置就是(x+dx[t], y+dy[t])。 增量数组:dx = (1, 2, 2, 1)dy = (-2, -1, 1, 2)
为了判断棋子是否落在棋盘上,我们还必须知道当前棋子(扩展节点)的位置信息。而用一系列的方向来表示就比较麻烦,因此,我们另外使用两个变量来保存当前棋子的坐标。但这时千万要注意的是一定要在回溯的时候,修改当前棋子的坐标。为了判断棋子是否落在棋盘上,我们还必须知道当前棋子(扩展节点)的位置信息。而用一系列的方向来表示就比较麻烦,因此,我们另外使用两个变量来保存当前棋子的坐标。但这时千万要注意的是一定要在回溯的时候,修改当前棋子的坐标。 • 跳马问题的解空间是从左下角到右上角的所有路径。 • 解空间的长度是所有路径中最长的路径的长度。 • 解空间的组织形式是一棵四叉树,一个可行的解就是从根节点到叶子节点的一条路径。 • 控制策略则是马必须在棋盘内。
分析: 1、马跳的方向: x:array[1..4,1..2]of integer= ((1,-2),(2,-1),(2,1),(1,2)); 4个方向横向和纵向的增量。 2、记录马经过的位置坐标 a:array[1..16,1..2]of integer; 第i步所在的位置,1:横坐标 2:纵坐标 3、马的当前位置:(a[I,1],a[I,2]) 下一个位置可能是: (a[I,1]+x[j,1],a[I,2]+x[j,2]) 1<=j<=4 4、目标:a[I,1]=n;a[I,2]=m;
const maxx=15; maxy=15; x:array[1..4,1..2]of integer=((1,-2),(2,-1),(2,1),(1,2)); {4个方向} var n,m,t:integer; a:array[1..maxx+1,1..2]of integer;{记录走的路径坐标} procedure print(i:integer); var j:integer; begin inc(t); write(t,':'); for j:=1 to i do write('(',a[j,1],' ',a[j,2],')'); writeln; end;
procedure try(i:integer); {搜索到当前第i个点} var j:integer; begin if (a[i,1]=n) and (a[i,2]=m) then print(i); for j:=1 to 4 do if (a[i,1]+x[j,1]>=0)and(a[i,1]+x[j,1]<=n) and(a[i,2]+x[j,2]>=0)and(a[i,2]+x[j,2]<=m){判界} then begin a[i+1,1]:=a[i,1]+x[j,1]; a[i+1,2]:=a[i,2]+x[j,2]; try(i+1); end; end; begin assign(output,'house1.out'); rewrite(output); readln(n,m); t:=0; a[1,1]:=0;{起始位置作为第1个点,x,y为0} a[1,2]:=0; try(1); close(output); end.
求最少步到达B点。 Best:最短路线,a:临时得到的一个路线。Min:最少步。 procedure try(i:integer); {搜索到当前第i个点} var j:integer; begin if ((a[i,1]=n) and (a[i,2]=m))and(i<min) then begin min:=i;best:=a;exit;end; {记下当前最短路径和最少步数} if ((a[i,1]<>n) or (a[i,2]<>m))and(i>=min) then exit; {剪枝优化} for j:=1 to 4 do if (a[i,1]+x[j,1]>=0)and(a[i,1]+x[j,1]<=n) and(a[i,2]+x[j,2]>=0)and(a[i,2]+x[j,2]<=m) then begin a[i+1,1]:=a[i,1]+x[j,1]; a[i+1,2]:=a[i,2]+x[j,2]; try(i+1); end; end; 19 19
跳马问题(思考) • 在本例中,我们只要求输出一条可行路径,如果我们需要输出所有的可行路径,怎样改动此程序呢? • 如果我们把问题改成“在n×m的棋盘上有一骑士,开始时,骑士在点(x,y),现在请你找出一条可行的路径,使得骑士可以不重复的走遍棋盘上的每一个点。”的话,又要如何编写此程序呢?
0,1背包问题 • 已知一个容量大小为M重量的背包和N种物品,每种物品的重量为Wi。若将物品放入背包将得到Pi的效益,求怎样选取物品将得到效益最大 输入:第一行一个整数,为背包的容量M;第二行一个整数,为物品的种数N; 第三行N个整数为各物品的重量;第四行N个整数分别为N个物品的价值输出:第一行为最大总价值;第二行为装入的各物品的重量(未装的物品用0); 第三行为装入的各物品的价值 (未装的物品用0) 返回到题目列表
算法分析 • 本题可以用递归求解:设当前有N个物品,容量为M;因为这些物品要么选,要么不选,我们假设选的第一个物品编号为I(1~I-1号物品不选),问题又可以转化为有N-I个物品(即第I+1~N号物品),容量为M-Wi的子问题……如此反复下去,然后在所有可行解中选一个效益最大的便可。 • 另外,为了优化程序,我们定义一个函数如下: • F[I]表示选第I~N个物品能得到的总效益。不难推出: • F[N]=Pn • F[I]=F[I+1]+Pi (I=1…N-1) • 假设当前已选到第I号物品,如果当前搜索的效益值+F[I+1]的值仍然比当前的最优值小,则没有必要继续搜索下去。 返回到题目列表
算法框架 procedure search(i:integer; j:byte); {递归求解} var k:byte; begin if now+f[j]<=ans then exit; {如果没有必要搜索下去} if now>ans then begin {修改最优解} ans:=now; out:=ou; end; for k:=j to n do {选取物品} if w[k]<=i then begin now:=now+p[k]; ou[k]:=true; search(i-w[k],k+1); now:=now-p[k]; ou[k]:=false; end; end;