深度优先搜索.docx
《深度优先搜索.docx》由会员分享,可在线阅读,更多相关《深度优先搜索.docx(17页珍藏版)》请在冰豆网上搜索。
深度优先搜索
深度优先搜索
一、搜索过程
深度优先搜索的搜索过程类似树的先序遍历,也叫回溯法。
搜索过程如下:
从源节点开始发现有一节点v,如果v还有未探测到的边,就沿此边继续探测下去,当节点v的所有边都被探测过,搜索过程将回溯到最初发现v点的源节点。
这一过程一直进行到已发现从源节点可达的所有节点为止。
这显然是一个递归过程。
为了在遍历过程中区分顶点是否被访问,往往可以引入一个数组,如以mark[1..n]作为标记。
数组的元素取0和1,初值为0。
当节点被访问时,与节点相应得数组元素为1,每次访问节点时,都得先检查它的标记值,找0值得节点访问,并深度继续。
深度大的先得到扩展,具有“后产生先扩展”的特点,因此在数据结构上采用堆栈来存储(新节点入栈,节点不能扩展时,栈定出栈)。
二、搜索特点
1、由于深度搜索过程中有保留已扩展节点,则不致于重复构造不必要的子树系统。
2、深度优先搜索并不是以最快的方式搜索到解,因为若目标节点在第i层的某处,必须等到该节点左边所有子树系统搜索完毕之后,才会访问到该节点,因此,搜索效率还取决于目标节点在解答树中的位置。
3、由于要存储所有已被扩展节点,所以需要的内存空间往往比较大。
4、深度优先搜索所求得的是仅仅是目前第一条从起点至目标节点的树枝路径,而不是所有通向目标节点的树枝节点的路径中最短的路径。
5、适用范围:
适用于求解一条从初始节点至目标节点的可能路径的试题。
若要存储所有解答路径,可以再建立其它空间,用来存储每个已求得的解。
若要求得最优解,必须记下达到目前目标的路径和相应的路程值,并与前面已记录的值进行比较,保留其中最优解,等全部搜索完成后,把保留的最优解输出。
三、算法描述
1、算法数据结构描述:
深度优先搜索时,最关键的是结点扩展(OPEN)表的生成,它是一个栈,用于存放目前搜索到待扩展的结点,当结点到达深度界限或结点不能再扩展时,栈顶结点出栈,放入CLOSE表(存放已扩展节点),继续生成新的结点入栈OPEN表,直到搜索到目标结点或OPEN栈空为止。
具体算法如下:
①把起始结点S放到非扩展结点OPEN表中(后进先出的堆栈),如果此结点为一目标结点,则得到一个解。
②如果OPEN为一空表,则搜索失败退出。
③取OPEN表最前面(栈顶)的结点,并把它放入CLOSED的扩展结点表中,并冠以顺序编号n。
④如果结点n的深度等于最大深度,则转向2。
⑤否则,扩展结点n,产生其全部子结点,把它们放入OPEN表的前头(入栈),并配上指向n的返回指针;如果没有后裔,则转向2。
⑥如果后继结点中有任一个为目标结点,则求得一个解,成功退出;否则,转向2。
2、算法程序描述:
①递归
递归过程为:
ProcedureDEF-GO(step)
fori:
=1tomaxdo
if子结点符合条件then
产生新的子结点入栈;
if子结点是目标结点then输出
elseDEF-GO(step+1);
栈顶结点出栈;
endif;
enddo;
主程序为:
ProgramDFS;
初始状态入栈;
DEF-GO
(1);
②非递归
ProgramDEF(step);
step:
=0;
repeat
step:
=step+1;
j:
=0;p:
=false
repeat
j:
=j+1;
if结点符合条件then
产生子结点入栈;
if子结点是目标结点then输出
elsep:
=true;
else
ifj>=maxthen回溯p:
=false;
endif;
untilp=true;
untilstep=0;
回溯过程如下:
ProcedureBACK;
step:
=step-1;
ifstep>0then栈顶结点出栈
elsep:
=true;
例如 八数码难题--已知8个数的起始状态如图1(a),要得到目标状态为图1(b)。
2 8 3 1 2 3
1 6 4 8 ■ 4
7 ■ 5 7 6 5
(a) (b)
图1
求解时,首先要生成一棵结点的搜索树,按照深度优先搜索算法,我们可以生成图2的搜索树。
图中,所有结点都用相应的数据库来标记,并按照结点扩展的顺序加以编号。
其中,我们设置深度界限为5。
粗线条路径表示求得的一个解。
从图中可见,深度优先搜索过程是沿着一条路径进行下去,直到深度界限为止,回溯一步,再继续往下搜索,直到找到目标状态或OPEN表为空为止。
图2
四、关于深度优先搜索的下界
对于许多问题,深度优先搜索查找的解答树可能含有无穷分支(深度优先搜索误入无穷分支就不可能找到目标节点),或者其深度可能至少要比某个可以接受的解答系列的已知上限还要深,或者能估计出目标节点不会超过多少层。
为了避免可能太长的路径,给出一个节点扩展的最大深度,即深度界限D,任何节点达到了D,那么都将它们作为没有后继节点处理。
如图2我们设置深度界限为5,如果我们不对它的深度进行限定,那么第5层以下可以产生大量的搜索节点,而目标节点可以在第5层找到。
深度优先搜索是最常用的算法之一,而确定“深度D”是解题的关键,因为我们需要它消除不必要的搜索,提高搜索效率。
估算“深度D”的方法:
无章可循,凭经验和大致的计算,在时间和空间允许的范围内,宁大勿小。
例题1:
设有A,B,C,D,E五人从事J1,J2,J3,J4,J5五项工作,每人只能从事一项,他们的效益如下,求最佳安排使效益最高。
图 3
分析:
每人选择五项工作中的一项,在各种选择的组合中,找到效益最高的的一种组合输出。
算法步骤:
⒈数据库:
用数组f构成堆栈存放产生的结点;数组g存放当前最高效益结点的组合;数组p作为结点是否选择过的标志位。
⒉结点的产生:
(1)选择p(i)=0的结点;
(2)判断效益是否高于g记录结点的效益,是高于则更新g数组及最高效益值。
⒊搜索策略:
深度优先搜索。
源程序如下:
programexam1;
const
data:
array[1..5,1..5]ofinteger
=((13,11,10,4,7),(13,10,10,8,5),(5,9,7,7,4),
(15,12,10,11,5),(10,11,8,8,4));
var
i,max:
integer;
f,g:
array[1..5]ofinteger;
p:
array[1..5]ofinteger;
procedurego(step,t:
integer);{选择最佳效益结点的组合}
var
i:
integer;
begin
fori:
=1to5do
ifp[i]=0thenbegin
f[step]:
=i;
p[i]:
=1;
t:
=t+data[step,i];
ifstep<5thengo(step+1,t)
else
ift>maxthenbegin
max:
=t;
g:
=f;
end;
t:
=t-data[step,i];
p[i]:
=0;
end;
end;
begin
max:
=0;
fori:
=1to5dop[i]:
=0;
go(1,0);
writeln;
fori:
=1to5dowrite(chr(64+i),':
J',g[i],'':
3);
writeln;
writeln('supply:
',max);
end.
例题2:
马的遍历
中国象棋半张棋盘如图4(a)所示。
马自左下角往右上角跳。
今规定只许往右跳,不许往左跳。
比如图4(a)中所示为一种跳行路线,并将所经路线打印出来。
打印格式为:
0,0->2,1->3,3->1,4->3,5->2,7->4,8…
4
1
3
2
2
1
3
4
012345678
(a)(b)
图4
分析:
如图4(b),马最多有四个方向,若原来的横坐标为j、纵坐标为i,则四个方向的移动可表示为:
1:
(i,j)→(i+2,j+1);(i<3,j<8)
2:
(i,j)→(i+1,j+2);(i<4,j<7)
3:
(i,j)→(i-1,j+2);(i>0,j<7)
4:
(i,j)→(i-2,j+1);(i>1,j<8)
搜索策略:
S1:
A[1]:
=(0,0);
S2:
从A[1]出发,按移动规则依次选定某个方向,如果达到的是(4,8)则转向S3,否则继续搜索下一个到达的顶点;
S3:
打印路径。
源程序范例:
programexam2;
const
x:
array[1..4,1..2]ofinteger=((2,1),(1,2),(-1,2),(-2,1));{四种移动规则}
var
t:
integer;{路径总数}
a:
array[1..9,1..2]ofinteger;{路径}
procedureprint(ii:
integer);{打印}
var
i:
integer;
begin
inc(t);
fori:
=1toii-1do
write(a[i,1],',',a[i,2],'-->');
writeln('4,8',t:
5);
readln;
end;
proceduretry(i:
integer);{搜索}
var
j:
integer;
begin
forj:
=1to4do
if(a[i-1,1]+x[j,1]>=0)and(a[i-1,1]+x[j,1]<=4)and
(a[i-1,2]+x[j,2]>=0)and(a[i-1,2]+x[j,2]<=8)then
begin
a[i,1]:
=a[i-1,1]+x[j,1];
a[i,2]:
=a[i-1,2]+x[j,2];
if(a[i,1]=4)and(a[i,2]=8)then
print(i)
elsetry(i+1);{搜索下一步}
a[i,1]:
=0;a[i,2]:
=0
end;
end;
begin{主程序}
try
(2);
end.
例题3:
选书
书本学生
学校放寒假时,信息学竞赛辅导老师有A,B,C,D,E五本书,要分给参加培训的张、王、刘、孙、李五位同学,每人只能选一本书。
老师事先让每个人将自己喜欢的书填写在如下的表格中。
然后根据他们填写的表来分配书本,希望设计一个程序帮助老师求出所有可能的分配方案,使每个学生都满意。
学生
A
B
C
D
E
张同学
Y
Y
王同学
Y
Y
Y
刘同学
Y
Y
孙同学
Y
李同学
Y
Y
分析:
可用穷举法,先不考虑“每人都满意”这一条件,这样只剩“每人选一本且只能选一本”这一条件。
在这个条件下,可行解就是五本书的所有全排列,一共有5!
=120种。
然后在120种可行解中一一删去不符合“每人都满意”的解,留下的就是本题的解答。
为了编程方便,设1,2,3,4,5分别表示这五本书。
这五个数的一种全排列就是五本书的一种分发。
例如54321就表示第5本书(即E)分给张,第4本书(即D)分给王,……,第1本书(即A)分给李。
“喜爱书表”可以用二维数组来表示,1表示喜爱,0表示不喜爱。
算法设计:
S1:
产生5个数字的一个全排列;
S2:
检查是否符合“喜爱书表”的条件,如果符合就打印出来;
S3:
检查是否所有的排列都产生了,如果没有产生完,则返回S1;
S4:
结束。
上述算法有可以改进的地方。
比如产生了一个全排列12345,从表中可以看出,选第一本书即给张同学的书,1是不可能的,因为张只喜欢第3、4本书。
这就是说,1××××一类的分法都不符合条件。
由此想到,如果选定第一本书后,就立即检查一下是否符合条件,发现1是不符合的,后面的四个数字就不必选了,这样就减少了运算量。
换句话说,第一个数字只在3、4中选择,这样就可以减少3/5的运算量。
同理,选定了第一个数字后,也不应该把其他4个数字一次选定,而是选择了第二个数字后,就立即检查是否符合条件。
例如,第一个数选3,第二个数选4后,立即检查,发现不符合条件,就应另选第二个数。
这样就把34×××一类的分法在产生前就删去了。
又减少了一部分运算量。
综上所述,改进后的算法应该是:
在产生排列时,每增加一个数,就检查该数是否符合条件,不符合,就立刻换一个,符合条件后,再产生下一个数。
因为从第I本书到第I+1本书的寻找过程是相同的,所以可以用递归方法。
算法设计如下:
proceduretry(i);
begin
forj:
=1to5do
begin
if第i个同学分给第j本书符合条件then
begin
记录第i个数3
ifi=5then打印一个解
elsetry(i+1);
删去第i个数字
end;
end;
end;
源程序:
programexam3;
typefive=1..5;
const
like:
array[five,five]of0..1=((0,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]ofstring[6]=('zhang','wang','liu','sun','li');
var
book:
array[1..5]of0..5;
flag:
setoffive;
c:
integer;
procedureprint;
vari:
integer;
begin
inc(c);writeln('answer',c,':
');
fori:
=1to5do
writeln(name[i]:
10,':
',chr(64+book[i]));
end;
proceduretry(i:
integer);
varj:
integer;
begin
forj:
=1to5do
ifnot(jinflag)and(like[i,j]>0)then
begin
flag:
=flag+[j];book[i]:
=j;
ifi=5thenprint
elsetry(i+1);
flag:
=flag-[j];book[i]:
=0;
end;
end;
begin
flag:
=[];c:
=0;try
(1);
readln
end.
输出结果:
zhang:
C
wang:
A
liu:
B
sun:
D
li:
E
六、深度优先搜索
(二)
前面用深度优先搜索算法求解问题的过程中,用堆栈来存放产生的结点,因此只保留了与当前结点有关的父背结点,这样可以节约大量存储空间。
但如果求解的问题要求保留在搜索过程中产生的全部结点,算法需要如何设计呢?
可以用原综合数据库存放产生的结点,每个结点包括结点数据和父结点指针两项。
再设置两个索引表:
OPEN表和CLOSE表,OPEN表放还未扩展完其子结点的结点编号,CLOSE表放已扩展完的结点编号。
为实现最新产生的结点先扩展的深度优先原则,OPEN表设计成堆栈形式。
算法设计如下:
S1:
初始化数据库,根结点放入数据库;
S2:
CLOSE表为空,根结点编号压入OPEN表;
S3:
如OPEN表为空,则转S7;
S4:
弹出OPEN表顶的结点为当前结点,扩展她的新子结点Mj存入数据库,并把编号压入OPEN表中;
S5:
如果Mj是目标结点,则输出或记录;
S6:
返回S3;
S7:
结束处理。
例题4:
六个城市之间道路联系的示意图如下图所示。
连线表示两城市间有道路相通,连线旁的数字表示路程。
请编写程序,有计算机找出从C1城到C6城的没有重复城市的所有不同路径,按照路程总长度的大小从小到大地打印出来这些路径。
输出格式:
1:
1-->2-->5-->6const=14
2:
1-->2-->3-->5-->6const=15
3:
1-->3-->5-->6const=16
……………………………………..
【分析】道路之间的联系可以用一个6×6的“邻接距阵”(即二维数组)LINK来表示,LINK(i,j)的值表示Ci到Cj城之间的路程,如果值等于零表示没有通路。
123456
1048000
2403460
3830220
4042049
5062404
6000940
建立产生式系统:
其中数据库用数组OPEN做索引表,用NODE(字符传数组)记录路径,LONG记录路径长度;
产生式规则:
R为下一个城市编号,2≤R≤6,K是当前路径,则有5条规则:
IFLINK(K[LENGTH(K)],R)>0且CR没有到过THEN
增加一个NODE元素,把新增元素赋值为:
K+R。
增加一个OPEN元素,记下NODE元素的位置。
搜索策略:
见源程序
programexam4;
const
max=maxint;
link:
array[1..5,1..6]ofinteger=((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));{邻接表:
最后一行可以省略,因为到达C6后不能再到别的城市}
type
path=string[6];{字符串记录路径}
var
open:
array[1..6]ofinteger;{索引表}
node:
array[1..100]ofpath;{记录所有路径}
count,i,n:
integer;
proceduretry(k,dep:
integer);{搜索过程}
var
r,len:
byte;
temp:
path;
begin
temp:
=node[open[dep]];{取出NODE表中最后一个元素}
len:
=length(temp);
ifpos('6',temp)>0thenexit{不能再到别的城市}
else
forr:
=2to6do
if(link[k,r]>0)and(pos(chr(48+r),temp)=0)then
begin
inc(n);node[n]:
=temp+chr(48+r);
open[dep+1]:
=n;try(r,dep+1){搜索下一个城市}
end
end;
procedureprint;{打印}
var
f,i,j,k,l:
integer;
bool:
array[1..100]ofboolean;{记录某路径是否已经打印}
long:
array[1..100]ofinteger;{记录某路径的总长度}
begin
count:
=0;
fori:
=1tondo
ifnode[i,length(node[i])]<>'6'then
bool[i]:
=false
else
beginbool[i]:
=true;inc(count);long[i]:
=0;
forj:
=2tolength(node[i])do
long[i]:
=long[i]+link[ord(node[i,j-1])-48,ord(node[i,j])-48];{统计长度}
end;
fori:
=1tocountdo
begin
k:
=maxint;
forj:
=1tondo
if(bool[j])and(long[j]begink:
=long[j];l:
=jend;
bool[l]:
=false;write(i:
2,':
1');
forj:
=2tolength(node[l])dowrite('-->',node[l,j]);{输出路径}
writeln('cost=',k){输出总长度}
end;{输出}
readln
end;
begin
n:
=1;node[1]:
='1';open[1]:
=1;{赋初始值}
try(1,1);{搜索}
print;{打印}
end.