這次練習主要針對的是“回溯法”
簡單介紹一下,回溯法->深度優先搜索算法->dfs(Depth First Search)
所以個人習慣上都是對於任何需要回溯的問題,其函數命名為dfs。
深度優先搜索,本質上是對一顆搜索樹進行搜索。
相較於BFS來說,DFS搜索順序為“找到一個節點一直搜索到葉子結點,到了葉子再回頭”
對於BFS順序為:A,B,C,D,E,F,G……
對於DFS順序為:A,B,D,H,D,I,D,B,E,J,E,K,E,B,A……
BFS和DFS兩者之間:
1、dfs相較於bfs比較方便好使,因為過程不借助隊列
2、如果同一個問題用bfs和dfs都能實現情況下,通常用bfs,
原因有:搜索空間一定的情況下,dfs比bfs多了一步回溯的操作。
例如迷宮問題明顯時BFS優勢大。
對於期末考試考查以下三種能力
1、子集樹(01背包問題,幻方數)
2、排列樹(TSP問題)
3、類多叉樹遍歷(李白打酒,n皇后)
熱身環節
子集樹
算法本質
顧名思義,利用集合的思路進行操作。
集合有三種性質,(無序性,互異性,確定性)
在求解過程中,利用更多的是“互異性” (即集合中不存在兩個相同的元素)。
引入問題
01背包
問題描述:
對於給定一個背包,其背包有一定的承重能力,給出有n個物品,物品以<value,weight>形式出現。
請問在背包承重范圍內,實現背包的最大價值。
題解:
假定 每個物品“取和不取” => 取 <-> '1' 不取 <-> '0'
然后對於3個物品的背包問題共有2^3=8種情況,如下所示
(000) -> {}
(001) -> {1}
(010) -> {2}
(011) -> {1,2}
(100) -> {3}
(101) -> {1,3}
(110) -> {2,3}
(111) -> {1,2,3}
由於集合的互異性,每次在物品放與不放時,都需要判斷當前集合中是否存在物品。
我們需要借助一個標記數組來進行操作
標記數組名可以為“vis(visit)” , "book" , "st(state)" , "used"
問題轉化為:構建一顆搜索樹,高度為n層,每一個節點都表示背包狀態,
同時每一層都是表示物品放和不放,若走左子樹<->物品放,否則走右子樹<->物品不放。
答案即為:所有葉子結點的最小值.

1 //dfs子集樹解決01背包問題 2 #include<cstdio> 3 #include<algorithm> 4 using namespace std; 5 const int N = 5; 6 7 int value[] = { 45 , 25 , 25 }; 8 int weight[] = { 16 , 15 , 15 }; 9 int V = 30 ; 10 11 int n = 3 ; //物品數量 12 int ans ; //答案 13 int val , w ; //每個結點中<value,weight>的狀態 14 int vis[N] ; //物品標記狀態 15 16 void dfs( int step ){ 17 18 //到達葉子結點 19 if( step == n ){ 20 ans = max( ans , val ); 21 return ; 22 } 23 24 //物品不在背包中,且放物品后還在背包承受范圍內. 25 if( vis[step] == 0 && w + weight[step] <= V){ 26 //對於第Step個物品進行標記,同時更新結點對應的<val,w>狀態 27 vis[step] = 1 ; 28 w += weight[step] ; 29 val += value[step] ; 30 31 //往左子樹走 32 dfs( step + 1 ); 33 34 //回溯,返回結點后需要把第step個物品取出來, 35 //同時恢復 結點對應的<val,w>狀態 36 val -= value[step] ; 37 w -= weight[step] ; 38 vis[step] = 0 ; 39 } 40 //往右子樹走 41 dfs( step + 1 ); 42 } 43 44 int main() 45 { 46 dfs(0) ; 47 printf("%d\n",ans); 48 return 0 ; 49 }
結點狀態<value,weight>如果用全局變量表示比較復雜。
但如果用函數參數來表示當前結點狀態則清爽很多。
改寫成下面的代碼:

