搜索与回溯算法(一)
简单深度优先搜索问题
搜索与回溯是计算机解题中常用的算法,很多问题无法根据某种确定的计算法则来求解,可以利用搜索与回溯的技术求解。回溯是搜索算法中的一种控制策略。它的基本思想是:为了求得问题的解,先选择某一种可能情况向前探索,在探索过程中,一旦发现原来的选择是错误的,就退回一步重新选择,继续向前探索,如此反复进行,直至得到解或证明无解。
如迷宫问题:进入迷宫后,先随意选择一个前进方向,一步步向前试探前进,如果碰到死胡同,说明前进方向已无路可走,这时,首先看其它方向是否还有路可走,如果有路可走,则沿该方向再向前试探;如果已无路可走,则返回一步,再看其它方向是否还有路可走;如果有路可走,则沿该方向再向前试探。按此原则不断搜索回溯再搜索,直到找到新的出路或从原路返回入口处无解为止。
递归回溯法算法框架
1 procedure Search(k:integer); 2 begin 3 for i:=1 to 算符种数 Do 4 if 满足条件 then 5 begin 6 保存结果 7 if 到目的地 then 输出解 8 else Search(k+1); 9 恢复:保存结果之前的状态{回溯一步} 10 end; 11 end;
1 procedure Search(k:integer); 2 begin 3 if 到目的地 then 输出解 4 else 5 for i:=1 to 算符种数 Do 6 if 满足条件 then 7 begin 8 保存结果 9 Search(k+1,参数表); 10 恢复:保存结果之前的状态 11 end; 12 end;
1 procedure Search(k:integer); 2 begin 3 if 到目的地 then 4 begin 5 输出解; 6 exit; 7 end; 8 for i:=1 to 算符种数 Do 9 if 满足条件 then 10 begin 11 保存结果 12 Search(k+1,参数表); 13 恢复:保存结果之前的状态 14 end; 15 end;
【例1】求从1~4中任意挑出3个数的所有组合方案。
1 For i:=1 to 4 do 2 For j:=i+1 to 4 do 3 For k:=j+1 to 4 do writeln(i,j,k);
1 Procedure DFS(k:integer); 2 Var i:integer; 3 Begin 4 If k>3 then begin print(Ans);exit;end; 5 For i:=1 to 4 do 6 if i>Ans[k-1] then 7 begin Ans[k]:=I; DFS(k+1);end; 8 End;
【例2】求从1~N中任意挑出m个数的所有组合方案。
1 Procedure DFS(k:integer); 2 Var i:integer; 3 Begin 4 If k>M then begin print(Ans);exit;end; 5 For i:=1 to N do 6 if i>Ans[k-1] then 7 begin Ans[k]:=I; DFS(k+1);end; 8 End;
1 type arr=array[0..50] of integer; 2 var n,m,i:integer; 3 ans:arr; 4 procedure print(a:arr); 5 begin 6 for i:=1 to m do write(ans[i]); writeln; 7 end; 8 procedure dfs(k:integer); 9 var i:integer; 10 begin 11 if k>m then begin print(ans);exit;end; 12 for i:=ans[k-1]+1 to n do 13 begin ans[k]:=I; dfs(k+1); end; 14 end; 15 begin 16 readln(n,m); 17 dfs(1); 18 end.
【例3】求1,2,3三个数的全排列并输出。
1 For i1:=1 to 3 do 2 For i2:=1 to 3 do 3 For i3:=1 to 3 do 4 If (i1<>i2) and (i2<>i3)and(i3<>i1) 5 then writeln(i1,i2,i3);
1 For i1:=1 to 3 do 2 For i2:=1 to 3 do 3 If i2<>i1 then 4 For i3:=1 to 3 do 5 If (i2<>i3) and (i1<>i3) then 6 writeln(i1,i2,i3);
1 S:=[]; 2 For i1:=1 to 3 do begin 3 S:= S + [i1]; 4 For i2:=1 to 3 do 5 If not (i2 in S) then begin 6 S:= S + [i2]; 7 For i3:=1 to 3 do 8 If not(i3 in S) then writeln(i1,i2,i3); 9 S:=S-[i2]; 10 End; 11 S:=S-[i1]; 12 End;
1 Procedure DFS(k:integer); 2 Var i:integer; 3 Begin 4 If k>3 then begin print(Ans);exit;end; 5 For i:=1 to 3 do 6 If not(i in S) then begin 7 S:=S+[i]; 8 Ans[k]:=i; 9 DFS(k+1); 10 S:=S-[i]; 11 End; 12 End;
【例4】求从1~N中任意挑出m个数的所有排列方案。
1 Procedure DFS(k:integer); 2 Var i:integer; 3 Begin 4 If k>M then begin print(Ans);exit;end; 5 For i:=1 to N do 6 If not(i in S) then begin 7 S:=S+[i]; 8 Ans[k]:=i; 9 DFS(k+1); 10 S:=S-[i]; 11 End; 12 End;
1 Procedure DFS(s:se;k:integer); 2 Var i:integer; 3 Begin 4 If k>M then begin print(Ans);exit;end; 5 For i:=1 to N do 6 If not(i in S) then begin 7 Ans[k]:=i; 8 DFS(s+[i],k+1); 9 End; 10 End;
由上边的程序代码,我们可以看出深搜算法其实也是枚举,只不过采用了递归结构使枚举的层数可以更多,代码结构更清晰罢了。
注意:由于递归结构在pascal里使用的是系统栈,空间有限,所以尽量不要在子程序里定义太多变量甚至数组,否则递归层数多了容易造成系统栈溢出,建议能定义成全局变量的尽量用全局变量,如果使用了全局变量,注意递归后要恢复保存前的状态。
【例5】八皇后问题:要在国际象棋棋盘中放八个皇后,使任意两个皇后都不能互相吃。(提示:皇后能吃同一行、同一列、同一对角线的任意棋子)
放置第i个(行)皇后的算法为:
1 procedure Search(i); 2 begin 3 for 第i个皇后的位置=1 to 8 do; //在本行的8列中去试 4 if 本行本列允许放置皇后 then 5 begin 6 放置第i个皇后; 7 对放置皇后的位置进行标记; 8 if i=8 then 输出 //已经放完个皇后 9 else Search(i+1); //放置第i+1个皇后 10 对放置皇后的位置释放标记,尝试下一个位置是否可行; 11 end; 12 end;
【算法分析】
显然问题的关键在于如何判定某个皇后所在的行、列、斜线上是否有别的皇后;
可以从矩阵的特点上找到规律,如果在同一行,则行号相同;
如果在同一列上,则列号相同;
如果同在/ 斜线上的行列值之和相同;
如果同在\ 斜线上的行列值之差相同;
从下图可验证:
考虑每行有且仅有一个皇后,设一维数组A[1..8]表示皇后的放置:第i行皇后放在第j列,用A[i]=j来表示,即下标是行数,内容是列数。例如:A[3]=5就表示第3个皇后在第3行第5列上。
判断皇后是否安全,即检查同一列、同一对角线是否已有皇后,建立标志数组b[1..8]控制同一列只能有一个皇后,若两皇后在同一对角线上,则其行列坐标之和或行列坐标之差相等,故亦可建立标志数组c[1..16]、d[-7..7]控制同一对角线上只能有一个皇后。
如果斜线不分方向,则同一斜线上两皇后的行号之差的绝对值与列号之差的绝对值相同。在这种方式下,要表示两个皇后I和J不在同一列或斜线上的条件可以描述为:A[I]<>A[J] AND ABS(I-J)<>ABS(A[I]-A[J]){I和J分别表示两个皇后的行号}
1 program ex5_4; 2 var a:array[1..8] of integer; 3 b:array[1..8] of boolean; 4 c:array[1..16] of boolean; 5 d:array[-7..7] of boolean; 6 sum:integer; 7 procedure print; 8 var i:integer; 9 begin 10 inc(sum); writeln(' sum=',sum); //方案数累加1 11 for i:=1 to 8 do write(a[i]:4); //输出一种方案 12 end; 13 procedure Search(t:integer); 14 var j:integer; 15 begin 16 for j:=1 to 8 do //每个皇后都有8位置(列)可以试放 17 if b[j] and c[t+j] and d[t-j] then //寻找放置皇后的位置 18 begin //放置皇后,建立相应标志值 19 a[t]:=j; //摆放皇后 20 b[j]:=false; //宣布占领第j列 21 c[t+j]:=false; d[t-j]:=false; //占领两个对角线 22 if t=8 then print //8个皇后都放置好,输出 23 else Search(t+1); //继续递归放置下一个皇后 24 b[j]:=true; //递归返回即为回溯一步,当前皇后退出 25 c[t+j]:=true; d[t-j]:=true; 26 end; 27 end; 28 BEGIN 29 fillchar(b,sizeof(b),#1); //数组b、c、d初始化,赋初值True 30 fillchar(c,sizeof(c),#1); 31 fillchar(d,sizeof(d),#1); 32 sum:=0; //用于统计方案数 33 Search(1); //从第1个皇后开始放置 34 END.
【例】任何一个大于1的自然数n,总可以拆分成若干个小于n的自然数之和。
当n=7共14种拆分方法:
7=1+1+1+1+1+1+1
7=1+1+1+1+1+2
7=1+1+1+1+3
7=1+1+1+2+2
7=1+1+1+4
7=1+1+2+3
7=1+1+5
7=1+2+2+2
7=1+2+4
7=1+3+3
7=1+6
7=2+2+3
7=2+5
7=3+4
total=14
1 【参考程序】 2 program ex5_3; 3 var a:array[0..100]of integer; 4 n,t,total:integer; 5 procedure print(t:integer); 6 var i:integer; 7 begin 8 write(n,'='); 9 for i:=1 to t-1 do write(a[i],'+'); 10 //输出一种拆分方案 11 writeln(a[t]); 12 total:=total+1; 13 //方案数累加1 14 end; 15 procedure Search(s,t:integer); 16 var i:integer; 17 begin 18 for i:=1 to s do 19 if (a[t-1]<=i)and(i<n) then 20 //当前数i要大于等于前1位数, 21 且不过n 22 begin 23 a[t]:=i; 24 //保存当前拆分的数i 25 s:=s-a[t]; 26 //s减去数i, s的值将继续拆分 27 if s=0 then print(t) 28 //当s=0时,拆分结束输出结果 29 else Search(s,t+1); 30 //当s>0时,继续递归 31 s:=s+a[t]; 32 //回溯:加上拆分的数, 33 以便产分所有可能的拆分 34 end; 35 end; 36 BEGIN 37 readln(n); 38 Search(n,1); 39 //将要拆分的数n传递给s 40 writeln('total=',total); 41 //输出拆分的方案数 42 readln; 43 END.