簡單深度優先搜索問題
搜索與回溯是計算機解題中常用的算法,很多問題無法根據某種確定的計算法則來求解,可以利用搜索與回溯的技術求解。回溯是搜索算法中的一種控制策略。它的基本思想是:為了求得問題的解,先選擇某一種可能情況向前探索,在探索過程中,一旦發現原來的選擇是錯誤的,就退回一步重新選擇,繼續向前探索,如此反復進行,直至得到解或證明無解。
如迷宮問題:進入迷宮后,先隨意選擇一個前進方向,一步步向前試探前進,如果碰到死胡同,說明前進方向已無路可走,這時,首先看其它方向是否還有路可走,如果有路可走,則沿該方向再向前試探;如果已無路可走,則返回一步,再看其它方向是否還有路可走;如果有路可走,則沿該方向再向前試探。按此原則不斷搜索回溯再搜索,直到找到新的出路或從原路返回入口處無解為止。
遞歸回溯法算法框架

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.