1 //dfs子集樹解決01背包問題 2 #include<cstdio> 3 #include<algorithm> 4 using namespace std; 5 const int N = 5; 6 7 int value[] = { 45 , 25 , 25 }; 8 int weight[] = { 16 , 15 , 15 }; 9 int V = 30 ; 10 11 int n = 3 ; //物品數量 12 int ans ; //答案 13 int vis[N] ; //物品標記狀態 14 15 void dfs( int step , int val , int w ){ 16 17 //到達葉子結點 18 if( step == n ){ 19 ans = max( ans , val ); 20 return ; 21 } 22 23 //物品不在背包中,且放物品后還在背包承受范圍內. 24 if( vis[step] == 0 && w + weight[step] <= V){ 25 vis[step] = 1 ; 26 dfs( step + 1 , val + value[step] , w + weight[step] ); 27 vis[step] = 0 ; 28 } 29 //往右子樹走 30 dfs( step + 1 , val , w ); 31 } 32 33 int main() 34 { 35 dfs(0,0,0) ; 36 printf("%d\n",ans); 37 return 0 ; 38 }
排列樹
算法本質
排列樹顧名思義,還是以排列(permutation)為基礎。
解決的問題是:通常以全排列為基礎的題目。
1,2,3的全排列:
123 , 132 , 213 , 231 , 312 , 321
譬如求解數字排列問題;或者以全排列的基礎的問題。
引入問題
TSP問題
【問題描述】
旅行商問題,即TSP問題(Traveling Salesman Problem)又譯為旅行推銷員問題、貨郎擔問題,是數學領域中著名問題之一。假設有一個旅行商人要拜訪n個城市,他必須選擇所要走的路徑,路徑的限制是每個城市只能拜訪一次。路徑的選擇目標是要求得的路徑路程為所有路徑之中的最小值。
【題解】
TSP問題是一個著名的NP問題,目前沒有找到一個有效的算法,只能通過窮舉方式來算。這里用的窮舉就是用全排列來實現,答案必定是n!種排列中的一種,我們就算出n!取其最小值的那個即可。
算法實現過程主要涉及到:
利用交換實現排列順序中“誰打頭”,然后其余的排列就是在后面的位置進行交換。
大家在草稿紙上自己寫一下:1,2,3,4的全排列
1234,1243,1324,1342,1432,1423……
2***,……
3***,……
4***,……
(共24種情況)
我們在自己寫的過程也是注重‘順序’。
這個順序是針對4個位置,固定前幾個,然后交換后面幾個順序。
其實這個排列樹就是和我們手寫時側重的思路是相符的。
這一過程其實是:固定n個位置,每一個位置放數字,
在一個排列中,通過交換方式實現每個數字都在該位置放置過。同時為了達到不重不漏,下一個位置還是進行相應的操作。
設計函數
設置兩個游標卡住數組的一個范圍 ->指的是對該部分[ start , end ]進行全排列。
當兩個游標指到同一個位置時:也就是到達了葉子結點。
如果還未到葉子結點,此過程必須要進行交換,當前位置其實是:S,交換的下標為[S,E]。
注意: 這里是[S,E],而不是[S+1,E],因為S這個位置本身也是作為一種排列。
舉例:‘1’,2,3,4,此時在第一個位置進行枚舉,若[S+1,E]
就會出現,'2',1,3,4 , '3',2,1,4 , '4',2,3,1 卻少了'1',2,3,4
每一個位置都進行相應的操作,就能實現全排列。
我們所需要的答案就為搜索樹的葉子結點。到達葉子結點時即為一種排列順序。
我們所求的最小值,需要對排列順序中兩個相鄰 利用dis[i][j]求和

