回溯算法入门及应用Word文档下载推荐.docx
《回溯算法入门及应用Word文档下载推荐.docx》由会员分享,可在线阅读,更多相关《回溯算法入门及应用Word文档下载推荐.docx(15页珍藏版)》请在冰豆网上搜索。
3:
(i,j)→(i-1,j+2);
(i>
0,j<
4:
(i,j)→(i-2,j+1);
1,j<
搜索策略:
S1:
A[1]:
=(0,0);
S2:
从A[1]出发,按移动规则依次选定某个方向,如果达到的是(4,8)则转向S3,否则继续搜索下
一个到达的顶点;
S3:
打印路径。
【算法设计】
proceduretry(i:
integer);
{搜索}
varj:
integer;
begin
forj:
=1to4do{试遍4个方向}
if新坐标满足条件then
记录新坐标;
if到达目的地thenprint{统计方案,输出结果}
elsetry(i+1);
{试探下一步}
退回到上一个坐标,即回溯;
end;
【参考程序】
programexam1;
constx:
array[1..4,1..2]ofinteger=((2,1),(1,2),(-1,2),(-2,1));
{四种移动规则}
vart:
{路径总数}
a:
array[1..9,1..2]ofinteger;
{路径}
procedureprint(ii:
{打印}
vari:
begin
inc(t);
fori:
=1toii-1do
write(a[i,1],'
'
a[i,2],'
->
'
);
writeln('
4,8'
t:
5);
end;
varj:
a[1,1]:
=0;
a[1,2]:
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
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,2]:
=0
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);
=1to5do
if第i个同学分给第j本书符合条件then
记录第i个数
ifi=5then打印一个解
删去第i个数字
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:
procedureprint;
inc(c);
writeln('
answer'
c,'
:
writeln(name[i]:
10,'
chr(64+book[i]));
ifnot(jinflag)and(like[i,j]>
0)then
=flag+[j];
book[i]:
=j;
ifi=5thenprint
=flag-[j];
=[];
try
(1);
readln
例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行被占领;
(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:
t:
=t+1;
write(t,'
'
fork:
=1to8dowrite(a[k],'
writeln;
=1to8do{每个皇后都有8种可能位置}
if(b[j]=0)and(c[i+j]=0)and(d[i-j]=0)then{判断位置是否冲突}
a[i]:
{摆放皇后}
b[j]:
=1;
{宣布占领第J行}
c[i+j]:
{占领两个对角线}
d[i-j]:
ifi<
8thentry(i+1){8个皇后没有摆完,递归摆放下一皇后}
elseprint;
{完成任务,打印结果}
{回溯}
=-7to16do{数据初始化}
b[k]:
c[k]:
d[k]:
{从第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:
procedurefind(start,m,k:
{从start开始,对m进行拆分,拆分是第k个数}
vari,j:
=startto(mdiv2)do{只要从start到m的一个半,可以避免重复}
write(n,'
='
a[k]:
=i;
b[k]:
=m-i;
=1tokdowrite(a[j],'
+'
writeln(b[k]);
find(i,b[k],k+1);
assign(input,'
word.in'
assign(output,'
word.out'
reset(input);
rewrite(output);
readln(n);
find(1,n,1);
close(input);
close(output);
解法二:
针对所给问题,定义问题的解空间;
如本题对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);
//输出
i:
ifx=1thenexit;
//判断是否存在n=n的情况
=1tox-1do
write(k[i],'
writeln(k[x]);
proceduretry(x,y,num:
//回溯搜索
ify=0thenbeginprint(num-1);
exit;
=xtoydo//非递减搜索
if(y=i)or(i<
=y*2)then//如果y=3,x=2,那么后面是不可能
k[num]:
try(i,y-i,num+1);
readln(n);
try(1,n,1);
例5、售货员的难题
某乡有n个村庄(1<
n<
=40),有一个售货员,他要到各个村庄去售货,各村庄之间的路程s(0<
s<
1000)是已知的,且A村到B村与B村到A村的路大多不同。
为了提高效率,他从商店出发到每个村庄一次,然后返回商店所在的村,假设商店所在的村庄为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;
i,j,n,ans:
map:
array[1..41,1..41]oflongint;
f:
array[1..41]ofboolean;
proceduredfs(t,x,tot:
ift=nthen
iftot+map[x,1]<
ansthenans:
=tot+map[x,1];
exit;
=2tondo
iff[i]then
iftot+map[x,i]<
ansthen
begin
f[i]:
=false;
dfs(t+1,i,tot+map[x,i]);
=true;
fillchar(f,sizeof(f),true);
=1tondo
=1tondo
read(map[i,j]);
ans:
=maxlongint;
dfs(1,1,0);
writeln(ans);
另一参考程序:
vari,j,n,min:
array[1..40,1..40]ofinteger;
//储存图
v:
array[1..40]ofboolean;
//判断该点是否访问过
proceduredfs(k,x,m:
//回溯
vari:
byte;
ifk=nthen//到达终点
ifm+a[x,1]<
minthenmin:
=m+a[x,1]
else
else
if(a[x,i]>
0)andv[i]then//当前点未访问
v[i]:
ifm+a[x,i]<
minthen//最优剪枝
dfs(k+1,i,m+a[x,i]);
{IF}
{dfs}
begin{main}
salesma.in'
salesma.out'
fillchar(v,sizeof(v),true);
fori:
read(a[i,j]);
min:
=maxint;
v[1]:
dfs(1,1,0);
writeln(min);
//输出最小值
三、总结:
在回溯算法中,把握好何时发生回溯(即发生回溯的条件)是非常关键的,应注意有效而正确地表达回溯条件。
还要注意,原问题的状态空间、每一步可能的行动方案、记录已走过的所有步骤的数据结构等,都是非常关键的。
回溯策略经常出现在搜索求解的应用中,通用采用递归算法实现,在程序实现时每一层递归循环变量的初始值和终值的确定是最重要的。
一般我们可以根据题意,通过思考,利用常规的解决方法找出每一层循环的变化规律,然后通过对问题的剖析,必要时可以借助数学的方法对算法进行优化,减少时间复杂度,用最短的时间来满足不同的输入需求。