回溯算法入门及应用.docx
- 文档编号:3270716
- 上传时间:2022-11-21
- 格式:DOCX
- 页数:15
- 大小:54.58KB
回溯算法入门及应用.docx
《回溯算法入门及应用.docx》由会员分享,可在线阅读,更多相关《回溯算法入门及应用.docx(15页珍藏版)》请在冰豆网上搜索。
回溯算法入门及应用
回溯算法入门及应用
广东省东莞市东华高级中学杨光文难易指数:
★★★
在求解一些问题(如马的遍历、选书、八皇后问题、自然数的拆分等问题)时,题目的要求可能是求出原问题的一种或所有可能的解决方案。
这类问题的解往往是由一个一个的步骤或状态所构成的,每一步骤又有若干种可能的决策方案;由于没有固定、明确的数学解析方法,往往要采用搜索的做法,即从某一个初始状态出发,不断地向前(即下一个状态)搜索,以期最终达到目标状态,从而得到原问题的一个解或所有的解。
在搜索的过程中,由于问题本身及所采取的搜索方法的特点(如在缺乏全局及足够的前瞻信息的情况下进行搜索等),会导致走到某一状态就走不下去的情况,这时,就必须回头(即回到上一步,而不是回到最初的状态),再尝试其他的可能性,换一个方向或方法再试试。
这样,不断地向前探索、回溯,再向前、再回溯,直至最终得出问题的解,或者一路回溯到出发点(出现这种情况即表示原问题无解)。
注意,这种搜索过程并不是尝试搜索问题解空间中所有的可能状态和路径,而是采用深度优先的方式,沿着一条路径,尽可能深入地向前探索,这就是回溯算法。
一、回溯算法的定义:
回溯算法也叫试探法,它是一种系统地搜索问题的解的方法。
回溯算法的基本思想是:
从一条路往前走,能进则进,不能进则退回来,换一条路再试。
用回溯算法解决问题的一般步骤为:
1、定义一个解空间,它包含问题的解。
2、利用适于搜索的方法组织解空间。
3、利用深度优先法搜索解空间。
4、利用限界函数避免移动到不可能产生解的子空间。
问题的解空间通常是在搜索问题的解的过程中动态产生的,这是回溯算法的一个重要特性。
下面通过一个具体实例加深大家对回溯算法的认识。
二、回溯算法的应用举例:
例1:
马的遍历
中国象棋半张棋盘如图1(a)所示。
马自左下角往向右上角跳。
规定只许往右跳,不许往左跳,马只能走日字。
比如图1(a)所示为一种跳行路线,并将所经路线打印出来,打印格式为:
0,0->2,1->3,3->1,4->3,5->2,7->4,8。
找出所有路径。
【算法分析】
如图1(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:
打印路径。
【算法设计】
proceduretry(i:
integer);{搜索}
varj:
integer;
begin
forj:
=1to4do{试遍4个方向}
if新坐标满足条件then
begin
记录新坐标;
if到达目的地thenprint{统计方案,输出结果}
elsetry(i+1);{试探下一步}
退回到上一个坐标,即回溯;
end;
end;
【参考程序】
programexam1;
constx:
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
a[1,1]:
=0;a[1,2]:
=0;
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
try
(2);
end.
例2:
选书
学校放寒假时,信息学竞赛辅导老师有A,B,C,D,E五本书,要分给参加培训的张、王、刘、孙、李五位同学,每人只能选一本书。
老师事先让每个人将自己喜欢的书填写在如下的表格中。
然后根据他们填写的表来分配书本,希望设计一个程序帮助老师求出所有可能的分配方案,使每个学生都满意。
输出结果:
zhang:
C
wang:
A
liu:
B
sun:
D
li:
E
【算法分析】
可用穷举法,先不考虑“每人都满意”这一条件,这样只剩“每人选一本且只能选一本”这一条件。
在这个条件下,可行解就是五本书的所有全排列,一共有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个数
ifi=5then打印一个解
elsetry(i+1);
删去第i个数字
end;
end;
end;
【参考程序】
programexam2;
constlike:
array[1..5,1..5]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[1..5]ofstring[6]=('zhang','wang','liu','sun','li');
varbook:
array[1..5]of0..5;
flag:
setof1..5;
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.
例3:
八皇后问题
【问题描述】
在一个8×8的棋盘里放置8个皇后,要求每个皇后两两之间不相"冲"(在每一横列竖列斜列只有一个皇后)。
【问题分析】
这道题可以用回溯算法来做,分别一一测试每一种摆法,直到得出正确的答案。
主要解决以下几个问题:
1、冲突。
包括行、列、两条对角线:
(1)列:
规定每一列放一个皇后,不会造成列上的冲突;
(2)行:
当第I行被某个皇后占领后,则同一行上的所有空格都不能再放皇后,要把以I为下标的标记置为被占领状态;
(3)对角线:
对角线有两个方向。
在同一对角线上的所有点(设下标为(i,j)),要么(i+j)是常数,要么(i-j)是常数。
因此,当第I个皇后占领了第J列后,要同时把以(i+j)、(i-j)为下标的标记置为被占领状态。
2、数据结构。
(1)解数组A。
A[I]表示第I个皇后放置的列;范围:
1..8
(2)行冲突标记数组B。
B[I]=0表示第I行空闲;B[I]=1表示第I行被占领;范围:
1..8
(3)对角线冲突标记数组C、D。
C[I-J]=0表示第(I-J)条对角线空闲;C[I-J]=1表示第(I-J)条对角线被占领;范围:
-7..7
D[I+J]=0表示第(I+J)条对角线空闲;D[I+J]=1表示第(I+J)条对角线被占领;范围:
2..16
【算法流程】
1、数据初始化。
2、从n列开始摆放第n个皇后(因为这样便可以符合每一竖列一个皇后的要求),先测试当前位置(n,m)是否等于0(未被占领),如果是,摆放第n个皇后,并宣布占领(记得要横列竖列斜列一起来哦),接着进行递归;如果不是,测试下一个位置(n,m+1),但是如果当n<=8,m=8时,却发现此时已经无法摆放时,便要进行回溯。
3、当n>8时,便一一打印出结果。
【参考程序】
programexam3;
vara:
array[1..8]ofinteger;
b,c,d:
array[-7..16]ofinteger;
t,i,j,k:
integer;
procedureprint;
begin
t:
=t+1;
write(t,'');
fork:
=1to8dowrite(a[k],'');
writeln;
end;
proceduretry(i:
integer);
varj:
integer;
begin
forj:
=1to8do{每个皇后都有8种可能位置}
if(b[j]=0)and(c[i+j]=0)and(d[i-j]=0)then{判断位置是否冲突}
begin
a[i]:
=j;{摆放皇后}
b[j]:
=1;{宣布占领第J行}
c[i+j]:
=1;{占领两个对角线}
d[i-j]:
=1;
ifi<8thentry(i+1){8个皇后没有摆完,递归摆放下一皇后}
elseprint;{完成任务,打印结果}
b[j]:
=0;{回溯}
c[i+j]:
=0;
d[i-j]:
=0;
end;
end;
begin
fork:
=-7to16do{数据初始化}
begin
b[k]:
=0;
c[k]:
=0;
d[k]:
=0;
end;
try
(1);{从第1个皇后开始放置}
end.
例4、自然数拆分
【问题描述】输入自然数n,然后将其拆分成若干数相加的形式,参与加法运算的数可以重复。
输入:
待拆分的自然数n
输出:
若干数的加法式子
【样例输入】
5
【样例输出】
5=1+1+1+1+1
5=1+1+1+2
5=1+1+3
5=1+2+2
5=1+4
5=2+3
【问题分析】
算法分析:
等式中后一个数必须大于等于前一个数,因为这个可以1、避免重复2提高效率我们用一个数组a[i]来记录拆分的数字,用b[i]记录剩下的数字。
K记录第几个拆分的数字。
每次拆分都可以把a[i]都打印出来。
把剩下的数字b[i]在进行拆分,并且是从i开始拆分的。
Find(b[i],i,k+1)
programcf;
vara,b:
array[1..100]ofinteger;
n,i:
integer;
procedurefind(start,m,k:
integer);
{从start开始,对m进行拆分,拆分是第k个数}
vari,j:
integer;
begin
fori:
=startto(mdiv2)do{只要从start到m的一个半,可以避免重复}
begin
write(n,'=');
a[k]:
=i;b[k]:
=m-i;
forj:
=1tokdowrite(a[j],'+');
writeln(b[k]);
find(i,b[k],k+1);
end;
end;
begin
assign(input,'word.in');
assign(output,'word.out');
reset(input);
rewrite(output);
readln(n);
find(1,n,1);
close(input);
close(output);
end.
解法二:
针对所给问题,定义问题的解空间;如本题对5的拆分来说,1<=拆分的数<=5。
确定易于搜索的解空间结构;如本题对5的拆分来说,用k[i]数组来存储解,每个数组元素的取值范围都是1<=拆分的数<=5,从1开始搜索直到5。
搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
如本题对5的拆分来说,为了避免重复,拆分出的加数要求满足k[1]+k[2]+…+k[i]=n且k[1]≤k[2]≤…≤k[i]。
programexam4;
var
k:
array[1..100]oflongint;
n:
longint;
procedureprint(x:
longint);//输出
var
i:
longint;
begin
ifx=1thenexit;//判断是否存在n=n的情况
write(n,'=');
fori:
=1tox-1do
write(k[i],'+');
writeln(k[x]);
end;
proceduretry(x,y,num:
longint);//回溯搜索
var
i:
longint;
begin
ify=0thenbeginprint(num-1);exit;end;
fori:
=xtoydo//非递减搜索
begin
if(y=i)or(i<=y*2)then//如果y=3,x=2,那么后面是不可能
begin
k[num]:
=i;
try(i,y-i,num+1);
end;
end;
end;
begin
readln(n);
try(1,n,1);
end.
例5、售货员的难题
【问题描述】
某乡有n个村庄(1 为了提高效率,他从商店出发到每个村庄一次,然后返回商店所在的村,假设商店所在的村庄为1,他不知道选择什么样的路线才能使所走的路程最短。 请你帮他选择一条最短的路。 【输入】 村庄数n和各村之间的路程(均是整数)。 【输出】 最短的路程。 【样例】 salesman.insalesman.out 3{村庄数}3 021{村庄1到各村的路程} 102{村庄2到各村的路程} 210{村庄3到各村的路程} 【问题分析】 题目给定的村庄数不多(≤40),所以可以用回溯的方法,从起点(第一个村庄)出发找出所有经过其他所有村庄的回路,计算其中的最短路程。 当村庄数n比较大时这种方法就不太适用了。 用一个过程road(step,line: byte)来描述走的状况,其中step是当前已到村庄数、line是当前所在的村庄。 如果step=n,下面只能回起点了,直接看第line个村庄到第一个村庄的路程加上已走的总路程,如果比最小值还小则替换最小值(要保存路径的话也可保存,这是回溯算法的优点,考虑到达最小值的路径可能不止一条,不便于测试,题目没要求输出路径)。 如果step还小于n,那么将还没有到过的村庄一个一个地试过去,再调用下一步road(step+1,新到的村庄号)。 Programexam5; var i,j,n,ans: longint; map: array[1..41,1..41]oflongint; f: array[1..41]ofboolean; proceduredfs(t,x,tot: longint); var i: longint; begin ift=nthen begin iftot+map[x,1] =tot+map[x,1]; exit; end; fori: =2tondo iff[i]then begin iftot+map[x,i] begin f[i]: =false; dfs(t+1,i,tot+map[x,i]); f[i]: =true; end; end; end; begin readln(n); fillchar(f,sizeof(f),true); fori: =1tondo forj: =1tondo read(map[i,j]); ans: =maxlongint; dfs(1,1,0); writeln(ans); end. 另一参考程序: vari,j,n,min: integer; a: array[1..40,1..40]ofinteger;//储存图 v: array[1..40]ofboolean;//判断该点是否访问过 proceduredfs(k,x,m: longint);//回溯 vari: byte; begin ifk=nthen//到达终点 ifm+a[x,1] =m+a[x,1] else else fori: =2tondo if(a[x,i]>0)andv[i]then//当前点未访问 begin v[i]: =false; ifm+a[x,i] dfs(k+1,i,m+a[x,i]);//回溯 v[i]: =true; end;{IF} end;{dfs} begin{main} assign(input,'salesma.in'); reset(input); assign(output,'salesma.out'); rewrite(output); fillchar(v,sizeof(v),true); readln(n); fori: =1tondo forj: =1tondo read(a[i,j]); min: =maxint; v[1]: =false; dfs(1,1,0);//回溯 writeln(min);//输出最小值 close(input); close(output); end. 三、总结: 在回溯算法中,把握好何时发生回溯(即发生回溯的条件)是非常关键的,应注意有效而正确地表达回溯条件。 还要注意,原问题的状态空间、每一步可能的行动方案、记录已走过的所有步骤的数据结构等,都是非常关键的。 回溯策略经常出现在搜索求解的应用中,通用采用递归算法实现,在程序实现时每一层递归循环变量的初始值和终值的确定是最重要的。 一般我们可以根据题意,通过思考,利用常规的解决方法找出每一层循环的变化规律,然后通过对问题的剖析,必要时可以借助数学的方法对算法进行优化,减少时间复杂度,用最短的时间来满足不同的输入需求。
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 回溯 算法 入门 应用