1 //dfs子集樹解決TSP問題 2 #include<cstdio> 3 #include<algorithm> 4 using namespace std; 5 const int N = 5; 6 7 //鄰接矩陣 8 int dis[N][N] = { 9 0 , 0 , 0 , 0 , 0 , 10 0 , 0 , 30, 6 , 4 , 11 0 , 30, 0 , 5 , 10, 12 0 , 6 , 5 , 0 , 20, 13 0 , 4 , 10, 20, 0 14 }; 15 //人為設定dis[][]下標是由1開始的,所有dis[0][j] 和 dis[i][0]都為0 16 17 // permutation : 排列方式,排列數 18 int perm[] = { 0 , 1 , 2 , 3 , 4 };//令其下標為1開始 19 int path[N] ; 20 int n = 4 ; //城市個數 21 int ans = 1e6; //答案,初始化一個很大的數 22 23 void dfs( int S , int E ){ // Start , End 24 if( S == E ){ 25 /* 26 顯示所有排列方式 27 for( int i = 1 ; i <= n ; i++ ){ 28 printf("%3d",perm[i],i==n?'\n':' '); 29 } 30 putchar('\n'); 31 */ 32 //計算其該排列順序的代價 33 int tmp = 0 ; 34 for( int i = 2 ; i <= n ; i++ ){ 35 tmp += dis[perm[i]][perm[i-1]]; 36 } 37 //更新其答案,並記錄當前的路徑 38 if( tmp < ans ){ 39 for( int i = 1 ; i <= n ; i++ ){ 40 path[i] = perm[i] ; 41 } 42 ans = tmp ; 43 } 44 return ; 45 } 46 for( int i = S ; i <= E ; i++ ){ 47 swap( perm[S] , perm[i] ); 48 dfs( S + 1 , E ) ; 49 swap( perm[S] , perm[i] ); 50 } 51 } 52 53 int main() 54 { 55 dfs( 1 , 4 ) ; 56 printf("%d\n",ans); 57 for( int i = 1 ; i <= n ; i++ ){ 58 printf("%-2d",path[i]); 59 } 60 putchar('\n'); 61 return 0 ; 62 }
實戰環節
李白打酒
【題目描述】
話說大詩人李白,一生好飲。幸好他從不開車。一天,他提着酒壺,從家里出來,酒壺中有酒2斗。他邊走邊唱: 無事街上走,提壺去打酒。 逢店加一倍,遇花喝一斗。 這一路上,他一共遇到店5次,遇到花10次,已知最后一次遇到的是花,他正好把酒喝光了。 請你計算李白遇到店和花的次序,可以把遇店記為a,遇花記為b。則:babaabbabbabbbb 就是合理的次序。像這樣的答案一共有多少呢?請你計算出所有可能方案的個數(包含題目給出的)。
【題解】
李白打酒這類問題,本質問題其實也是一棵搜索樹,這棵樹進行遍歷,李白走到某一位置時相當於走到搜索樹哪一層,同時每個節點往上看都說明李白之前encounter 店和花的數量 和 酒壺中的酒的量。針對該問題:我們可以得知它時二叉樹,因為李白走下一個位置只有花和店兩種情況。每個節點都是一個<flower,shop,wine>的三元組。
往左子樹走時,我們定義為遇到酒店,<flower,shop+1,wine*2>
往右子樹走時,我們定義為遇到花朵,<flower+1,shop,wine-1>
我們這顆搜索樹就是只有flower+shop層。如例子中,5+10=15層,葉子結點就是flower=5,shop=10.
問題就轉化成 :到達二叉樹的15層后,葉子結點<flower==5,shop==10,wine==0>的情況。輸出李白在途中所遇到的店和花的排列問題,也就是遍歷二叉樹的問題,答案即為葉子結點當且僅當滿足花遇到10遍,酒店遇到5遍,酒壺沒有酒的情況 進行統計。

1 //dfs解決李白打酒問題 2 #include<cstdio> 3 #include<algorithm> 4 using namespace std; 5 const int N = 20 ; 6 char path[N] ; 7 int cnt = 0 ; 8 void dfs( int step , int shop , int flower , int wine ){ 9 if( step == 15 && shop == 5 && flower == 10 && wine == 0 ){ 10 for( int i = 0 ; i < step ; i++ ){ 11 printf("%c",path[i]); 12 } 13 putchar('\n'); 14 cnt ++ ; 15 return ; 16 } 17 // 若走左子樹,已遇到酒店的數量不超過5,同時過程中不能出現酒為0 18 // 因為 若不加以判斷,酒為0時 0 * 2 = 0 ,明顯是不合法的 19 if( shop < 5 && wine >= 1 ){ 20 path[step] = 'a' ; 21 dfs( step + 1 , shop + 1 , flower , wine * 2 ); 22 } 23 // 若走右子樹,已遇到花的數量不超過10, 同時酒壺也必須有酒 24 if( flower < 10 && wine >= 1 ){ 25 path[step] = 'b' ; 26 dfs( step + 1 , shop , flower + 1 , wine - 1 ); 27 } 28 29 } 30 int main() 31 { 32 dfs( 0 , 0 , 0 , 2 ); 33 printf("%d\n",cnt); 34 return 0 ; 35 }
N皇后
【問題描述】
N個國際象棋的皇后,放置在N*N的棋盤,使其相互之間不能攻擊到對方。
皇后的攻擊范圍:所在行,所在列,所在兩個對角線。
請問有多少種情況。
【題解】
N皇后是經典的題目,對於搜索來說,可以是過程中利用標記數組,同時還需要判斷過程是否合法。
我們搜素順序是按照列的順序,然后每一列中我們把皇后放在第i行中。
實際該問題類似n叉樹,但是n叉樹會隨着搜索樹往下而減少。
第一層是n叉樹,第二層每一個結點 都是 n-1叉樹……
答案就是搜索到最后一層即可,因為過程中放的皇后必定與前面的皇后無沖突,所以直接統計到達葉子結點的個數。

