图的基本知识
一、什么是图
什么是计算机中所说的图?请先看下面的“柯尼斯堡桥问题”。传说在东普鲁士境内,有一座柯尼斯堡城,希雷格尔河流经这个城市的克奈霍福岛后,就将这个城市一分为二,形成如图1—1(左)的A、B、C、D 4个地区。人们建造了7座桥将这4个地区连起来。在游览中有人提出,是否可以从A地出发,各座桥恰好通过一次,最后又回到原来出发地呢?
![](http://qfzx.nhedu.net/IT/UploadFiles/20031026171252734.gif)
这个问题在18世纪被数学家欧拉解决了。他把这个问题转化为图1—1右边所示的图。图上用A、B、C、D4个顶点分别表示4个地区,用两点间的线段表示连接各地的桥。这样原来的问题就转化为:从A顶点出发经过其中每一条线段一次,而且仅一次,再回到A点的“一笔画”问题。
欧拉对柯尼斯堡问题作出了否定的结论。因为对于每一个顶点,不论如何经过,必须有一条进路和一条出路,所以对每一个顶点(除起点和终点)来说与它有关的线段(称为边)必须是偶数条。而图1-1(右)的顶点有关的线段都是奇数条,因此不可能一笔画出。而如图4—12中的图形是可以一笔画出的。
欧拉通过对柯尼斯堡桥问题的研究,于1736年发表了著名的关于图论的论文,从而创立了图论的学说。图1—2一类的问题就是图论中所指的图。
![](http://qfzx.nhedu.net/IT/UploadFiles/20031026171258580.gif)
又如,有6个足球队之间进行循环赛,他们比赛的场次可以用图1-3(1)来表示。有3个人相互写信,可以用图1—3(2)来表示。
![](http://qfzx.nhedu.net/IT/UploadFiles/20031026171258621.gif)
从上面两个例子可看出,我们这里所说的图(graph),与人们通常所熟悉的图,如圆、四边形、函数图象等是很不相同的。是指某些具体事物和这些事物之间的联系。如果我们用点来表示事物(如地点、队),用线段来表示两事物之间的联系,那么一个图就是表示事物的点集和表示事物之间联系的线段集合所构成。其中线段仅表示两点的关系,它的长短与曲直是无关紧要的。例如图1-4中3个图,被认为是同一个图。
![](http://qfzx.nhedu.net/IT/UploadFiles/20031026171258360.gif)
二、图的基本概念
定义:图G定义为一个偶对(V,E),记作G:(V,E)。其中
1)V是一个非空有限集合,它的元素称为顶点;
2)E也是一个集合,它是如下集合(它的元素称为边)的子集:
E∩{(a,b|a ∈ V,b ∈ V}
例如图4-1中的图有4个顶点,4条边。
或者定义:图G(Graph)是由顶点的集合V和边的集合E所组成的二元组,记作:
其中V是顶点的集合,E是边的集合。
无向图与有向图:边的表示方式是用该边的两个顶点来表示的,如果边的表示无方向,那么,对应的图就是无向图,否则称为有向图,如下图所示:
![](http://qfzx.nhedu.net/IT/UploadFiles/20031026171258371.gif)
在无向图中,边的两个顶点在边的表示中可以互换,如边(V1,V4)与边(V4,V1)是等价的,表示的是同一条边。(无向图中边的表示用圆括号)
在有向图中,边的走向不同就认为是不同的边。如在边的集合E={< 1,4 >,< 3,4 >,< 5,2 >,< 5,3 >,< 2,1 >,< 5,5 >}(见右上图)中,其中< 1,4 >表示该边是由顶点1出发,到顶点4结束,即边< 1,4 >表明了该边的方向性,且两个顶点的顺序不能颠倒。(有向图中边的表示用尖括号)
简单图(无环)与完全图(每一对不同的顶点都有一条边相连)
顶点的度:与顶点关联的边的数目,有向图中等于该顶点的入度与出度之和。
入度——以该顶点为终点的边的数目和
出度——以该顶点为起点的边的数目和
图的阶:图中顶点的个数。例如图4—14中的图的阶是4,图1—3中分别是6和3。
度数为奇数的顶点叫做奇点,度数为偶数的点叫做偶点。
例如图1-4中,A和C是奇点,B和D是偶点。
[定理1] 图G中所有顶点的度数之和等于边数的2倍。因为计算顶点的度数时。每条边均用到2次。
[定理2] 任意一个图一定有偶数个奇点。
连通:如果顶点u,v属于G,u,v之间有一条从u通过若干条边到达v的通路,则认为顶点u和v是连通的。
连通图:如果对于图G中每一对不同顶点u,v都有一条(u,v)通路,则称G是连通图。
通路指u-->边1-->顶点1-->边2-->顶点2-->……-->v,点和边交替相接,且互不相同。
图的存储结构
在计算机中,有多种方法存储图的信息,由于图的结构复杂,使用广泛,一般应根据实际的应用,选择适合的表示方法。常用的图的存储结构有邻接矩阵、邻接多重表和邻接表。
1、邻接矩阵(数组)
邻接矩阵是表示顶点之间相邻关系的矩阵,实质上是一个二维数组。设G=(V,E)是具有n>=1个顶点的图,则G的邻接矩阵是具有如下性质的n阶方阵:
若(Vi,Vj)或< Vj,Vi >是E(G)中的边,则A[i,j]=1,否则A[i,j]=0,如:
| 0 1 0 1 | | 0 0 0 0 0 | | 1 0 1 1 | | 1 0 0 0 0 | A(G)= | 0 1 0 1 | B(G)= | 0 0 0 1 0 | | 1 1 1 0 | | 0 0 0 0 0 | | 0 1 1 0 1 |
显见,用邻接矩阵表示的无向图是一个对称的矩阵。
图的类型定义:
Cconst n=< 图的顶点上限 > Type adj=0..1; adjm=array[1..n,1..n]OF adj;{邻接矩阵} graph=RECORD V:array[1..n]OF Vtype;{顶点元素} E:adjm; END;
2、邻接表(链表)
邻接表是图的一种链式存储结构。在邻接表中,对图的每一个顶点建立一个单链表,第i个 单链表中的结点表示依附于顶点Vi的边。每个结点由2个域组成:
顶点域——指示与顶点Vi邻接的点的序号;
链域——指向依附于顶点Vi的下一条边所对应的结点。
顶点v[i] | 相邻顶点d[i,j] | 邻接顶点数c[i] | ||||
1 | —> | 2 -> | 4 -> | 5 | 3 | |
2 | —> | 1 -> | 3 -> | 5 | 3 | |
3 | —> | 2 -> | 4 -> | 2 | ||
4 | —> | 1 -> | 3 -> | 5 | 3 | |
5 | —> | 1 -> | 2 -> | 4 | 3 |
TYPE
arcptr=^arcnode;
arcnode=RECORD
adjvex:1..num; {邻接点域}
next:atrcptr; {链域}
END;
vexnode=RECORD
vertex:vertype; {顶点信息}
firstatc:arcptr
END
adj1=ARRAY[1..num] of vexnode;
3、邻接多重表(关联矩阵)
图的应用之一:一笔画问题
图的典型应用是一笔画问题,其他应用将在图论算法中涉及。
[例题] 编程找出图1-2(1)的一笔画路线。
解:(1)首先用邻接矩阵来表示图:如果i,j两点间有线段连接则值为1,否则为0。
(2)计算每个点的度数存dgr[i]中。
(3)统计奇点的个数,即dgr中有几个奇数,存odt中。
(4)如果odt>2则无解,否则从一个奇点开始搜索路线。
算法如下
![](http://qfzx.nhedu.net/IT/UploadFiles/20031026171742580.gif)
Program postal_route;
const n = 6;
links:array[1..n, 1..n] OF Integer=
((0,1,0,0,1,1), (1,0,1,1,0,1),
(0,1,0,1,0,0), (O,1,1,0,1,1),
(1,0,0,1,0,1), (1,1,0,1,1,0));
var
dgr: array[1..n] of integer;
i, j, r, sum, odt, start, nowd: integer;
Procedure find_degree;
Begin
sum:=0; odt:=0; start:=1;
For i:= 1 to n Do
Begin
dgr[i]:= 0;
For j:= 1 to n do dgr[i]:= dgr[i] + links[i,j];
sum:= sum + dgr[i];
If odd(dgr[i]) Then
Begin odt := odt + 1; start:= i End
End;
End;
{ main code .... }
BEGIN
Find_degree
if odt>2 Then
Begin WRiteln('no sulution. ');exit End;
Nowd:= start;
write(start);
repeat
r:=0;
repeat
r:=r+1;
until (links[nowd,r]>0) and ((dgr[r]>1) or ((dgr[r]=1) and (sum=2)));
links[nowd,r]:=0; links[r,nowd]:=0; sum:=sum-2;
dec(dgr[nowd]); dec(dgr[r]);
nowd :=r;
write('->',r);
until sum = 0;
writeln; readln
END.
运行结果为:
5->1->2->3->->4->2->6->4->5->6->1
思考练习:试编程找出图1-2中第(2)和第(3)图一笔画的路线。
§1 深度优先搜索 DFS
我们在对一些问题进行求解时,会发现有些问题很难找到规律,或者根本无规律可寻。对于这样的问题,可以利用计算机运算速度快的特点,先搜索查找所有可能出现的情况,再根据题目条件从所有可能的情况中,删除那些不符合条件的解。
【例题1】 有A、B、C、D、E 5本书,要分给张、王、刘、赵、钱5位同学,每人只能选1本。每个人都将自己喜爱的书填写在下表中。请你设计一个程序,打印出让每个人都满意的所有分书方案。
┌──┬───┬───┬───┬───┬───┐ ││A │ B │ C │ D │ E │ ├──┼───┼───┼───┼───┼───┤ │张│││√│√││ ├──┼───┼───┼───┼───┼───┤ │王│√│√│││√│ ├──┼───┼───┼───┼───┼───┤ │刘││√│√│││ ├──┼───┼───┼───┼───┼───┤ │赵││││√││ ├──┼───┼───┼───┼───┼───┤ │钱││√│││√│ └──┴───┴───┴───┴───┴───┘
★问题分析
题目中每人喜爱哪本书是随意的,无规律可循,所以用穷举方法解较为合适。按穷举法的一般算法,可以暂不考虑一些条件,先求出满足部分条件的解,即可行解。然后,再加上尚未考虑的条件,从可行解中删除不符合这些条件的解,留下的就是问题的解。具体到本题中,我们可以先不考虑“让每人都满意”这个条件,这样,就只剩“每人选一本且只能选一本”这一个条件了。在这个条件下,可行解是5本书的所有全排列,一共有5!=120种情况。从这120种可行解中删去不符合“每人都满意”这一条件的解,剩下的就是本题的解。
为编程方便,我们用1、2、3、4、5分别表示这5本书。这5个数字的—种全排列就是5本书的一种分法。例如54321就表示第五本书(即E)分给张,第四本书(即D)分给王……,第—本书(即A)分给钱。
每个人“喜爱书表”,在程序中我们用二维数组Like来表示,1表示喜爱,0表示不喜爱。排列的产生可以用穷举法,也可以用专门算法。
★算法设计:
第一步:产生5个数字的一个全排列;
第二步:检查所产生的全排列是否符合“喜爱书表”,如果符合就输出;
第三步:检查是否所有排列都产生了,如果没有产生完,则返回第一步;
第四步:结束。
根据题目给出的条件,还可以对上面算法进行一些改进。例如产生一个全排列12345时,第一个数1表示将第一本书给小张。但从表中可以看出,这是不可能的,因为小张只喜欢第三、第四本书。也就是说,1X X X X这一类分法是不符合条件的。由此使我们想到,如果选定第一本书后,就立即检查一下是否符合条件,当发现第一个数的选择不符合条件时,就不必再产生后面的4个数了,这样做可以减少很多的运算量。换句话说,第一个数只在3和4中选择,这样就可以减少3/5的运算量。同理,在选定了第一个数后,其他4个数字的选择也可以用类似的方法处理,即选择第二个数后,立即检查是否符合条件。例如,第一个数选3,第二个数选4后,立即进行检查,发现不符合条件,就另选第二个数。这样就又把34XXX一类的分法删去了,从而又减少了一部分运算量。
综上所述,改进后本题算法应该是:在产生各种排列时,每增加一个数字,就检查一下该数的加入是否符合条件,如不符合,就立刻换一个;若符合条件,则再产生下一个数。因为从第i本书到第i+1本书的寻找过程是相同的,所以可以用递归方法编程。
★算法框图
PROCEDURE TRY(i);(递归算法) ┌─────────────────────┐ │For j:= 1 to 5 do │ ├─┬───────────────────┤ ││T\第I个学生喜爱第j本书/F│ │├────────────┬──────┤ ││记录第 i个数││ │├────────────┤│ ││\i= 5/││ ││T\/ F ││ │├─────┬──────┤│ ││打印一个解│Try(i+1)││ │├─────┴──────┤│ ││删去第i 个数字││ └─┴────────────┴──────┘
我们用二维数组like存放“喜爱书表”,用集合flag存放已分出书的编号,数组book存储各人所分得书的编号,如book[1]=3,则表示第一个同学(小张)分得编号为3的书。
递归程序如下(程序中将小张的喜欢的书改成了ACD):
Program allot_book(output); type five=1..5; const like: array[five,five] of 0..1 =((1, 0, 1,1 ,0), (1,1,0,0,1),(0,1,1,0,0),(0,0,0,1,0),(0,1,0,0,1)); {个人对各种书的喜好情况} name:array[five] of string[5] = ('zhang', 'wang','liu', 'zhao', 'qian' ); {数组name存放学生姓名} var book: array[1..5] of 0..5;{存放各人分配到的书的编号} flag: set of five; c: integer; procedure print; {打印分配方案} var i: integer; begin inc(c); {计数,统计得到分配方案数} writeln( 'answer', c,':'); for i:=1 to 5 do writeln(name[i]: 10,':', chr(64 + book[i] ) ); end; procedure try(i: integer); {判断第 I 个学生分得书的编号} var j: integer; begin for j:=1 to 5 do if not(j in flag) and (like[i,j]>0) then begin {第j本书未选过,且第I个学生喜爱第j本书} flag:= flag + [j]; {修改已选书编号集合,加入第j本书} book[i]:=j; {记录第 I 个学生分得书的编号} if i= 5 then print {I = 5,5 个学生都分到自己喜爱的书} else try(i + 1); {i<5,继续搜索下一个学生可能分到书的情况} flag:= flag - [j]; {后退一步,以便查找下一种分配方案} book[i]:=0; end end; { main prg } begin flag:= []; c:=0; try(1); readln end.
运行结果为:
zhang: C
wang: A
liu:B
Zhao: D
qian: E
另外,此题也可以用非递归的算法解。非递归算法的基本思想是用栈来存放被选中书的编号。设dep表示搜索深度,r为待选书号,p为搜索成功标志。算法表示如下(非递归算法)。
PROCEDURE dfs;(非递归算法) ┌────────────────────────────┐ │Dep:=0│ ├─┬──────────────────────────┤ ││dep:=dep+1│ │├──────────────────────────┤ ││j:=0; p:=False;│ │├─┬────────────────────────┤ │││j:=j+1│ ││├────────────────────────┤ │││T\子结点mr符合统计/F│ ││├──────────────┬─────────┤ │││产生子结点,并记录│T\Mxar/F│ ││├──────────────┼────┬────┤ │││T\子结点是目标/F│回溯│P:=Fatse│ ││├──────┬───────┤││ │││输出并出栈│P:= true│││ │├─┴──────┴───────┴────┴────┤ ││UNTIL p=True│ ├─┴──────────────────────────┤ │UNTIL dep= 0│ └────────────────────────────┘
尽管深度优先基本算法类似,但在处理不同问题时,在具体处理方法、编程的技巧上,却不尽相同; 有时甚至会有很大的差别。
比如,例1的解法还可以这样来设计:从表中看出,赵同学只喜爱D这一本书,无其它选择余地。因此,赵同学得到书的编号在搜索前就确定下来了。为了编程方便,可以把赵钱2人位置交换,这样程序只需对张王刘钱4人情况进行搜索测试。
另外,发现表示“喜爱书表”的数组有多个0,为减少不必要的试探,我们改用链表来表示。例如第三位同学的链表是:Like[3,0]=2.Like[3,2]=3.Like[3,3]=0,其中,Like[3,0]=2 表示他喜爱的第一本书编号是2,Like[3,2]=3即表示喜爱的编号为2的书后面是编号为3的书,Like[3,3]=0, 表示编号为3的书是其最后1本喜爱的书。
这样基本算法不变,但程序改进如下:
Program allot_book(output); {linking List} type five=1..5;{将小张的喜欢的书改成了ACD} const Link: Array[ 1..5,0..5 ] of 0..5 = ((1,3,0,4,0,0),(1,2,5,0,0,0),(2,0,3,0,0,0),(4,0,0,0,0,0),(2,0,5,0,0,0)); {个人对各种书的喜好情况} name:array[five] of string[5] = ('zhang', 'wang','liu', 'zhao', 'qian' ); {数组name存放学生姓名} var book: array[1..5] of 0..5;{存放各人分配到的书的编号} flag: set of five; c: integer; procedure print; {打印分配方案} var i: integer; begin inc(c); {计数,统计得到分配方案数} writeln( 'answer', c,':'); for i:=1 to 5 do writeln(name[i]: 10,':', chr(64 + book[i] ) ); end; procedure try(i: integer); {判断第 I 个学生分得书的编号} var j: integer; begin j:=0; repeat j:=link[i,j]; { 取链表中喜爱书编号j } If not(j in flag) and (j>0) then Begin flag:= flag+ [j]; book[i]:=j; if i=5 then print else try(i + 1); flag:= flag - [j]; {后退一步,以便查找下一种分配方案} book[i]:=0; End; until j = 0; end; { main prg } begin flag:= []; c:=0; try(1); readln end.
§1-2 深度优先搜索实例1
【例题2】 电子老鼠闯迷宫。图中有阴影的部分表示墙,无阴影的部分表示通路。老鼠在迷宫中可以沿上下左右
4个方向摸索前进。
①编一程序,由计算机自动找到一条从上面入口处到下面出口处的一条通道。②让计算机记住路径,当第二次走
时,应能按路径较快从入口到达出口。
★问题分析:
(1)迷宫的表示方法:迷宫一般可以用一个二维数组A(Y,X)来表示,其中,Y表示行,X表示列。
数组中的元素由数字0和1组成。它们的含义是:
↗ 1 墙 A(Y,X)=< ↘ 0 路 设迷宫入口处的坐标为:(1,8),迷宫出口处的坐标为:(10,7)。 (2)搜索方向的识别:对于迷宫中的任意一点A(Y,X),有4个搜索方向: A(Y-1,X) ↑ A(Y,X-1) ← A(Y,X) → A(Y,X+1) ↓ A(Y+1,X)
(3)搜索方向的表示方法:我们设置一组坐标增量来描述这上下左右4个搜索方向
①(0,1)表示向右:(X增1)
②(-1,0)表示向上:(Y减1)
③(0,-1)表示向左:(X减1)
④(1,0)表示向下:(Y增1)
在程序中表示为,X方向增量为DX(I),Y方向增量为DY(I),I的取值为1,2,3,4,表示4个搜索方向。则老鼠
向某个方向试探一步后的新坐标应表示为:
NX=X+DX(I) NY=NY+DY(I)
(4)向某个方向搜索方法:从上面的分析中看到,向某方向搜索前进一步的处理,只需在当前位置坐标,加上
搜索方向的坐标增量DX(i)和DY(i)。 但修改位置坐标后一定要注意:任何一点的坐标加上要搜索方向的坐标增量
之后,都要判断一下是否已超出了迷宫的边界。凡是X<0,或 x>10,或Y<0,或Y>10的情况,均表示新的位置坐标
已超出了迷宫的边界。这时,就应放弃向该方向搜索移动,改为向下一个方向探索前进。
另外,在不超出迷宫边界的情况下,如果A(Y,X)=0,则表示该方向有通路,可以继续向前走,若A(Y,X)=1,
则表示该方向是墙,不能前行。
(5)为了防止老鼠在迷宫中误入歧途并在原地打转,当老鼠从某条死胡同中退回时,应把该路堵死。要实现这
一点很容易,只需使A(Y,X)=2,将已走过的路的标志设为死路标志即可。
(6)为了能记住走出迷宫的路径,在搜索过程中还要建立一个堆栈,将老鼠走过的每一步路都记录下来,当老
鼠碰壁后退时, 就将退出的路从堆栈中弹出去。这样,最终堆栈中保存的就是走出迷宫的一条通路了。这时栈底
元素为迷宫的入口处坐标,栈顶元素是迷宫的出口处坐标。
产生式系统:
(1)数据库。要存储老鼠在迷宫中走过的路,那么每个记录需要有:行、列坐标,搜索方向3个数据;根据以上
分析,数据库应组成堆栈形式,用数组path表示,并用变量DEP作为栈顶指针,同时表示搜索的深度。
(2)产生规则。有8条,若用数组DX和DY表示各方向增量:
nx=x+dx(j);ny=y+dy(j)
if (ny,nx)是通路then (ny,nx)是新结点
(3)搜索策略。采用深度优先搜索法,即上节中总结的算法进行搜索。由于在迷宫探索路程较长,搜索深度较大,
用递归可能产生溢出,所以用非递归的深度优先算法。
程序如下:
program labyrinth(output); uses crt; type node=record {定义存放行走路径的记录类型} xx,yy:byte; {位置坐标} r:byte {搜索方向} end; a11=array[0..11,0..11] of byte; const dx:array[1..4] of integer=(0,1,0,-1);{X方向增量} dy:array[1..4] of integer=(1,0,-1,0);{Y方向增量} a:a11= ((1,1,1,1,1,1,1,1,1,1,1,1),(1,0,0,0,0,0,0,1,0,1,1,1), (1,0,1,0,1,1,0,0,0,0,0,1),(1,0,1,0,1,1,0,1,1,1,0,1), (1,0,1,0,0,0,0,0,1,0,0,1),(1,0,1,1,1,1,1,1,1,1,1,1), (1,0,0,0,1,0,1,0,0,0,0,1),(1,0,1,1,1,0,0,0,1,1,1,1), (1,O,O,O,O,O,1,O,O,O,O,1),(1,1,1,O,1,1,1,1,0,1,O,1), (1,1,1,1,1,1,1,1,0,1,1,1),(1,1,1,1,1,1,1,1,1,1,1,1)); {迷宫布局,为了编程方便,在迷宫的外面四周加了死路标记} varb:all; dep,j,k,x,y,xo,yo,nx,ny:integer; path:array[0..300] of node; p:boolean; procedure wait; begin for k:=1 to 5000 do end; procedure display(a:a11); {打印迷宫布局} var i,j:byte; begin fOr i:=0 to 11 do begin for j:=0 to 11 do write(a[i,j]); writeln end end; function check:bcxolean;{检查向某方向的探索前进是否成功} begin nx:=x+dx[j]; {修改X方向坐标} ny:=y+dy[j]; {修改Y方向坐标} if (nx<1) or (nx>11) or (ny<1) or (ny>11) {超出迷宫边界} then check:=false {check为假,表示此方向搜索前进失败} else if a[ny,nx]>0 then check:=false {前方为墙或死路} else check:=true; {此搜索方向可行} end; procedure backtrack; {回溯,返回上一步} begin repeat dec(dep); {返回上一步,将搜索深度减1 } if dep = 0 then t7: = true else hegin a[y,x]: =2; {置死路标记} gotoxy(x+1,y+1) ;write(chr(176)); {打印后退标记“圈”} wait; x:= path[dep].xx; {栈顶元素出栈1} y:= path[dep].yy; j:= path[dep].r end; until (dep=O) or (j<4); end; procedure print(dep: integer); var i, k: integer; ch: char; begin gotoxy(nx + 1, ny + 1) ;write(chr(219) ); {打印行走成功标记“曰”} gotoxy(1,15); wri re( 'see you again(y/n) :'); readln(ch); if (ch='y') or (ch='Y') then {重走迷宫时处理} begin clrscr ;display(b); {打印迷宫初始布局} for i: = 1 to dep do with path[i] do begin gotoxy(xx + 1, yy + 1) ;write(chr(219) ); {根据数组Path中记录的行走路线,走出迷宫路线} wait; end; gotoxy(1,20); write( 'end !' ); readln end; halt end; { ...... main prg .......... } begin y0: = 10; x0: =7; {迷宫出口坐标} b:=a; {保存迷宫初始布局} clrser; display(a); dep: = 1; with path[1] do {初始化,设置迷宫入口情况} begin xx: =8;yy: = 1 end; repeat with path[dep] do begin x: =xx;y: =yy {取栈顶元素} end; gotoxy( x + 1, y + 1 ); write(chr(219) ); wait; j: =0;p:=false; repeat inc(j); { 搜索前进方向} if check then {某方向搜索前进成功 } begin a[y,x]:= 1; {将来路设为死路,避免老鼠在迷,宫中原地打转} path[dep], r: =j; {记录前进方向} inc(dep); with path[dep] do begin xx:= nx;yy:=ny; { 改坐标,前进一步} end; if (nx=x0) and (ny= y0) then print(dep) {达迷宫出口} else p: = true; end else if j >= 4 then backtrack; until p = true; until dep=0; readln end.
想一想:上面题目中规定,老鼠在迷宫中的行走方向为上下左右4个方向,如果将老鼠在迷宫中可摸索前
进的方向扩充为8个,即除了可以沿上下左右4个方向摸索前进外,还可以沿左上、右上、左下、右下4个方向
摸索前进,上述程序应如何修改,请同学们自己试一试。
§1-2 深度优先搜索实例2
【例题3】 骑士周游世界。在国际象棋的棋盘上,有一位骑士按照国际象棋中马的行走规则从棋盘上的某一
方格出发,开始在棋盘上周游。若能不重复地走遍棋盘上的每一个方格,这样的一条周游路线在数学上被称
之为国际象棋盘上马的哈密尔顿链。请你设计一个程序,对于从键盘输入的任意一个起始方格的坐标,由计
算机自动寻找并按如下格式打印出国际象棋盘上马的哈密尔顿链。例如,当马从坐标点(5,8)出发时,相应
的哈密尔顿链如下图所示:
┌───┬───┬───┬───┬───┬───┬───┬───┐ │60│11│26│29│54│13│24│21│ ├───┼───┼───┼───┼───┼───┼───┼───┤ │27│30│61│12│25│22│51│14│ ├───┼───┼───┼───┼───┼───┼───┼───┤ │10│59│28│55│50│53│20│23│ ├───┼───┼───┼───┼───┼───┼───┼───┤ │31│64│57│62│43│48│15│52│ ├───┼───┼───┼───┼───┼───┼───┼───┤ │58│9│32│49│56│19│42│1│ ├───┼───┼───┼───┼───┼───┼───┼───┤ │33│6│63│44│47│36│39│16│ ├───┼───┼───┼───┼───┼───┼───┼───┤ │8│45│4│35│18│41│2│37│ ├───┼───┼───┼───┼───┼───┼───┼───┤ │5│34│7│46│3│38│17│40│ └───┴───┴───┴───┴───┴───┴───┴───┘
★问题分析:
(1)棋盘的表示方法:我们用一个8X8的二维数组A(X,Y)来表示国际象棋的棋盘。在骑士没有开始周游棋
盘时,棋盘上所有的格中数值均置为零。以后,骑士周游到哪一个格,就将其周游走过的步数记录在相应的
空格中。
(2)棋盘上马的跳跃方向:如下图所示,在国际象棋盘上,一匹马共有8个可能的跳跃方向。
┌─┬─┬─┬─┬─┐ ││⑧││①││ ├─┼─┼─┼─┼─┤ │⑦││││②│ ├─┼─┼─┼─┼─┤ │││马│││ ├─┼─┼─┼─┼─┤ │⑥││││③│ ├─┼─┼─┼─┼─┤ ││⑤││④││ └─┴─┴─┴─┴─┘
我们设置一组坐标增量来描述这8个跳跃方向:
①(1,2) ②(2,1)
③(2,-1) ④(1,-2)
⑤(-1,-2) ⑥(-2,-1)
⑦(-2,1) ⑧(-1,2)
(3)马的跳跃方向的表示方法:设I表示行,J表示列,每次跳跃后,行增量为DI(r),列增量为DJ(r),
其中r=1,2,3,4,5,6,7,8表示马的8个跳跃方向。即马向某个方向试探跳跃一步后新的坐标表示为:
NI=I+DI(r) NJ=J+DJ(r)
(4)判断马朝某个方向试探性的跳跃一步是否成功:如上所述,马跳跃后新位置的坐标表示为原位置坐
标加上跳跃方向的坐标增量。但每得到一个新位置坐标后,都要判断一下是否已超出了棋盘的边界。 对于
例2中讨论的问题,若 I<0,或I>8,或j<0,或j>8时,都表示已超出了棋盘的边界,这时,应放弃向该方
向的跳跃,转而对下一个方向试探。
在跳跃不出界的情况下,还要判断此棋盘格是否已经走过,如果A(NI,NJ)=0,表示此格未走过,该方
向搜索前进成功,可以继续向前跳跃。若A(NI,NJ)>0,则表示该格已经走过了,不能再走。放弃该方向并
转向下一个方向进行试探。
思考练习:本题程序请同学们自己写出。
§1-3 深度优先搜索习题
【习题1】 设有一个4×4的棋盘(即每行每列有4个正方形格),用4格棋子布在格子中,要求满足以下条件:
A)任意2个棋子不在同一行和同一列上;
B)任意2个棋子不在同一对角线上;
试问有多少种棋局?编程把他们找到并打印出来。
【习题2】覆盖问题。 有边长为N(N为偶数)的正方形,请你用N 2/2个长为2宽为1的长方形,将它全部覆盖。
编程打印出所有覆盖方法。下面是当N=4时几种覆盖方法及打印格式:
【习题3】液晶数字
§2 广度优先搜索 BFS
在深度优先搜索算法中,是深度越大的结点越先得到扩展。如果在搜索中把算法改为按结点的层次进行
搜索, 本层的结点没有搜索处理完时,不能对下层结点进行处理,即深度越小的结点越先得到扩展, 也就
是说先产生 的结点先得以扩展处理,这种搜索算法称为广度优先搜索法。英语中用 Breadth-First-Search
表示,所以我们 也把广度优先搜索法简称为BFS。1、广度优先搜索的基本思想
从图中某一顶点Vo出发,首先访问Vo相邻的所有未被访问过的顶点V1、V2、……Vt;再依次访问与V1、
V2、……Vt相邻的且未被访问过的所有顶点。如此继续,直到访问完图中所有的顶点。
如果用广度优先法对下图中结点进行搜索,从结点V1出发,先搜索处理 它的子结点V2和V3,即深度为
2的结点;然后搜索深度为3的子结点V4、V5、V6、V7;最后搜索深度为 4 的结点V8和V9。整个搜索的次序
与结点产生的次序完全一致。深度 __V1__ 1 / / V2 V3 2 / / / / V4 V5 V6 V7 3 / / V8 V9 42.广度优先搜索基本算法:
1)从某个顶点出发开始访问,被访问的顶点作相应的标记,并输出访问顶点号;
2)从被访问的顶点出发,依次搜索与该顶点有边的关联的所有未被访问的邻接点,并作相应的标记。
3)再依次根据2)中所有被访问的邻接点,访问与这些邻接点相关的所有未被访问的邻接点,直到所
有顶点被访问为止。
【算法过程】procedure guangdu(i); begin write(i); v[i]:=true; insert(q,i);{q是队列,i进队} repeat k:=delete(q);{出队} for j:=1 to n do if (a[k,j]=1) and (not v[j]) then begin write(j); v[j]:=true; insert(q,j); end; until 队列q为空;【实际应用】:实际应用的算法流程图通常如下:
【问题描述】如下图,找出C1到C6的一条最短路径并求出其路程总长度(采用广度优先搜索的顶点访问
序列为C1,C2,C3,C4,C5,C6)。
【Pascal程序】
program tu3bfs;
type fg=set of 1..6;
const link:array[1..5,1..6] of integer=((0,4,8,0,0,0),
(4,0,3,4,6,0),(8,3,0,2,2,0),(0,4,2,0,4,9),(0,6,2,4,0,4));
var pnt,city:array[1..10] of 0..6;
flag:fg;
r,k,head,tail:integer;
procedure print;
var n, i,cost,y:integer;
s:array[1..7] of 1..6;
begin
y:=tail;n:=0; cost:=0;
while y>0 do begin inc(n);s[n]:=y;y:=pnt[y] end;
writeln('minpath=',n-1);
write('1');
for i:=n-1 downto 1 do
begin
write('->',s[i]);
cost:=cost+link[s[i+1],s[i]];
end;
writeln;
writeln('cost=',cost);
end;
begin
flag:=[1];
pnt[1]:=0; city[1]:=1;
head:=0;tail:=1;
repeat
head:=head+1;
k:=city[head];
for r:=2 to 6 do
if not(r in flag) and (link[k,r]>0) then
begin
inc(tail);city[tail]:=r;
pnt[tail]:=head;
flag:=flag+[r];
if r=6 then begin print;halt end;
end;
until head>=tail;
readln;
end.§2-2 广度优先搜索实例
【例题】八数码难题(Eight-puzzle)。在3X3的棋盘上,摆有 8个棋子,在每个棋子上标有1~8中的某一数字。
棋盘中留有一个空格。空格周围的棋子可以移到空格中。要求解的问题是,给出一种初始布局(初始状态)和目
标布局(目标状态),找到一种最少步骤的移动方法,实现从初始布局到目标布局的转变。初始状态和目标状态
如下:初始状态 目标状态 2 8 3 1 2 3 1 6 4 8 4 7 5 7 6 5求解本题我们可以分3步进行。
问题分析:
由于题目要找的解是达到目标的最少步骤,因此可以这样来设计解题的方法:
初始状态为搜索的出发点,把移动一步后的布局全部找到,检查是否有达到目标的布局,如果没有,再从
这些移动一步的布局出发,找出移动两步后的所有布局,再判断是否有达到目标的。依此类推,一直到某布局
为目标状态为止,输出结果。由于是按移动步数从少到多产生新布局的,所以找到的第一个目标一定是移动步
数最少的一个,也就是最优解。
建立产生式系统:
(1)综合数据库。用3X3的二维数组来表示棋盘的布局比较直观。我们用Ch[i,j]表示第i行第j列格子上放
的棋子数字,空格则用0来表示。为了编程方便,还需存储下面3个数据:该布局的空格位置(Si,Sj);初始布
局到该布局的步数,即深度dep;以及该布局的上一布局,即父结点的位置(pnt)。这样数据库每一个元素应该
是由上述几个数据组成的记录。
在程序中,定义组成数据库元素的记录型为:
Type
node=record
ch:array[1..3,1..3] of byte;{存放某种棋盘布局}
si,sj:byte; {记录此布局中空格位置}
dep,pnt:byte;
end;
因为新产生的结点深度(从初始布局到该结点的步数)一般要比数据库中原有的结点深度大(或相等)。按广度
优先搜索的算法,深度大(步数多)的结点后扩展,所以新产生的结点应放在数据库的后面。而当前扩展的结点从
数据库前面选取,即处理时是按结点产生的先后次序进行扩展。这样广度优先搜索的数据库结构采用队列的结构
形式较合适。我们用记录数组data来表示数据库,并设置两个指针:Head为队列的首指针,tail为队列的尾指针。
(2)产生规则。原规则规定空格周围的棋子可以向空格移动。但如果换一种角度观察,也可看作空格向上、
下、左、右4个位置移动,这样处理更便于编程。设空格位置在(Si,sj),则有4条规则:
①空格向上移动: if si-1>=1 then ch[si,sj]:=ch[si-1,sj];ch[si-1,sj]:=0
②空格向下移动: if si+1<=3 then [si,sj]:=ch[si+1,sj];ch[si+1,sj]:=0
③空格向左移动: if sj-1<=1 then [si,sj]:=ch[si,sj-1];ch[si,sj-1]:=0
④空格向右移动: if sj+1<=3 then [si,sj]:=ch[si,sj+1];ch[si,sj+1]:=0
我们用数组Di和Dj来表示移动时行列的增量,移动后新空格的位置可表示为:
nx:=si+di(r)
ny:=sj+dj(r)
其中,r=1,2,3,4为空格移动方向,且r 1 2 3 4 方向 左 上 右 下 di 0 -1 0 1 dj -1 0 1 0(3)搜索策略。按照问题分析中提出的方法,算法设计如下:
program num8;
程序中新布局与队列中已有布局是否重复,用dup函数检查;找到目标结点后,print过程负责打印出从初始态到目标态
移动时各步的布局,buf[n)是用来存放待输出的布局在队列中的位置。
procedure print;
根据上述算法编制的程序如下:
program num8_str1;
uses Crt;
type a33:array[1..3,1..3] Of byte;
{3X3的二维数组,用于存放棋盘布局}
a4=array[1..4] of shortint;
node=record {定义数据库中每个元素记录类型结构}
ch: a33;
si, sj: byte;
pnt, dep: byte;
end;
const goal:a33 = ((1,2,3), (8,0,4), (7,6,5)); {目标布局}
start:a33 =((2,8,3), (1,6,4), (7,0,5)); {初始布局}
di:a4=(0,-1, 0, 1);
dj:a4=(-1, 0, 1, 0);
var data:array[1..100] of node;
temp: node;
r, k, ni, nj, Head, Tail, depth: integer;
{变量depth存放当前搜索深度}
function check(k: integer) :boolean; { 检查某步移动是否可行}
begin
hi:=temp.si+di[k] ; nj:=temp.sj+dj[k];
if (ni in [1..3]) and (nj in [1..3]) {~移动后新位置仍在棋盘中}
then check:=true else check:= false;
end;
function dupe: boolean; { 检查队尾新存入布局是否已在队列中存在}
var i,j, k: integer;
buf:boolean;
Begin
buf:= false; i: = 0;
{变量将i依次指向队列中的各个布局(最后一个除外)的位置}
repeat
inc(i) ;buf:= true;
for j:=1 to 3 do
for k:=1 to 3 do
if data[i].ch[j,k] < >data[tail].ch[j,k]
{data[tail]是队列中最后一个元素,即新产生的布局}
then bur:= false;
until buf or (i> = tail-1);
{buf=truee新布局与队列中布局有重复}
dupe:= buf
end;
function goals: boolean; { 比较是否达到目标布局状态}
var i,j :byte;
begin
goals:= true;
for i:=1 to 3 do
for j:=1 to 3 do
if data[tail].ch[i,j] < >goa1[i,j]
then goals:=false {未达到目标布局}
end;
procedure trace;
var i,j :byte;
begin
write( 'cl=', Head,' op=', tail);
writeln('dep=',depth,'k=',k);
fori:=1 to 3 do
begin
for j:= 1 to 3 do write(data[tail], ch[i,j]);
writeln end;
end;
procedure print; {输出移动步骤}
var buf: array[1..100] of integer;
{数组buf存放起始态、目标态以及从起始态到目标态所经过的各态的位置}
i,j, k, n: integer;
begin
n:= 1;
i:= tail;buf[1]:= i; {buf[1]中是目标布局在队列中位置}
repeat
j:=data[i].pnt; {data[I].pnt的值是布局I的父结点的位置}
inc(n); buf[n]:=j; i:=j
until i=0; {根结点(初态)的父结点为0,即I=0}
writeln(' staps:', depth + 1);
for i:= 1 to 3 do {打印棋盘布局}
begin
for k:=n-1 down to 1 do
begin
for j:= 1 to 3 do write(data[buf[k]].ch[i,j]);
if i = 2 then write( ' - > ') else write(' ');
end;
writeln;
end;
readln; halt
end;
{ main program = }
begin
Head:= 0; tail:= 1;
with data[1] do {队列中存入第一个元素(初始状态)}
begin ch:= start; si:= 3; sj:= 2;
pnt:= 0; dep:= 0;
end;
repeat
inc(Head);temp:=data[Head]; {取队首记录}
depth:= temp.dep;
for r:= 1 to 4 do {对取出记录进行扩展}
if check(r) then {布局中空格向某方向移动成功}
begin
inc(tail);data[tail]:= temp; {新产生布局存入队尾}
with data[tail] do
begin ch[si,si]:= ch[nj,nj];
ch[ni,nj]:=0;si:=nj;si:=nj;
pnt:=Head;{记录此布局的上一布局在队列中的位置}
dep:= depth + 1;{记录本布局的搜索深度}
end;
trace;
if dupe then dec(tail) {dec(tail删除新产生的结点)}
else if goals then print;
end;
until Head>=tail; {队列空}
writeln('no solution');readln
end
运行结果:
283 283 283 023 123 123
164—>104—>184—>184—>084—>804
705 765 765 765 765 765
上述程序产生的搜索各个布局图略。
从上面搜索图中可看出,程序执行时先产生深度为1的所有结点,然后再产生深度为2的所有结点……,最后
产生含有目标的深度为5的结点结束。先往横向扩展,再往纵向深入,这就是广度优先搜索法搜索过程。
从上例我们看出,广度优先搜索法可以求出步数最少的解,即深度最少的解。因此广度优先搜索法经常用于
一些求最优解的问题中。
与深度优先搜索法类似,不同的问题用广度优先搜索法的基本算法都是一样的,但在数据库的表示方法上、
在产生的结点是否符合条件上和重复的判断上可以有不同的编程技巧,程序运行效率也会有所不同。以八数码问
题为例,上面程序中用3X3的二维数组表示布局比较直观,但在判断有重复布局,判断是否达到目标布局方面,
却增加了编程复杂性,同时也影响了运行速度。我们可以改用字符串形式来表示布局。 例如初始布局表示为
"283164705'’,目标布局表示为“123804765”,即按行的顺序排列。
产生规则也必须作相应改动。设空格当前位置是Si,则有:
(1)空格向上移动:空格的位置减3,即交换Si和Si-3的字符;
(2)空格向左移动:空格的位置减1,即交换Si和Si-1的字符;
(3)空格向右移动:空格的位置加1,即交换Si和Si+1的字符;
(4)空格向下移动:空格的位置加3,即交换Si和Si+3的字符;
如设规则编号为k,则上述四条规则可归纳为一条:
交换Si和Si+(2*k-5)的字符。
其中,k=1为向上移动;k=2为向左移动;k=3是向右移动;k=4为向下移动。
布局用字符串表示后,使得判断重复和是否目标态变得十分简单,只需判断两个字符串是否相等就可以了。
【思考】试按照上述改进算法,编制出解八数码题的PASCAL程序。§2-3 双向广度优先搜索
广度优先搜索遵循从初始结点开始一层层扩展直到找到目标结点的搜索规则,它只能较好地解决状态不是太多
的情况,承受力很有限。如果扩展结点较多,而目标结点又处在较深层,采用前文叙述的广度搜索解题,搜索
量巨大是可想而知的,往往就会出现内存空间不够用的情况。双向搜索和A算法对广度优先的搜索方式进行了
改良或改造, 加入了一定的“智能因素”,使搜索能尽快接近目标结点,减少了在空间和时间上的复杂度。(1)搜索过程
有些问题按照广度优先搜索法则扩展结点的规则,既适合顺序,也适合逆序,于是我们考虑在寻找目标结点或
路径的搜索过程中,初始结点向目标结点和目标结点向初始结点同时进行扩展—,直至在两个扩展方向上出现
同一个子结点,搜索结束,这就是双向搜索过程。出现的这个同一子结点,我们称为相交点,如果确实存在一
条从初始结点到目标结点的最佳路径,那么按双向搜索进行搜索必然会在某层出现“相交”,即有相交点,初
始结点一相交点一目标结点所形成的一条路径即是所求路径。
例如:移动一个只含字母A和B的字符串中的字母,给定初始状态为(a)表,目标状态为(b)表,给定移动规
则为:只能互相对换相邻字母。请找出一条移动最少步数的办法。 [AABBAA] [BAAAAB] (a) (b)解题分析:从初始状态和目标状态均按照深度优先搜索扩展结点,当达到以下状态时,出现相交点,如图1(a),
结点序号表示结点生成顺序。
双向扩展结点:顺序 逆序 1 1 ___AABBAA___ BAAAAB 2 / / 3 2 / / 3 __ABABAA__ AABABA ABAAAB BAAABA 4 / |5 / 6 7 / / 8 4 / ABBAAA BAABAA ABAABA AAABBA AABAAB AABAAB (a) 图1 (b)顺序扩展的第8个子结点与逆序扩展得到的第4个子结点就是相交点,问题的最佳路径如图2。
[AABBAA]—[AABABA]—[AABAAB]—[ABAAAB]—[BAAAAB]
图2
从搜索的结点来看,双向广度要简单得多。假设每一个子结点可以扩展的子结点数是X,不计约束条件,
以完全X叉树计算,那么用广度优先搜索一个长度为I的最佳路径的解,共需要扩展结点X(XL-1)÷(X-1)。从
双向搜索来看,设正个方向的搜索在第y层找到向交点,那么正向共搜索了 X(XY-1)÷(X-1),逆向扩展的结
点数为(XL-y-1)÷(X-1),两个方向共搜索了 X(XY+XL-Y-2)÷(X-1)。我们假设L为偶数,则Y=L/2,双向搜索
扩展的结点数约为单向搜索的2÷(XL/2+1)*100%,相对减少(XL/2-1)÷(XL/2+1)*100%。
当然这里只是作个粗略的比较,事实上在其它一般情况下,双向搜索搜索量都要比单向搜索来的少。(2)结点扩展顺序
双向扩展结点,在两个方向的扩展顺序上,可以轮流交替进行,但由于大部分的解答树并不是棵完全树,
在扩展完一层后,下一层则选择结点个数较少的那个方向先扩展,可以克服两个方向结点生成速度不平衡的状
态,明显提高搜索效率。(3)数据结构
单向广度优先搜索需建立两个表OPEN和CLOSED,用来存储生成结点和已扩展结点,双向搜索从两个方向进
行扩展,我们建立两个二维表 OPEN,CLOSED,OPEN[1],CLOSED[1], OPEN[2],CLOSED[2]分别存储两个方向
上的生成结点和已扩展结点,OPEN仍然是具有“先进先出”的队列结构。为编程方便,我们采用基于广度优先
搜索算法的双向,建立三个二维指针:Q1,Q2,Q3其作用如下:
Q1[1],Q1[2]:分别指向两个方向上当前待扩展层的第一个结点。
Q2[1],Q2[2]:分别指两个方向上队尾新产生的结点。
Q3[1],Q3[2]:分别指向两个方向上下一层的第一个结点位置。
为了区分当前搜索方向,设方向标志:
t=1表示处于正向搜索,t=2表示处于逆向搜索。
Fail—有一个方向搜索失败时,为真,并且结束搜索过程,否则为假。
I—全局变量,指向当前要扩展的结点。(4)算法描述Program DOUBFS; 初始化,初始结点,和目标结点分别进入OPEN[1]和OPEN[2]表; Q1[1]:=1;Q2[1]:=1;Q1[2]:=1;Q2[2]:=1; repeat if (Q2[1]-Q1[1])<=(Q2[2]-Q1[2]) then t:=1 else t:=2; for I:=Q1[t] to Q2[t] do EXPEND(t);{扩展第1个结点} Q1[t]:=Q3[t]; until(Q1[t]>Q2[t]); 其中过程EXPEND(t)的结构如下: Procedure expand(t:integer); Var j:integer; begin for j:=1 to n do {n为最多后继状态数} begin 产生i点的第j个后继状态,将它加入到队尾(Q2[t]+1); if新结点未与其上一层以上的所有点重复 then if isans(t) then [输出解;halt;] else else将新点从队列中去掉;(Q2[t]-1) end; - end; 判断是否是相交点的过程isans(t)如下: function isans(t:integer):Boolean; var j,t1:integer; begin if t=1 then t1:=2 else t1:=1; isans:=false; forj:=Q1[t1] to Q2[t1] do if Q2[t]=j {Q2[t]新结点是相交点} then [isans:=true;exit]; end;(5)例题应用
【例1】魔方问题
在魔方风靡全球之后,Rubik先生发明了它的简化版——魔板。魔板由8个同样大小的方块组成,每个方块
的颜色均不相同,本题中以数字1—8分别表示,可能出现在魔板的任一位置,任一时刻魔板的状态可以用方块
的颜色序列表示:从魔板的左上角开始,按顺时针方向依次写下各方块的颜色代号,得到的数字序列即可表示
此时魔板的状态。
例如,序列(1,2,3,4,5,6,7,8)表示题中魔板的初始状态。1 2 3 4 8 7 6 5对于魔板,可以施加三种不同的操作,分别以A,B,C标识。
具体操作方法如下:
A:上下行互换,如上图可以变换为状态87654321
B:每行同时循环右移一格,如上图可以变换为41236785
C:中间4个方块顺时针旋转一格,上图可以变换为17245368。
应用这三种基本操作,可以由任一状态达到任意另一状态。
子任务A:
请编一程序,对于输入的一个目标状态,寻找一种操作的序列,使得从初始状态开始,经过此操作序列后
使该魔板变为目标状态。
子任务B:
如果你的程序寻找到的操作序列在300步以内,会得到子任务B的分数。
输入数据:
文件名INPUT.TXT,第一行包含8个以一个空格相隔的正整数,表示目标状态。
输出数据:
输出文件名为OUTPUT.TXT,在第一行输出你的程序寻找到的操作序列的步数L,随后L行是相应的操作序列
,每行的行首写一个字符,代表相应的操作。
【算法分析】
A.空间复杂度 如果解的步数为n,则状态表示空间约占3n。
B.基本算法 本题是典型的广度优先算法题,很自然的想到能否构造启发算法。但本题不同于八数码,很难找到
一个估价函数。因为每一种状态的达到都有三种本质不同的方法,因此在计算某一状态的估价值时,容易将状态
各个数字的最少移动步数重复计算,或忽略计算,不能够构造出一个恰当函数f*,使得f*< f。因此不宜采用启发
算法得出最优解,而只能得可行解。现在考虑双向广度优先搜索。
双向搜索与单向广度搜索相比的优点在于节省了存储空间,避免搜索出更多的无用结点,提高丁搜索速度,如果
采用动态数组存储(655 350Byte)可以做到大约21~22步,甚至可以更多。
【参考程序】Program Rubic; Uses Crt; Const n=8; input = 'input.txt'; Type dar = record f: integer; d: array[1..n] of Integer; End; Var Cab: array[1..2,1..7500] of ^dat; dat1,dat2: dat; Procedure Init; Var f: text; i,i: Integer; Begin assign(f, input); reset(f); new(cab[1,1]); for I:=1 To n do read(f,cab[1,I]^.d[i]); cab[1,1]^.f := 0; readln(f); new(cab[2,1 ] ); for I := 1 tondo read(f,cab[2,1]^.d[i]); readln(f); cab[2,1]^.f := 0; End; Function Check(x,y: Integer) :boolean; Var i,j,k: Integer; ok: Boolean; Begin for i:= 1 to y-1 do Begin forj := 1 to n do if cab[x,i]^.d[j] < > dat1.d[j] then Begin ok := true; Break; End else Ok:= false; if not ok then break; End; Check := ok; End; Function CheckOut(X,Y: Integer;Var a: Integer): Boolean; Var i,j,k: Integer; ok: Boolean; Begin a:=0; fori := 1 to y do Begin for j := 1 to n do if cab[x,i]A.d[j] < > dat1.d[j] then Begin ok := true; Break; End else Ok: = false; if not ok then Begin a:= i; break; End; End; CheckOut := ok; End; Procedure Print(a,b,c: Integer); Var i,j,k,l: Integer; s1,s2: array[1..30] of Integer; x,y: Integer; Begin fillchar(s1,sizeof(s1), 0); fillchar(s2,sizeof(s2) ,0); if a = 1 then Begin i:=1; j:=2; End else Begin i:=2; j:=1; End; k:= O; Repeat inc(k); s1[k] := b; b := cab[i,b]^.f; Until b = 0; l:= 0; Repeat inc(l); s2[l] := c; c := cab[j,c]^.f; Until c = 0; if a = 1 then begin for x := k downto 1 do Begin for y := 1 to n do write(cab[1,s1[x]]^.d[y]: 3); if y mod 4 = 0 then writeln; End; writeln('-----'); Readln; End; for x := 2 to l do Begin for y := 1 to n do Begin write(cab[2,s2[x]]^.d[y]: 3); if y mod 4 = 0 then writdn; End; writeln('-----'); Readln; End; End else Begin for x := l downto 1 do Begin for y := 1 to n do write(cah[1,s2[x]]^.d[y]: 3); if y mod 4 = 0 then writdn; End; writeln('-----'); Readln; End; for x := 2 to k do Begin for y:= 1 to n do begin write(cab[2,s1[x]]^.d[y]: 3); if y mod 4 = 0 then writeln; End; writeln('-----'); Readln; End; End; Halt; End; Procedure Double; Var i,j: array[1..2] of Integer; Out: Boolean; k,l,kk,s: Integer; i[1] := 1; i[2] := 1; j[1] := 2; j[2] := 2; Out := false; repeat kk:=2; k:=1; {--1--} dat1.d := Cab[k,i[k]]^.d; for l := 1 to 4 do Begin dat1.d[l] := dat1.d[l+4]; dat1.d[l+4] := cab[k,i[k]]^.d[l]; End; dat1.f := i[k]; if Check(k,j[k]) then Begin new(mb[kd[k]]); mb[kd[k]]^:= dat1; inc(j[k]); if Not CheckOut(kk,j[kk] - 1,s) then Print(k,j[k] - 1 ,s); End; {--2--} dat1.d := Cab[k,i[k]]^.d; dat1.d[3]: = dat1.d[2]; dat1.d[2] := dat1.d[5]; dat1.d[5] := dat1.d[6]; dat1.d[6] := cab[k,i[k]]^.d[3]; dat1.f: = i[k]; if Check(k,j[k]) then Begin new(cab[k,j[k]]); cab[k,j[k]]^ := dat1; inc(j[k]); if NOt CheckOut(kk,j[kk] - 1,s) then Print(k,j[k] - 1,s); End; {--3--} dat1.d:= Cab[k,i[k] ]^.d; dat1.d[4]: = dat1.d[3]; dat1.d[3]: = dat1.dj2]; dat1.d[2] := dat1.d[1]; dat1.dj1] := cab[k,i[k]]^.d[4]; dat1.f := i[k]; if Check(k,j[k]) then Begin new(cab[k,j[k]]); cab[k,j[k]]^:= dat1; inc(j[k]); if Not CheckOut(kk,j[kk]- 1,s) then Print(k,j[k] - 1,s); End; Inc(i[k]); kk:= 1; k:=2; {--1--} dat1.d := Cab[k,i[k]]^.d; for l := 1 to 4 do Begin dat1.d[l] := dat1.d[l+4]; dat1.d[l+4] := cab[k,i[k]]^.d[l]; End; dat1.f:= i[k]; if Check(k,j[k]) then Begin new(cab[k,j[k] ]); cab[k,j[k]]^:= dat1; inc(j[k]); if Not CheckOut(kk,j[kk] - 1 ,s) then Print(k,j[k] - 1 ,s); End; {--2--} dat1.d := Cab[k,i[k]]^.d; daft. d[2] : = dat1. d[3]; dat1.d[3] := dat1.d[6]; dat1.d[6]: = dat1.d[5]; dat1.d[5] := cab[k,i[k]]^.d[2]; dat1.f: = i[k]; if Check(k,j[k]) then Begin new(cab[k,j[k]]); cab[k,j[k]]^:= dat1; ine(j[k]); if Not CheckOut(kk,j[kk] - 1 ,s) then Print(k,j[k] - 1 ,s); End; {---3---} dad.d:= Cab[k,i[k]]^.d; dat1.d[1] := dat1.d[2]; dat1.d[2] := dat1.d[3]; dat1.d[3] := dat1. d[4]; dat1.dj4] := cab[k,i[k] ]^.d[1]; dat1.f := i[k]; if Check(k,j[k]) then Begin new(cab[k,j[k]]); cab[k,j[k]]^ := dat1; inc(j[k]); if Not CheckOut(kk,j[kk] - 1,s) then Prim(k,j[k] - 1,s); End; Inc(i[k]); until Out; End; Begin INit; clrscr; Double; End.<script src="../../../../lib/footer.js" type="text/javascript"> </script> <script src="../../../../lib/footer.js" type="text/javascript"> </script>§2-4 广度优先搜索练习题
很多问题都可以用广度优先搜索进行处理,如翻币问题(参见归纳策略中的移动棋子问题)、最短路径问题(参见动态规划)等。
1)用字符串的方式‘283164705’-> '123804765'处理8数码难题。
2)电子老鼠闯迷宫。如下图12×12方格图,找出一条自入口(2,9)到出口(11,8)的最短路径。
3)如下图:求图中被*围成的封闭区域的面积(方格的个数不包括*所在的方格)。
* *
*
*
*
*
* * *
*
*
*
*
* * *
*
*
*
* * * * *
*
*
* * *
4)分酒问题:有一酒瓶装有8斤酒,没有量器,只有分别装5斤和3斤的空酒瓶。设计一程序将8斤酒分成两个4斤,并以最少的步骤给出答案。
5)移动棋子游戏:在下列所示的10个格子里,前面两格是空格,后面相间的放着4个A和4个B
A
B
A
B
A
B
A
B
若每次可移动任意两个相邻的棋子进入空格,移动时两棋子不得更动其原来次序目标是将4个A连在一起,空格位置不限。试编程,求出一种
方案并输出每移动一次后得棋子状态。