图的广度优先搜索的应用
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
图的广度优先搜索的应用
◆内容提要
广度优先搜索是分层次搜索,广泛应用于求解问题的最短路径、最少步骤、最优方法等方面。
本讲座就最短路径问题、分酒问题、八数码问题三个典型的范例,从问题分析、算法、数据结构等多方面进行了讨论,从而形成图的广度优先搜索解决问题的模式,通过本讲座的学习,能明白什么样的问题可以采用或转化为图的广度优先搜索来解决。
在讨论过程中,还同时对同一问题进行了深层次的探讨,进一步寻求解决问题的最优方案。
◆知识讲解和实例分析
和深度优先搜索一样,图的广度优先搜索也有广泛的用途。
由于广度优先搜索是分层次搜索的,即先将所有与上一层顶点相邻接的顶点搜索完之后,再继续往下搜索与该层的所有邻接而又没有访问过的顶点。
故此,当某一层的结点出现目标结点时,这时所进行的步骤是最少的。
所以,图的广度优先搜索广泛应用于求解问题的最短路径、最少步骤、最优方法等方面。
本次讲座就几个典型的范例来说明图的广度优先搜索的应用。
先给出图的广度优先搜索法的算法描述:
F:=0;r:=1;L[r]:=初始值;
H:=1;w:=1;bb:=true;
While bb do
begin
H:=h+1;g[h]:=r+1;
For I:=1 to w do
Begin
F:=f+1;
For t:=1 to 操作数do
Begin
⑴m:=L[f]; {出队列};
⑵判断t操作对m结点的相邻结点进行操作;能则设标记bj:=0,并生成新结点;不能,则设标记bj:=1;
if bj:=0 then {表示有新结点生成}
begin
for k:=1 to g[h]-1 do
if L[k]=新结点then {判断新扩展的结点是否以前出现过}
begin
bj:=1;k:=g[h]-1
end;
if bj<>1 then {没有出现过} begin
r:=r+1;L[r]:=新结点;{新结点进队列}
b[r]:=f;c[r]:=t;{并链接指针,保存操作数} end; end; end; end;
w:=r+1-g[h];s:=0;{计算新生成的一层的结点数}
for k:=g[h] to r do {在新生成的一层结点中,判断是否有目标结点存在} if L[k]=目标结点 then begin
s:=s+1; {累计解的条数} 根据链接指针求出路径; end;
if s:<>0 then begin
输出s 条路径;
bb:=false; {设程序结束条件} end; end;
例1:最短路径问题
求从任意一个顶点V i 出发,对给出的图,求到达任意顶点V j (i<>j )的所有最短路径 [问题分析]
1、首先用邻接表表
示此图各端点的邻接关系。
2、数据结构
4 7
8
const
d:array[1..8,1..4] of byte=((2,3,4,0),(1,3,7,0),(1,2,4,5),(1,3,6,0),(3,6,7,8),(4,5,8,0),(2,5,8,0),(5,6,7,0))
{二维数组存放邻接表}
n:array[1..8] of byte=(3,3,4,3,4,3,3,3); {存放邻接顶点数}
var
L:array[1..64] of byte {队列}
F,r:byte {f队头指针,r队尾指针}
B:array[1..64] of byte {链接表,表示某一个结点的前趋结点}
G:array[1..10] of byte {表示层结点的首结点在队列开始的位置}
H:byte {搜索的层次}
由于搜索过的点不再进行搜索,故设置一个数组E[M]为标记,表示结点M是否访问过e:array[1..8] of 0..1;{用1表示已访问过,0表示还没有访问}
c:array[1..8,1..8]of byte; {C[s,j]存储到达目标结点后各最短路径的线路}
bb:Boolean {搜索结束标记}
3、算法描述
⑴设立初值,并令起始结点进队:
f:=0;r:=1;lL[r]:=st,E[st]:=1;w:=1;h:=1;
⑵将此时第h层(开始h=1,表示第一层)的w(开始时w=1,表示一个结点)顶点的顺序出队,并访问与该层各顶点相邻接但又没有访问过的顶点,同时将这些结点进队列,且设立前趋链接指针和访问过标记,若此时的结点为目标结点,则只设立前趋链接指针而不设立访问过标记
⑶计算此时第h+1层的顶点个数w:=r+1-g[h],然后看该层有多少个顶点为目标结点,凡是出现目标顶点的,就将其个数累计,也就是为最短路径的条数,同时从这个目标结点按前趋链接指针将到达该目标结点的路径的各个顶点编号存入c[s,j]中,然后转⑷,若目标顶点累计个数为0,表明该层没有出现目标结点,则转⑵。
⑷打印搜索到的各条最短路径的各结点编号,并结束程序。
程序如下:(见exp7_1.pas)
program exp7_1;
const
d:array[1..8,1..4] of byte=((2,3,4,0),(1,3,7,0),(1,2,4,5),(1,3,6,0),(3,6,7,8),(4,5,8,0),(2,5,8,0),(5,6,7,0));
n:array[1..8] of byte=(3,3,4,3,4,3,3,3);
var
L,b:array[1..64] of byte;
F,r,h,m,st,ed,I,j,t,k,s,p,w:byte;
G:array[1..10] of byte;
e:array[1..8] of 0..1;
c:array[1..8,1..8]of byte;
bb:Boolean;
begin
write('start:');readln(st);
write('end:');readln(ed);
fillchar(e,sizeof(e),0); {标记数组清零}
fillchar(c,sizeof(c),0); {路径数组清零}
f:=0;r:=1;L[r]:=st;h:=1;w:=1;bb:=true;
while bb do
begin
h:=h+1;g[h]:=r+1; {记录h+1层的首地址}
for i:=1 to w do
begin
f:=f+1;m:=L[f];e[m]:=1; {取队首结点,并设访问过标记}
for t:=1 to n[m] do {依次访问m结点的相邻结点}
if e[d[m,t]]=0 then {若m的相邻结点没有访问过}
begin
r:=r+1;L[r]:=d[m,t];b[r]:=f; {则进队列}
end;
end;
w:=r+1-g[h]; {计算第h层的新结点数目}
s:=0;
for k:=g[h] to r do {检查第h层上的新结点是否存在目标结点}
if L[k]=ed then {等于目标结点}
begin
s:=s+1;p:=b[k];j:=1;
c[s,j]:=L[k];
while p<>1 do
begin j:=j+1;c[s,j]:=L[p];p:=b[p]; end;
j:=j+1;c[s,j]:=L[p];
for t:=j downto 1 do
if t=1 then writeln(c[s,t]) else write(c[s,t],'-→');
end;
if s<>0 then
begin
writeln(st,'-→',ed,'total=',s,'step=',j-1);
bb:=false;
end;
end;
end.
输入:
start:1
end:8
输出:
1-→2-→7-→8
1-→3-→5-→8
1-→4-→6-→8
1-→8 total=3 step=3
输入:
start:2
end:6
输出:
2-→1-→4-→6 2-→3-→4-→6 2-→3-→5-→6 2-→7-→5-→6 2-→7-→8-→6 2-→1-→4-→6
2-→6 total=5 step=3 推广应用(作业题1):如下图表示的是从城市A 到城市H 的交通图,从图中可以看出,从城市A 到城市H 要经过若干个城市。
现要找出一条经过城市最少的一条路线。
例2:分酒问题
有一8斤酒瓶装满酒,没有量器,只有两个分别能装5斤和3斤的空酒瓶。
试设计一程序将8斤酒对分为两个4斤,并以最少的步骤给出答案。
[问题分析]
1、 分析
在倒酒过程中,看起来是每一次倒酒,上面的六种操作都可能进行,然而有此操作却是无意义的。
如8斤瓶空时,则8→3、8→5是无意义的。
又如8斤瓶满时,则5→8、3→8操作无意义。
因此,每次倒酒操作后,都必须知道此时三个酒瓶到底多少酒,这样才能准确判断此时何种操作不能进行,何种操作可以进行。
为了表示每操作一次后各酒瓶中的酒量,设变量M 表示8斤瓶在进行第i 操作后装的酒量,N 表示5斤瓶在进行第i 操作后装的酒量,A 表示3斤瓶在进行第i 操作后装的酒量,由于整个酒量为8,所有A=8-M-N 。
对以上六种操作能和不能进行的条件如下:
8→5操作:不能进行的条件为:N=5或M=0
能进行时,剩余量为:N=N+M ,此时如果N>5,则M=N-5,N=5,否则M=0 8→3操作:不能进行的条件为:8-N-M=3或M=0 能进行时,剩余量为:如果M<3-(8-M-N ),则M=0,否则M=5-N ,N 不变 5→8操作:不能进行的条件为:M=8或N=0
能进行时,剩余量为:M=N+M 且N=0 5→3操作:不能进行的条件为:8-N-M=3或N=0
能进行时,剩余量为:如果N<3-(8-M-N ),则N=0,否则N=5-M ,M 不变 3→8操作:不能进行的条件为:8-N-M=0或M=8 能进行时,剩余量为:M=8-N ,N 不变
A B C D G E F
H
3→5操作:不能进行的条件为:8-N-M=0或N=5
能进行时,剩余量为:N=8-M,M不变
2、定义数据结构
const
d:array[1..6] of string[4]=(‘8->5’,’8->3’,’5->8’,’5-3’,’3-8’,’3-5’); {6种操作}
var
L:array[1..50,1..2] of shortint; {表示倒酒后8斤和5斤瓶中的剩余量}
B:array[1..50] of shortint; {每一层的各结点的前趋结点的链指针}
C: array[1..50] of shortint; {每进行一次操作的操作数}
E:array[1..10,1..20] of shortint;{最少步骤的所有解中,该层各结点是由上一层各结点通过何种操作而得到的操作数}
X,y:array[1..10,1..20] of shortint ;{在最少步骤所有解中,各层的结点其8斤和5斤瓶中的剩余量}
G:array[1..20] of shortint; {各层结点的首结点在队列的位置}
F,r,h,w,n,m:shortint; {f为队列首指针,r为队列尾指针,m,n分别表示8斤和5斤瓶中的剩余量,h为搜索的层数,w为每一层结点的个数}
3、算法描述:
⑴设立队首队尾指针初值,f=0,r=1。
并使开始状态的结点入队,L(r,1)=8,同时设立其它初值:h=1,g[h]=r,w=1。
⑵取队列首结点,对该状态分别进行六种操作,并用对六种操作能否进行的条件进行判断,若能进行此操作,则计算进行这种操作后8斤、5斤和3斤瓶中各自的数量,同时将此时的各瓶剩量和上一层以前的各结点进行比较,相同则不产生该结点,即换另一操作并进行刚才相同的判断;若不同,则将此时产生的结点进入队列,然后继续取另一种操作再重复刚才的过程。
⑶当六种操作能否进行的判断完成之后,则对所产生的该层的所有结点进行判断,是否已出现目标结点,若出现目标结点,则将此时的一条最少步骤的解的各结点进行链指针的链接,并将链接的各结点存于数组x[s,i]和y[s,i]中,同时将到达该结点所进行的操作的操作数存于e[s,i]中,同时累计出现目标结点的个数s,并判断s的值是否为0,若s=0则表明没有出现目标结点,转⑵;否则继续⑷
⑷打印输出s种最少步骤的最优解的方案。
4、程序清单:(见exp7_2.pas)
program exp7_2;
uses crt;
const
d:array[1..6] of string[4]=('8->5','8->3','5->8','5->3','3->8','3->5');
var
L:array[1..50,1..2] of shortint;
b,c:array[1..50] of shortint;
e,x,y:array[1..10,1..20] of shortint;
g:array[1..20] of shortint;
f,r,w,h,n,i,m,t,a,bj,s,k:shortint;
b1:boolean;
procedure fpro(k,h:byte); {根据链指针找出解路径}
var
i,p:byte;
begin
p:=k;
for i:=h downto 1 do
begin
x[s,i]:=L[p,1];y[s,i]:=L[p,2];
e[s,i]:=c[p];p:=b[p];
end;
end;
begin
f:=0;r:=1;h:=1;L[r,1]:=8;L[r,2]:=0;不开{初值入队列}
w:=1;b1:=true;a:=0;
while b1 do
begin
h:=h+1;g[h]:=r+1; {记录第h+1层的首位置}
for i:=1 to w do {将原第h层的w个结点逐个扩展}
begin
f:=f+1; {移首指针}
for t:=1 to 6 do {逐一尝试六种操作}
begin
m:=L[f,1];n:=L[f,2]; {取首指针的结点}
case t of {判断对首结点状态哪种操作能进行}
1:{8-->5} {表示是8斤瓶倒5斤瓶}
if (m=0) or (n=5) then bj:=1 {不能操作,设标记bj为1}
else begin {能操作,设bj为0,并计算倒出后各瓶的数量}
n:=n+m;bj:=0;
if n>5 then begin m:=n-5;n:=5;end
else m:=0;
end;
2:{8-->3}
begin
a:=8-m-n; {计算3斤瓶中的酒量}
if (m=0) or (a=3) then bj:=1
else begin
bj:=0;
if m<(3-a) then m:=0 else m:=5-n
end;
end;
3:{5-->8}
if (m=8) or (n=0) then bj:=1
else begin m:=m+n;n:=0;bj:=0 end;
4:{5-->3}
begin
a:=8-m-n;
if (n=0) or (a=3) then bj:=1
else
begin
bj:=0;
if n<3-a then n:=0 else n:=5-m
end;
end;
5:{3-->8}
begin
a:=8-m-n;
if (m=8) or (a=0) then bj:=1
else begin m:=8-n;bj:=0 end;
end;
6:{3-->5}
begin
a:=8-m-n;
if (n=5) or (a=0) then bj:=1
else begin
bj:=0;
if a>(5-n) then n:=5 else n:=8-m;
end;
end;
end;
if bj=0 then {表示有新结点生成}
Begin
for k:=1 to g[h]-1 do {检查新结点是否已出现过}
if (L[k,1]=m) and (L[k,2]=n) then
begin bj:=1;k:=g[h]-1;end;
if bj<>1 then {说明以前没有出现新结点}
begin {新结点进队列,并链接指针,保存操作}
r:=r+1;L[r,1]:=m;L[r,2]:=n;b[r]:=f;c[r]:=t
end;
end;
end;
end;
w:=r+1-g[h];s:=0; {计算已生成的第h层的结点总数}
for k:=g[h] to r do
if (L[k,1]=4) and (L[k,2]=4) then
begin
s:=s+1;
fpro(k,h) {第h层若出现目标结点则找出路径}
end;
if s<>0 then {说明路径存在,打印路径}
begin
clrscr;writeln('best answer');
writeln('step:':5,'operate:':15,'8p':6,'5p':6,'3p':6);
for t:=1 to s do
begin
for k:=1 to h do
begin
a:=8-x[t,k]-y[t,k];
if k=1 then writeln(k-1:4,':',x[t,k]:20,y[t,k]:6,a:6)
else
writeln(k-1:4,':',d[e[t,k]]:15,x[t,k]:5,y[t,k]:6,a:6);
end
end;
b1:=false;
end;
end;
end.
输出结果(略)
推广应用:1、有一10斤酒瓶装满酒,没有量器,只有两个分别能装7斤和3斤的空酒瓶。
试设计一程序将8斤酒对分为两个5斤,并以最少的步骤给出答案。
进一步:有一x斤酒瓶装满酒,没有量器,只有两个分别能装y斤和z斤的空酒瓶(其中x>y>z,且x=y+z)。
试设计一程序将x斤酒对分为两个x/2斤,并以最少的步骤给出答案。
(作业题2)
思考:有两个无刻度标志的水壶,分别可装x升和y升(x,y为整数,x、y<=100)的水。
设另一方面有一水缸,可用来向水壶灌水或倒出水,两水壶间,水也可以相互倾灌。
已知x升为满壶,y升为空壶。
问如何通过倒水或灌水操作用最少步数能在y升壶中量出z (z<=100)升的水来。
(作业题3)
例子3:八数码问题
如下图所示,给出3×3的九个方格,现将1~8这八个自然数填入方格中,给定一个初始状态,例如为:283104765(如图),其中空方格用数字0表示。
现允许移动空格,但每次只能移动1格。
试编一程序完成对于任意给定的一个目标状态(如下图),能够以最少步数实现从初始状态到目标状态的转换。
1、问题分析:
由于空格要进行移动,所以,我们先将方向和移动量表示如下:
我们可以用广度搜索法来解决。
由于广度搜索算法是分层进行的,当搜索的层次越深,每一层的结点数量几乎是以几倍甚至几十倍的数量增加,由于受到内存的影响,所以该程序定义的数组只适合解决目标结点在十一、十二层处。
2、 定义数据结构: const
di:array[1..4] of integer=(0,-1,0,1);{列的方向移动量} dj:array[1..4] of integer=(-1,0,1,0);{行的方向移动量}
d:array[1..3,1..3] of integer=((1,2,3),(8,0,4),(7,6,5));{目标状态各结点的位置} var
L:array[1..3000,1..3,1..3] of integer;{存放能有效移动的各结点}
C:array[1..30,1..3,1..3] of integer;{存放各层通往目标结点的各结点} A:array[1..3,1..3] of integer;{初始和变化过程中的八数码各结点的位置} B:array[1..3000] of integer;{各结点的前趋结点位置的链指针} G:array[1..100] of integer;{队列中各层首结点在队列中的位置} F,r:integer;{队首和队尾的指针} H:integer;{层数}
W:integer;{每层结点个数} 3、算法描述:
⑴输入要排的八数码初始值,并设立队首队尾指针的初值:f:=0;r:=1;h:=1;w:=1;g[h]:=r; ⑵将八数码初始值取出放入a 数组中,同时将初值的各结点的数据入队列; ⑶层次加1:h:=h+1;并记录首结点的位置:g[h]:=r+1;
⑷将h-1层的w 个结点逐一取出,即先移动队首指针:f:=f+1;然后将f 指针位置上的结点各数据L[f,t,j]取出,分别赋给a[t,j],并记录空格所在的位置坐标x,y (列和行)。
然后,在该位置上按空格移动的四个方向顺序移动,并检查哪个方向为有效的移动(无效移动就是移动后出现以前的状态),并将有效移动后的各结点值入队;生成为第h 层的各结点,并继续⑸。
⑸计算第h 层的结点总数w:=r+1-g[h],并对第h 层的各结点进行判断,检查是否出现了目标结点,若没有出现目标结点,则转⑶;若出现目标结点,则进行各层次的解的路径指针值的链接,并转⑹
⑹打印从初始结点到目标结点各层的移动方案。
4、程序清单:(见exp7_3.pas ) program exp7_3; const
di:array[1..4]of integer=(0,-1,0,1);
2
4
3
dj:array[1..4] of integer=(-1,0,1,0);
d:array[1..3,1..3] of integer=((1,2,3),(8,0,4),(7,6,5));
var
a:array[1..3,1..3] of integer;
c:array[1..30,1..3,1..3] of integer;
L:array[1..3000,1..3,1..3] of integer;
b:array[1..3000] of integer;
g:array[1..100] of integer;
st:string[9];
f,r,h,w,i,j,k,t,x,x1,y,y1,m,code,bj,n,s,p:integer;
bb:boolean;
begin
fillchar(L,sizeof(L),0);fillchar(c,sizeof(c),0);{各数组清零}
fillchar(b,sizeof(b),0);fillchar(g,sizeof(g),0);
repeat
write('start:');readln(st);
until length(st)=9;
f:=0;h:=1;r:=1;w:=1;g[h]:=r;k:=0;{设置队列初值}
for i:=1 to 3 do
for j:=1 to 3 do
begin k:=k+1;val(copy(st,k,1),L[r,i,j],code) end; {初始结点入队}
bb:=true;
while bb do
begin
h:=h+1;g[h]:=r+1; {设置第h+1层首结点的位置}
for i:=1 to w do
begin
f:=f+1; {移动首指针}
for k:=1 to 4 do {四个方向循环进行尝试}
begin
for t:=1 to 3 do
for j:=1 to 3 do
begin
a[t,j]:=L[f,t,j];{取队列指针所指的结点}
if a[t,j]=0 then {判断空格的位置}
begin x:=j;y:=t;x1:=j;y1:=t; end;
end;
x:=x+di[k];y:=y+dj[k]; {计算K方向进行移动的坐标}
if (x<1) or (x>3) or (y<1) or (y>3) then {若超出3×3方阵,则另选K}
else begin {在K方向移动后}
m:=a[y,x];a[y,x]:=a[y1,x1];a[y1,x1]:=m; {交换两者位置}
bj:=0;
for n:=1 to g[h]-1 do {判断新结点以前出现过没有}
begin
for t:=1 to 3 do
for j:=1 to 3 do
if L[n,t,j]=a[t,j] then s:=s+1;
if s=9 then begin n:=g[h]-1;bj:=1; end;
end;
if bj=0 then {表示以前没有出现过}
begin {入队}
r:=r+1;
for t:=1 to 3 do
for j:=1 to 3 do L[r,t,j]:=a[t,j];
b[r]:=f;
end;
end;
end;
end;
w:=r+1-g[h]; {计算新生成的第h层的结点个数}
for k:=g[h] to r do {检查h层}
begin
bj:=0;
for t:=1 to 3 do
for j:=1 to 3 do
if L[k,t,j]<>d[t,j] then {不是目标结点}
begin
j:=3;t:=3;bj:=1;
end;
if bj=0 then {有目标结点}
begin
p:=k;
for i:=h downto 1 do {根据链指针找出解路径}
begin
p:=k;
for t:=1 to 3 do
for j:=1 to 3 do
c[i,t,j]:=L[p,t,j];
p:=b[p];
end;
for i:=1 to h do {打印解的路径}
begin
if i=1 then write('start:') else write(i-1,':');
for t:=1 to 3 do
begin
for j:=1 to 3 do write(c[i,t,j],' ');
end;
end;
writeln('step:',h-1);
bb:=false;
end;
end;
end;
end.
输入:
start:2 8 3 1 0 4 7 6 5
输出:(略)
优化1:由于受内存的影响,本程序只能到12层,在再向下搜索则内存不够。
我们可以用动态存储结构来存储。
优化2:本程序表示布局比较直观,但在判断重复,判断是否达到目标方面,却给程序增加了复杂性,也影响了运行速度。
可以改用字符形式表示布局。
如:初始布局表示为“283104765”,目标布局为“123804765”
产生的规则也必须作相应的改动,设空格当前位置是s,则有:
⑴空格向上移动:空格有位置减3,则交换s和s-3的字符;
⑵空格向左移动:空格的位置减1,则交换s和s-1的字符;
⑶空格向右移动:空格的位置加1,则交换s和s+1的字符;
⑷空格向下移动:空格的位置加3,则交换s和s+3的字符;
如果设规则编号为k,则上述四条规则可归纳为一条:
交换s和s+(2*k-5)的字符
优化3:用状态存储的方法进行优化。
对于一个扩展出来的节点,需要用9个字节来存储状态,2个字节来存储它的父亲节点,因此对于每一个节点,要用11个字节来存储。
然而就一个简单的状态而言,如“012345678”,如果用一个数来描述的话,是小于1000000000的,因此可以考虑用一个长整型数来进行存储,这样一来存储一个状态需要的字节降到了4个字节。
采用这种方法存储是非常好了,但是还可以进一步优化存储方法。
我们知道八数码问题状态的总数为9!种,因此0到9!即1到9!个整数,每一个整数都可以相应地表示一种八数码问题的状态,并且可以保证每一个整数所对应的状态都是唯一的,每个整数也唯一的对应一种状态。
这样存储每一种状态只需要3个字节了。
另外,由于状态总数只有9!种,因此可以采用压缩存储哈希表的方法,来对状态进行判重。
运用这种方法能够扩展的节点数目达到30000多个,能够得出较多的解。
【阅读材料】:
利用哈希表进行判重方法
并提供了三条规则:
(1)上下两行互相交换
(2)全体向右平移一格
(3)中间四个顺时针旋转
现在的问题是,如何使用这三条规则,使任意一种给定的状态转变为标准状态。
[分析]看到题目,首先想到的是广度优先搜索。
但是搜索中必然会出现许多重复,大大地增加了时空复杂度,比如连续使用规则A显然是浪费的。
因此需要判重。
如果用传统方法进行判重,将花去大量的时间,事实也是如此。
受到多维数组元素地址的计算方法的启发,可以将魔板的每一种状态按下列规则对应于一个双字节的整数K来考虑:
(1)用一维数组S[1..8]表示一种状态。
例如初始状态S[i]=I;
(2)令T[i]表示S[1..i-1]这i-1个数中,比S[i]小的数的个数;
可以证明0~40319之间的每一个整数唯一的对应一个T数列,也就是说,魔板的每一个状态都与每一个整数一一对应。
因此可以用一个0~40319的布尔型映射数组,初始状态为假。
如果一种状态已经搜索到,则将其对应的数组元素标记为真。
判断一种状态是否与以前的状态重复的时候,只需要检查该状态所对应的数组元素,若为真则该状态一定已经搜索到了。
这样省略了查找工作的时间,虽然多花了40K的空间,却大大提高了时间效率。
源程序如下:
program hx;
const
fact:array[0..7] of word=(1,1,2,6,24,120,720,5040); {数0..7的阶乘}
rule:array[1..3,1..8] of byte=((8,7,6,5,4,3,2,1),(4,1,2,3,6,7,8,5),(1,7,2,4,5,3,6,8));{三种转换规则}
obj=40319 {目标状态编码}
type
ttable=array[1..8] of byte; {描述一个状态}
tlist=^tnode; {搜索中的链表}
tnode=object {搜索的一个结点}
now:word; {当前状态}
next:tlist;
end;
var
t,t1:ttable;
head,tail:tlist;{搜索队列的头和尾}
state:array[0..40319] of byte;{描述状态是否搜索到的映射数组} function changel:word; {将状态转化为数}
var
i,j,s:word;
k:ttable;
begin
for i:=8 downto 1 do
begin
k[i]:=0;
for j:=1 to i-1 do
if t[j]<t[i] then inc(k[i])
end;
s:=0;
for i:=1 to 8 do
inc(s,k[i]*fact[i-1]);
change1:=s;
end;
procedure change2(s:word); {将数转化为状态}
var
i,j:byte;
b:array[1..8] of Boolean;
begin
for i:=8 downto 1 do
begin
t1[i]:=s div fact[i-1];
s:=s mod fact[i-1];
end;
fillchar(b,sizeof(b),false);
for i:=8 downto 1 do
begin
s:=1;
for j:=1 to t1[i] do
begin
while b[s] do inc(s);
inc(s)
end;
while b[s] do inc(s);
t1[i]:=s;b[s]:=true;
end;
end;
procedure reading;
var
i:byte;
writeln(‘input the init state:’);
for i:=1 to 8 do read(t[i]);
new(head);
head^.now:=change1;
head^.next:=nil;
tail:=head;
if head^.now=obj then halt;
fillchar(state,sizeof(state),0);
state[head^.now]:=4;
end;
procedure add(k:word);{加入一个结点,如达到目标,则打印} begin
new(tail^.next);
tail:=tail^next;
tail^.now:=k;
tail^.next:=nil;
state[k]:=I;
if k<>obj then exit;
i:=0;
repeat
write(chr(state[k]+64));
change2(k);
for j:=1 to 8 do
t[j]:=t1[rule[state[k],j]];
k:=change1;
inc(i);
until state[k]=4;
writeln(‘(‘,I,’)’);
halt;
end;
procedure work;
var
q:tlist;
k:word;
begin
repeat
change2(head^.now);
for i:=1 to 3 do
begin
for j:=1 to 8 do
t[rule[I,j]]:=t1[j];
k:=chang1;
if state[k]=0 then add(k);
end;
q:=head;
head:=head^.next;
dispose(q);
until head=nil;
writeln(‘No answer!’);
end;
begin
readin;
work;
end.
习题
题1 如下图表示的是从城市A 到城市H 的交通图,从图中可以看出,从城市A 到城市H 要经过若干个城市。
现要找出一条经过城市最少的一条路线。
输出样例:
(work1.out)
H->F->A
题2 有两个无刻度标志的水壶,分别可装x 升和y 升(x,y 为整数,x 、y<=100)的水。
设另一方面有一水缸,可用来向水壶灌水或倒出水,两水壶间,水也可以相互倾灌。
已知x 升为满壶,y 升为空壶。
问如何通过倒水或灌水操作用最少步数能在y 升壶中量出z (z<=100)升的水来。
输入样例1: (work2.in)
8 5 3
输出样例1: (work2.out)
sep 0: 8 0
sep 1: 3 5
sep 2: 3 0
sep 3: 0 3
输入样例2:(work2.in)
20 10 1
输出样例2:(work2.out)
No answer! A B
C D G E
F H
题3 有N个硬币(6≤N≤8000)正面朝上排成一排,每次将5个硬币翻过来放在原来位置,直到最后全部翻成反面朝上为止。
编写程序让计算机找出步数最少的翻法,并把翻币过程及次数打印出来(用O表示正面,*表示反面)。
输入样例:(work3.in)
6
输出样例:(work3.out)
setp0:oooooo
step1:*****o
step2:oooo**
step3:***ooo
step4:oo****
step5:*ooooo
step6:******
题4现将1~8这八个自然数填入方格中,给定一个初始状态,例如为:281463750,其中空方格用数字0表示。
现允许移动空格,但每次只能移动1格。
试编一程序完成对于任意给定的一个目标状态,如:123804765,能够以最少步数实现从初始状态到目标状态的转换。
(运用状态存储方法)
输入样例:(work4.in)
2 8
3 1 0
4 7 6 5
1 2 3 8 0 4 7 6 5
输出样例:(work4.out)
2 8 3
1 0 4
7 6 5
2 0 3
1 8 4
7 6 5
0 2 3
1 8 4
7 6 5
1 2 3
0 8 4
7 6 5
1 2 3
8 0 4
7 6 5
输入样例:(work4.in)
8 6 3 2 0 5 1 7 4
1 2 3 8 0 4 7 6 5
输出样例:(work4.out)
NO solution!。