1 //dfs解決N皇后問題 2 3 #include<cstdio> 4 #include<cstdlib> 5 const int N = 50 ; 6 int vis[N] , Queen[N] ; 7 8 //判斷放置在x列的皇后是否合法 9 bool check( int x ){ 10 for( int i = 1 ; i < x ; i++ ){ 11 if( abs( x - i ) == abs( Queen[x] - Queen[i] ) ){ 12 return false ; 13 } 14 } 15 return true ; 16 } 17 18 int n = 8 ; 19 int ans = 0 ; 20 21 void dfs( int step ){ 22 if( step == n + 1 ){ 23 ans ++ ; 24 return ; 25 } 26 for( int i = 1 ; i <= n ; i++ ){ 27 //當前行沒被占據 28 if( vis[i] == 0 ){ 29 //把皇后 放置在 <第i行,第step列> 30 Queen[step] = i ; 31 32 //判斷其是否合法,若合法繼續搜索 33 if( check(step) ){ 34 //往下深搜時,記得打標記. 35 vis[i] = 1 ; 36 dfs( step + 1 ); 37 //回溯使記得撤標. 38 vis[i] = 0 ; 39 } 40 } 41 } 42 } 43 int main() 44 { 45 dfs( 1 ) ; 46 printf("%d\n",ans); 47 return 0; 48 }
幻方數
【題目描述】
幻方是把一些數字填寫在方陣中,使得行、列、兩條對角線的數字之和都相等。歐洲最著名的幻方是德國數學家、畫家迪勒創作的版畫《憂郁》中給出的一個4階幻方。 他把1,2,3,...16 這16個數字填寫在4 x 4的方格中。 16 ? ? 13 ? ? 11 ? 9 ? ? * ? 15 ? 1 表中有些數字已經顯露出來,還有些用?和*代替。
【題解】
根據幻方的定義,對於方格進行搜索,過程中借助標記數組。
然后搜素到最后利用幻方的定義判斷是否都行列對角線是否符合幻方要求。
其要求為:行,列,對角線 之和都為34.

1 //dfs解決幻方數 2 3 /* 4 16 ? ? 13 5 ? ? 11 ? 6 9 ? ? * 7 ? 15 ? 1 8 */ 9 10 #include<cstdio> 11 12 const int N = 20 ; 13 const int M = 5 ; 14 int magic[M][M] , n = 4 ; 15 int row[M] , col[M] , diag[2] ; 16 int vis[N] ; 17 18 void Init(){ 19 magic[1][1] = 16 ; magic[1][4] = 13 ; 20 magic[2][3] = 11 ; magic[3][1] = 9 ; 21 magic[4][2] = 15 ; magic[4][4] = 1 ; 22 //vis[*] = -1 固定着,不讓其修改 23 vis[1] = vis[9] = vis[11] = vis[13] = vis[15] = vis[16] = -1 ; 24 25 //設定行列對角線的初始值 26 row[1] = 29 ; row[2] = 11 ; row[3] = 9 ; row[4] = 16 ; 27 col[1] = 25 ; col[2] = 15 ; col[3] = 11 ; col[4] = 14 ; 28 29 diag[0] = 17 ; diag[1] = 24 ; 30 } 31 32 //遍歷順序是從左往右,從上到下 33 void dfs( int x ,int y ){ 34 35 //到達最后一個格子,因為最后一個位置已固定,所以直接計算即可 36 if( x == n && y == n ){ 37 int tmp = 0 ; 38 for( int i = 1 ; i <= n ; i++ ){ 39 tmp += (row[i]==34) ; 40 tmp += (col[i]==34) ; 41 } 42 tmp += ( diag[0] == diag[1] && diag[0] == 34 ); 43 44 if( tmp == 9 ){ 45 for( int i = 1 ; i <= n ; i++ ){ 46 for( int j = 1 ; j <= n ; j ++ ){ 47 printf("%-3d",magic[i][j]); 48 } 49 putchar('\n'); 50 } 51 } 52 return ; 53 } 54 if( vis[magic[x][y]] == -1 ){ 55 if( y == n ){ 56 dfs( x + 1 , 1 ); 57 }else{ 58 dfs( x , y + 1 ); 59 } 60 } 61 else{ 62 //因為1,15,16都已在幻方中,所以直接搜索[2,14] 63 for( int i = 2 ; i <= 14 ; i++ ){ 64 //當前這個數字沒有被用過 65 if( vis[i] == 0 ){ 66 vis[i] = 1 ; 67 magic[x][y] = i ; 68 69 //添加后修改該行列對角線的總和 70 row[x] += i ; 71 col[y] += i ; 72 diag[0] += (x==y) * (i); 73 diag[1] += (x==n+1-y) * (i) ; 74 75 //往下走,如果到最右邊,則換行 76 if( y == n ){ 77 dfs( x + 1 , 1 ); 78 }else{ 79 dfs( x , y + 1 ); 80 } 81 82 //記得撤出標記 83 vis[i] = 0 ; 84 row[x] -= i ; 85 col[y] -= i ; 86 diag[0] -= (x==y) * (i); 87 diag[1] -= (x==n+1-y) * (i) ; 88 } 89 } 90 } 91 } 92 int main(){ 93 Init() ; 94 dfs( 1 , 1 ); 95 return 0; 96 }