图的广度优先搜索的应用.docx
《图的广度优先搜索的应用.docx》由会员分享,可在线阅读,更多相关《图的广度优先搜索的应用.docx(32页珍藏版)》请在冰豆网上搜索。
图的广度优先搜索的应用
图的广度优先搜索的应用
◆内容提要
广度优先搜索是分层次搜索,广泛应用于求解问题的最短路径、最少步骤、最优方法等方面。
本讲座就最短路径问题、分酒问题、八数码问题三个典型的范例,从问题分析、算法、数据结构等多方面进行了讨论,从而形成图的广度优先搜索解决问题的模式,通过本讲座的学习,能明白什么样的问题可以采用或转化为图的广度优先搜索来解决。
在讨论过程中,还同时对同一问题进行了深层次的探讨,进一步寻求解决问题的最优方案。
◆知识讲解和实例分析
和深度优先搜索一样,图的广度优先搜索也有广泛的用途。
由于广度优先搜索是分层次搜索的,即先将所有与上一层顶点相邻接的顶点搜索完之后,再继续往下搜索与该层的所有邻接而又没有访问过的顶点。
故此,当某一层的结点出现目标结点时,这时所进行的步骤是最少的。
所以,图的广度优先搜索广泛应用于求解问题的最短路径、最少步骤、最优方法等方面。
本次讲座就几个典型的范例来说明图的广度优先搜索的应用。
先给出图的广度优先搜索法的算法描述:
变量名
表示含义
F
队列首指针
R
队列尾指针
W
该层顶结点的数目
数组L
队列
数组b
该结点的前趋指针
数组c
生成该结点的操作数
H
搜索树的层数
G[h]
第h层首结点的位置编号
F:
=0;r:
=1;L[r]:
=初始值;
H:
=1;w:
=1;bb:
=true;
Whilebbdo
begin
H:
=h+1;g[h]:
=r+1;
ForI:
=1towdo
Begin
F:
=f+1;
Fort:
=1to操作数do
Begin
⑴m:
=L[f];{出队列};
⑵判断t操作对m结点的相邻结点进行操作;能则设标记bj:
=0,并生成新结点;不能,则设标记bj:
=1;
ifbj:
=0then{表示有新结点生成}
begin
fork:
=1tog[h]-1do
ifL[k]=新结点then{判断新扩展的结点是否以前出现过}
begin
bj:
=1;k:
=g[h]-1
end;
ifbj<>1then{没有出现过}
begin
r:
=r+1;L[r]:
=新结点;{新结点进队列}
b[r]:
=f;c[r]:
=t;{并链接指针,保存操作数}
end;
end;
end;
end;
w:
=r+1-g[h];s:
=0;{计算新生成的一层的结点数}
fork:
=g[h]tordo{在新生成的一层结点中,判断是否有目标结点存在}
ifL[k]=目标结点then
begin
s:
=s+1;{累计解的条数}
根据链接指针求出路径;
end;
ifs:
<>0then
begin
输出s条路径;
bb:
=false;{设程序结束条件}
end;
end;
例1:
最短路径问题
求从任意一个顶点Vi出发,对给出的图,求到达任意顶点Vj(i<>j)的所有最短路径
[问题分析]
1、首先用邻接表表示此图各端点的邻接关系。
顶点
接邻顶点
个数
1
2
3
4
3
2
1
3
7
3
3
1
2
4
5
4
4
1
3
6
3
5
3
6
7
8
4
6
4
5
8
3
7
2
5
8
3
8
5
6
7
3
2、数据结构
const
d:
array[1..8,1..4]ofbyte=((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]ofbyte=(3,3,4,3,4,3,3,3);{存放邻接顶点数}
var
L:
array[1..64]ofbyte{队列}
F,r:
byte{f队头指针,r队尾指针}
B:
array[1..64]ofbyte{链接表,表示某一个结点的前趋结点}
G:
array[1..10]ofbyte{表示层结点的首结点在队列开始的位置}
H:
byte{搜索的层次}
由于搜索过的点不再进行搜索,故设置一个数组E[M]为标记,表示结点M是否访问过
e:
array[1..8]of0..1;{用1表示已访问过,0表示还没有访问}
c:
array[1..8,1..8]ofbyte;{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)
programexp7_1;
const
d:
array[1..8,1..4]ofbyte=((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]ofbyte=(3,3,4,3,4,3,3,3);
var
L,b:
array[1..64]ofbyte;
F,r,h,m,st,ed,I,j,t,k,s,p,w:
byte;
G:
array[1..10]ofbyte;
e:
array[1..8]of0..1;
c:
array[1..8,1..8]ofbyte;
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;
whilebbdo
begin
h:
=h+1;g[h]:
=r+1;{记录h+1层的首地址}
fori:
=1towdo
begin
f:
=f+1;m:
=L[f];e[m]:
=1;{取队首结点,并设访问过标记}
fort:
=1ton[m]do{依次访问m结点的相邻结点}
ife[d[m,t]]=0then{若m的相邻结点没有访问过}
begin
r:
=r+1;L[r]:
=d[m,t];b[r]:
=f;{则进队列}
end;
end;
w:
=r+1-g[h];{计算第h层的新结点数目}
s:
=0;
fork:
=g[h]tordo{检查第h层上的新结点是否存在目标结点}
ifL[k]=edthen{等于目标结点}
begin
s:
=s+1;p:
=b[k];j:
=1;
c[s,j]:
=L[k];
whilep<>1do
beginj:
=j+1;c[s,j]:
=L[p];p:
=b[p];end;
j:
=j+1;c[s,j]:
=L[p];
fort:
=jdownto1do
ift=1thenwriteln(c[s,t])elsewrite(c[s,t],'-');
end;
ifs<>0then
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-8total=3step=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-6total=5step=3
推广应用(作业题1):
如下图表示的是从城市A到城市H的交通图,从图中可以看出,从城市A到城市H要经过若干个城市。
现要找出一条经过城市最少的一条路线。
例2:
分酒问题
有一8斤酒瓶装满酒,没有量器,只有两个分别能装5斤和3斤的空酒瓶。
试设计一程序将8斤酒对分为两个4斤,并以最少的步骤给出答案。
[问题分析]
1、分析
要解决分酒问题,先将所有倒酒的可能性全列出来,如下表:
操作数B[i]
1
2
3
4
5
6
操作D[k]
8→5
8→3
5→8
5→3
3→8
3→5
在倒酒过程中,看起来是每一次倒酒,上面的六种操作都可能进行,然而有此操作却是无意义的。
如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不变
3→5操作:
不能进行的条件为:
8-N-M=0或N=5
能进行时,剩余量为:
N=8-M,M不变
2、定义数据结构
const
d:
array[1..6]ofstring[4]=(‘8->5’,’8->3’,’5->8’,’5-3’,’3-8’,’3-5’);{6种操作}
var
L:
array[1..50,1..2]ofshortint;{表示倒酒后8斤和5斤瓶中的剩余量}
B:
array[1..50]ofshortint;{每一层的各结点的前趋结点的链指针}
C:
array[1..50]ofshortint;{每进行一次操作的操作数}
E:
array[1..10,1..20]ofshortint;{最少步骤的所有解中,该层各结点是由上一层各结点通过何种操作而得到的操作数}
X,y:
array[1..10,1..20]ofshortint;{在最少步骤所有解中,各层的结点其8斤和5斤瓶中的剩余量}
G:
array[1..20]ofshortint;{各层结点的首结点在队列的位置}
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)
programexp7_2;
usescrt;
const
d:
array[1..6]ofstring[4]=('8->5','8->3','5->8','5->3','3->8','3->5');
var
L:
array[1..50,1..2]ofshortint;
b,c:
array[1..50]ofshortint;
e,x,y:
array[1..10,1..20]ofshortint;
g:
array[1..20]ofshortint;
f,r,w,h,n,i,m,t,a,bj,s,k:
shortint;
b1:
boolean;
procedurefpro(k,h:
byte);{根据链指针找出解路径}
var
i,p:
byte;
begin
p:
=k;
fori:
=hdownto1do
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;
whileb1do
begin
h:
=h+1;g[h]:
=r+1;{记录第h+1层的首位置}
fori:
=1towdo{将原第h层的w个结点逐个扩展}
begin
f:
=f+1;{移首指针}
fort:
=1to6do{逐一尝试六种操作}
begin
m:
=L[f,1];n:
=L[f,2];{取首指针的结点}
casetof{判断对首结点状态哪种操作能进行}
1:
{8-->5}{表示是8斤瓶倒5斤瓶}
if(m=0)or(n=5)thenbj:
=1{不能操作,设标记bj为1}
elsebegin{能操作,设bj为0,并计算倒出后各瓶的数量}
n:
=n+m;bj:
=0;
ifn>5thenbeginm:
=n-5;n:
=5;end
elsem:
=0;
end;
2:
{8-->3}
begin
a:
=8-m-n;{计算3斤瓶中的酒量}
if(m=0)or(a=3)thenbj:
=1
elsebegin
bj:
=0;
ifm<(3-a)thenm:
=0elsem:
=5-n
end;
end;
3:
{5-->8}
if(m=8)or(n=0)thenbj:
=1
elsebeginm:
=m+n;n:
=0;bj:
=0end;
4:
{5-->3}
begin
a:
=8-m-n;
if(n=0)or(a=3)thenbj:
=1
else
begin
bj:
=0;
ifn<3-athenn:
=0elsen:
=5-m
end;
end;
5:
{3-->8}
begin
a:
=8-m-n;
if(m=8)or(a=0)thenbj:
=1
elsebeginm:
=8-n;bj:
=0end;
end;
6:
{3-->5}
begin
a:
=8-m-n;
if(n=5)or(a=0)thenbj:
=1
elsebegin
bj:
=0;
ifa>(5-n)thenn:
=5elsen:
=8-m;
end;
end;
end;
ifbj=0then{表示有新结点生成}
Begin
fork:
=1tog[h]-1do{检查新结点是否已出现过}
if(L[k,1]=m)and(L[k,2]=n)then
beginbj:
=1;k:
=g[h]-1;end;
ifbj<>1then{说明以前没有出现新结点}
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层的结点总数}
fork:
=g[h]tordo
if(L[k,1]=4)and(L[k,2]=4)then
begin
s:
=s+1;
fpro(k,h){第h层若出现目标结点则找出路径}
end;
ifs<>0then{说明路径存在,打印路径}
begin
clrscr;writeln('bestanswer');
writeln('step:
':
5,'operate:
':
15,'8p':
6,'5p':
6,'3p':
6);
fort:
=1tosdo
begin
fork:
=1tohdo
begin
a:
=8-x[t,k]-y[t,k];
ifk=1thenwriteln(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
3
8
0
4
7
6
5
2
8
3
1
0
4
7
6
5
1、问题分析:
由于空格要进行移动,所以,我们先将方向和移动量表示如下:
1
2
4
移动方向
1
2
3
4
Di
0
-1
0
1
Dj
-1
0
1
0
3
我们可以用广度搜索法来解决。
由于广度搜索算法是分层进行的,当搜索的层次越深,每一层的结点数量几乎是以几倍甚至几十倍的数量增加,由于受到内存的影响,所以该程序定义的数组只适合解决目标结点在十一、十二层处。
2、定义数据结构:
const
di:
array[1..4]ofinteger=(0,-1,0,1);{列的方向移动量}
dj:
array[1..4]ofinteger=(-1,0,1,0);{行的方向移动量}
d:
array[1..3,1..3]ofinteger=((1,2,3),(8,0,4),(7,6,5));{目标状态各结点的位置}
var
L:
array[1..3000,1..3,1..3]ofinteger;{存放能有效移动的各结点}
C:
array[1..30,1..3,1..3]ofinteger;{存放各层通往目标结点的各结点}
A:
array[1..3,1..3]ofinteger;{初始和变化过程中的八数码各结点的位置}
B:
array[1..3000]ofinteger;{各结点的前趋结点位置的链指针}
G:
array[1..100]ofinteger;{队列中各层首结点在队列中的位置}
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)
programexp7_3;
const
di:
array[1..4]ofinteger=(0,-1,0,1);
dj:
array[1..4]ofinteger=(-1,0,1,0);
d:
array[1..3,1..3]ofinteger=((1,2,3),(8,0,4),(7,6,5));
var
a:
array[1..3,1..3]ofinteger;
c:
array[1..30,1..3,1..3]