搜索与回溯算法讲解.docx
- 文档编号:23745084
- 上传时间:2023-05-20
- 格式:DOCX
- 页数:52
- 大小:115.43KB
搜索与回溯算法讲解.docx
《搜索与回溯算法讲解.docx》由会员分享,可在线阅读,更多相关《搜索与回溯算法讲解.docx(52页珍藏版)》请在冰豆网上搜索。
搜索与回溯算法讲解
搜索与回溯算法
回溯算法2
一、回溯法的思想2
二、实现要点2
三、应用举例2
【例1】马的遍历2
【例2】选书5
四、回溯典型例子7
【例1】八皇后问题7
【例2】骑士遍历问题12
五、递归中体现回溯15
【例1】走迷宫问题15
搜索算法19
一、深度优先搜索19
二、广度优先搜索29
回溯算法
一、回溯法的思想
在求解一些问题(如走迷宫、地图着色等问题)时,题目的要求可能是求出原问题的一种或所有可能的解决方案。
这类问题的解往往是由一个一个的步骤或状态所构成的,每一步骤又有若干种可能的决策方案;由于没有固定、明确的数学解析方法,往往要采用搜索的做法,即从某一个初始状态出发,不断地向前(即下一个状态)搜索,以期最终达到目标状态,从而得到原问题的一个解或所有的解。
在搜索的过程中,由于问题本身及所采取的搜索方法的特点(如在缺乏全局及足够的前瞻信息的情况下进行搜索等),会导致走到某一状态就走不下去的情况,这时,就必须回头(即回到上一步,而不是回到最初的状态),再尝试其他的可能性,换一个方向或方法再试试。
这样,不断地向前探索、回溯,再向前、再回溯,直至最终得出问题的解,或者一路回溯到出发点(出现这种情况即表示原问题无解)。
注意,这种搜索过程并不是尝试搜索问题解空间中所有的可能状态和路径,而是采用深度优先的方式,沿着一条路径,尽可能深入地向前探索。
二、实现要点
由于回溯法是一种搜索方法,而且是一种深度优先式的搜索算法,在搜索过程中,前进和回退交织进行,因而,必须有适当的手段和机制来已经走过的状态,以便在发生回溯时能够回退到上一步,再选择与上一步不同的方向继续探索下去。
要满足回溯过程中对状态进行记录的要求,最好的数据结构便是堆栈了,它与回溯时只回退到上一步非常吻合。
(在具体实现时,栈又可以采用数组、链表等结构来实现。
)此外,还要有适当的数据结构来表示整个问题的可能解空间,以及在每一步(每一状态)时可能的选择。
在理解和把握回溯法时,首先要能够手工地理解和模拟在搜索过程中,实际的前进和后退的过程,真正以人工的方式想通实际发生的前进-回退过程。
然后,在编制程序时,就象递归算法一样,从较为宏观而自然的层面上,来表达出这种不断进-退的过程,即可构造出有效的回溯算法。
在回溯算法中,把握好何时发生回溯(即发生回溯的条件)是非常关键的,应注意有效而正确地表达回溯条件。
还要注意,原问题的状态空间、每一步可能的行动方案、记录已走过的所有步骤的数据结构等,都是非常关键的。
此外,从本质上看,回溯算法符合递归算法,问题的解决可以转化为子问题,其子问题的解法与原问题相同,只是数据增大或减少;因此,在实现回溯算法时,又常常可以考虑采用递归算法来实现,这时,算法中所涉及的堆栈等数据结构就由系统来维护了。
三、应用举例
回溯法可以用来解决一系列问题,如自然数的排列、n皇后问题、迷宫问题、数的拆分、0/1背包问题、旅行商问题、货船装货问题、图形覆盖正方形等。
【例1】马的遍历
中国象棋半张棋盘如图1(a)所示。
马自左下角往右上角跳。
今规定只许往右跳,不许往左跳。
比如图中所示为一种跳行路线,并将所经路线打印出来。
打印格式为:
0,0->2,1->3,3->1,4->3,5->2,7->4,814(14表示第十四种路线,场宽为5)
【算法分析】
如图马最多有四个方向,若原来的横坐标为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:
打印路径。
【算法设计】
proceduretry(i:
integer);{搜索}
varj:
integer;
begin
forj:
=1to4do{试遍4个方向}
if新坐标满足条件then
begin记录新坐标;
if到达目的地thenprint{统计方案,输出结果}
elsetry(i+1);{试探下一步}
退回到上一个坐标,即回溯;
end;
end;
【参考程序】
programexam1;{递归+回溯}
const
x:
array[1..4,1..2]ofinteger=((2,1),(1,2),(-1,2),(-2,1));{四种移动规则}
vart:
integer;{路径总数}
a:
array[1..9,1..2]ofinteger;{路径}
procedureprint(ii:
integer);{打印}
vari:
integer;
begin
inc(t);{路径总数}
fori:
=1toii-1do
write(a[i,1],',',a[i,2],'->');
writeln('4,8',t:
5);
end;
proceduretry(i:
integer);{搜索}
varj:
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)thenprint(i)
elsetry(i+1);{搜索下一步}
a[i,1]:
=0;a[i,2]:
=0
end;
end;
BEGIN{主程序}
a[1,1]:
=0;a[1,2]:
=0;try
(2);
END.
programexam2;{回溯}
const
x:
array[1..4,1..2]ofinteger=((2,1),(1,2),(-1,2),(-2,1));{四种移动规则}
vart,p,i:
integer;{路径总数}
a:
array[1..9,1..3]ofinteger;{路径及栈}
begin
p:
=1;a[1,1]:
=0;a[1,2]:
=0;a[1,3]:
=0;t:
=0;
whilep>0do
ifa[p,3]<4then
begin
a[p,3]:
=a[p,3]+1;
if(a[p,1]+x[a[p,3],1]>=0)and(a[p,1]+x[a[p,3],1]<=4)
and(a[p,2]+x[a[p,3],2]>=0)and(a[p,2]+x[a[p,3],2]<=8)
thenbegin
a[p+1,1]:
=a[p,1]+x[a[p,3],1];
a[p+1,2]:
=a[p,2]+x[a[p,3],2];
p:
=p+1;a[p,3]:
=0;
if(a[p,1]=4)and(a[p,2]=8)then
begin
t:
=t+1;
fori:
=1top-1dowrite(a[i,1],',',a[i,2],'->');
writeln('4,8',t:
5);
end;
end;
end
else
begin
a[p,3]:
=0;
p:
=p-1;
end;
end.
【例2】选书
学校放寒假时,信息学竞赛辅导老师有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);
beginforj:
=1to5do
begin
if第i个同学分给第j本书符合条件then
begin
记录第i个数
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');
varbook:
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
四、回溯典型例子
【例1】八皇后问题
[问题描述]:
在国际象棋的棋盘上如何放置8个皇后,使它们能够相不干扰,即你吃不了我、我也吃不了你。
国际象棋中皇后吃子的方法是只要在它同一行、同一列或同一斜线上的对方棋子都可以吃。
所以放置这8个皇后要使它们任意两个都不在同一行、同一列或同一斜线上。
[分析与算法选择]:
国际象棋的棋盘共有8*8个格子,先从行来考虑,8个皇后都不在同一行,所以每行只能有一个皇后。
我们将第一行到第八行的皇后依次编号为1到8,每一行直接考虑放置一个皇后,因此不要考虑行是否冲突的问题。
对于每一个皇后放在具体的哪一列,可以用循环来检测。
检测的有两个部分:
当前位置不与前几行已放好的皇后在同一列;当前位置不跟前几行已放好的皇后在同一斜线上。
对于是否在同一列可以直接检查它们的列号是否相同,而是否在同一斜线上则有两种情况,一是从左上到右下方向的,另一个是从右上到左下方向的。
从左上到右下方向的斜线和从右上到左下的斜线各有15条,如下图所示:
其中只是跨一个格子的对于判定没有实际意义,真正有判定意义的是各有13个。
我们不必要分别检验过去看是哪一条。
假设这8*8个格子简化为点,分别用两个数字x和y来描述其位置,其中x表示其在第几行,y表示其在第几列。
(1)从左上到右下的斜线:
从(1,1)到(8,8)这条斜线上的各点的都有x=y,即x-y=0。
从(1,2)到(7,8)这条斜线上的点分别是:
(1,2),(2,3),(3,4),(4,5),(5,6),(6,7),(7,8),各点都符合条件x-y=-1。
……
从左上到右下的斜线都有类似的特点,即x-y=定值(从-7到7)。
(2)从右上到左下的斜线:
从(1,8)到(8,1)这条斜线上的各点都有x+y=9;
从(1,7)到(7,1)这条斜线上的各点都有x+y=8;
……
从右上到左下的斜线上的点有这样的特点:
x+y=定值(从2到16)。
所以检查斜线时只要检查是否有x+y或x-y相等的情况就行了,也就是只要检查是否有行号与列号的和、行号与列号的差相等的情况就行了。
这里指定的是8个皇后,所以可以通过8重循环来进行检查,完全符合条件的则输出结果,但这不是回溯的方法而是穷举的方法,如果不指定皇后的个数(棋盘格子数)时这种方法就不行了。
另外用穷举的方法时每对一个皇后放置一个位置时还要用循环来将其原来放的位置清除掉,程序复杂些重复代码多,以放置4个皇后的情况为例,程序如下:
constn=4;
typestack=array[1..n]ofinteger;
vari1,i2,i3,i4:
integer;{i1,i2,i3,i4分别用来表示4个皇后放置的位置,如果是8个皇后就要8个变量来存放,N个皇后当皇后数量不固定时就没有办法了}
s:
stack;
functioncheck:
boolean;
vari,j:
integer;
begin
fori:
=1ton-1do
forj:
=i+1tondo
if(s[i]=s[j])or(s[i]-i=s[j]-j)or(s[i]+i=s[j]+j)
thenbegincheck:
=false;exitend;
check:
=true
end;
procedureprint;
vari:
integer;
begin
fori:
=1tondowrite(s[i]:
2);
writeln
end;
begin
fori1:
=1tondo
fori2:
=1tondo
fori3:
=1tondo
fori4:
=1tondo
begin
s[1]:
=i1;s[2]:
=i2;s[3]:
=i3;s[4]:
=i4;
ifcheckthenprint;
end;
end.
这里定义了一个检测函数check(n),用来检测当前放在n位置的皇后跟不跟其它的有冲突。
我们针对检测的情况作这样的处理:
如果没有冲突则继续放下一个,直到放完N个;如果有冲突则换下一位置进行放置再进行检测。
在考虑下一位置时会有这种情况:
已经到了最后一个位置了没有下一个位置了,这说明这一步这个皇后无论怎样放都跟前面的皇后有冲突,不好放,因此要对前一步的皇后放的位置进行调整(换下一个位置)。
这就回到了上一步,也就是回溯了。
[程序清单]:
programexample14_3;
constn=8;{假设有8个皇后,当皇后数变化时只在改这里的值就行了}
typestack=array[1..n]ofinteger;
vari:
integer;s:
stack;
functioncheck(n:
integer):
boolean;
vari,j:
integer;
begin
fori:
=1ton-1do
forj:
=i+1tondo
if(s[i]=s[j])or(s[i]-i=s[j]-j)or(s[i]+i=s[j]+j){有冲突的情况}
thenbegincheck:
=false;exitend;
check:
=true
end;
procedureprint;
vari:
integer;
begin
fori:
=1tondowrite(s[i]:
2);write('':
4);
end;
begin
fori:
=1tondos[i]:
=0;
i:
=1;
whilei>0do
begin
s[i]:
=s[i]+1;
ifs[i]>nthen
begins[i]:
=0;i:
=i-1end{如果这一步已无地方可放皇后则回到上一步I-1}
else
ifcheck(i)then{如果这个位置可以放置}
ifi=nthenprint{已放全所有的皇后}
elsei:
=i+1{没放全所有的皇后继续放下一个}
end;
writeln;
end.
[运行示例]:
15863724168374251746825317582463
24683175257138642574186326174835
26831475273685142758146328613574
31758246352817463528647135714286
35841726362581743627148536275184
36418572364285713681475236815724
36824175372851463728641538471625
41582736415863724258613742736815
42736851427518634285713642861357
46152837468271354683175247185263
47382516475261384753168248136275
48157263485317265146827351842736
51863724524683175247386152617483
52814736531682475317286453847162
57138642571428635724813657263148
57263184574138625841362758417263
61528374627135846271485363175824
63184275631852476357142863581427
63724815637285146374182564158273
64285713647135286471825368241753
71386425724185367263148573168524
73825164742581367428613575316824
82417536825317468316257484136275
[提高与思考1]:
上面输出的共有92组解,仔细观察它们会发现这些解中有许多是对称的、本质上来说是相同的,如最后一个解84136275,在前面5开头的解里你一定可以找到这样的解57263148。
再仔细分析,真正不同的解应只有19个,其它的都可以通过依次旋转90度后得到。
不用check(n)函数也可以实现程序,直接用三个数组来存储有无冲突,a存储列的情况,b、c分别存储两个斜线的情况,程序如下:
programexample14_3b;{8queens}
constn=8;
typestack=array[1..n]ofinteger;
vari:
integer;
s:
stack;
a,b,c:
array[-n..2*n]ofboolean;
procedureprint;
vari:
integer;
begin
fori:
=1tondowrite(s[i]:
2);write('':
4);
end;
begin
fori:
=-nto2*ndo
begina[i]:
=true;b[i]:
=true;c[i]:
=trueend;
fori:
=1tondos[i]:
=0;
i:
=1;
whilei>0
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 搜索 回溯 算法 讲解