簡述
本算法摘選自啊哈磊所著的《啊哈!算法》第四章第一節的內容——深度優先搜索(DFS)。其實這個名詞以前聽說過很多次,但是就是沒有了解過這是什么東西,感覺很深奧離自己還很遠,而且目前遇到的項目中一直都未曾有使用這種算法來解決問題,可能是我才疏學淺不會用吧,所以對這算法的概念和用法也知之甚少。結此學習之機會,來開始逐步學習DFS這項算法吧,文中代碼使用C語言編寫,博主通過閱讀和理解,重新由Java代碼實現了一遍,希望能夠對DFS算法有比較深刻一點的認識。
從簡單案例開始
輸入一個數n,輸出1~n的全排列,每個數只出現一次,例如輸入3表示輸出1~3的全排列:123、132、213、231、312、321,要求寫個程序,能夠滿足輸入一個數n(1~9)之后打印出這個1~n的全排列可能性。
解法思路
求1~n的全排列,很容易想到的是暴力枚舉法,n是幾就來幾重循環,循環打印就是了,你比如n=3時,暴力法是這樣的:
1 public static void main(String[] args) { 2 for(int a = 1; a <= 3; a++) { 3 for(int b = 1; b <= 3; b++) { 4 for(int c = 1; c <= 3; c++) { 5 if(a != b && a != c && b != c) { 6 System.out.println(a + "" + b + "" + c); 7 } 8 } 9 } 10 } 11 }
上面的代碼看起來還好,但是如果要你打印1~9的數字全排列,寫9層循環嗎?不是不可以,只是顯得不夠專業而已,那有沒有更好的解決辦法呢?我們可以換一種思路來解決這個問題。
就拿上面輸出1~3這三個數的全排列來講,可以假設有三張撲克牌需要放入到編號為1~3的三個箱子中,每個箱子只能放一張撲克牌而且三個箱子需要放滿,這樣有多少種放法。
假設我們按照1~3的順序依次放入到編號為1~3的箱子中,每次放牌都按照這個順序進行。當嘗試把123這三張撲克牌都放進編號為123的箱子中之后,再嘗試放第4號的箱子,發現沒有箱子,也發現沒有撲克牌可用了,這就說明前面我們擺放的順序就是一種方式,手上沒撲克牌且箱子已經放滿就是一種結束或者說臨界條件。
接下來嘗試其他可能,先4號箱子往前走,走到3號箱子前面,拿出這張牌,因為3號箱子已經放過3號撲克牌,就不能再放了,此時手中又沒有別的撲克牌,於是繼續往前一個箱子看,到了2號箱子,把里面的2號牌拿出,此時手中有23兩張撲克牌,按照123的順序應該先放2號牌的,但是剛剛那輪已經先放了2號牌,於是現在放3號牌。放完2號箱子,繼續到下一個箱子前面,此時手中就只有2號牌,按照123的順序自然就把2號撲克牌投入到3號箱子中,再往下走,發現到了4號的位置沒有了箱子手中也沒有撲克牌,於是這輪放牌又結束了,前面3個箱子中的牌順序就是一個新的序列。
按照上面這種方式,把所有的撲克牌在處理完一輪之后又重新拿出進行下一輪順序的放牌,如此往復,直到所有情況都遍歷完。
代碼實現
1 public class DfsStart { 2 3 /** 4 * 當前要處理的序列數長度 5 */ 6 private static int max = 3; 7 /** 8 * 桶,用來記錄已經投出去的撲克牌牌面值 9 */ 10 private static int[] book = new int[10]; 11 /** 12 * 用來存放撲克牌的箱子 13 */ 14 private static int[] box = new int[10]; 15 16 /** 17 * 表示當前站在第幾個箱子面前,由此執行深度遍歷 18 * @param step 19 */ 20 public void dfs(int step) { 21 22 // 如果站在了最后一個箱子的下一個位置,則表示前面的箱子里都已經放好了撲克牌 23 if(step == max + 1) { 24 // 輸出前面這一種可能的序列,數值從1開始 25 for(int i = 1; i <= max; i++) { 26 System.out.print(box[i] + " "); 27 } 28 System.out.println(""); 29 30 // 打印完成后,回到上一次遞歸調用dfs的地方 31 return; 32 } 33 34 // 此時分別站在第n個箱子面前,決定要放哪一張撲克牌 35 // 撲克牌數值從1開始 36 for(int i = 1; i <= max; i++) { 37 38 // 判斷撲克牌是否還在手中,這里用了桶 39 if(book[i] == 0) { 40 41 // 在手中的話,則將牌放到第Step個箱子中 42 box[step] = i; 43 // 把桶做上標記,表示這個撲克牌已經投出去了 44 book[i] = 1; 45 46 // 第step個箱子已經放好撲克牌,接着往后走一步,嘗試到下一個箱子里放撲克牌 47 dfs(step+1); 48 49 // 如果走到最后的箱子發現已經放完一輪了,則回來的時候依次將牌收到手中,並把桶清理干凈 50 // 這一步很重要,因為不清理的話,后續就不能進行操作了 51 book[i] = 0; 52 } 53 } 54 return; 55 } 56 57 public static void main(String[] args) { 58 DfsStart dfsStart = new DfsStart(); 59 // 從第一個箱子開始投放 60 dfsStart.dfs(1); 61 } 62 }
其中,dfs是一個遞歸函數,表示走到某一步的時候該做的事情,這里,每走一步,就往一個箱子投放一張手中有的撲克牌,投遞完之后進行下一輪投放操作,這又是另外一個遞歸的開始,直到走到的下一步已經沒有箱子了,表示當前已經走完了所有的箱子,手中的牌也已經投放完了,那就打印出之前那些箱子中牌的順序,接着回去收牌,從上一次發牌的地方再嘗試下一種可能。
學習總結
上面這個例子,雖然很簡單,但是卻包含深度優先搜索(Depth First Search, DFS)的基本模型,理解深度優先搜索的關鍵在於解決“當下該如何做”,至於“下一步如何做”則與“當下該如何做”是一樣的,那么上面dfs這個函數就是為了解決當你在step個箱子的時候,你會怎么放撲克牌或者結束了投放操作,下一步也是這樣的操作,如果要遍歷所有的可能性,那可以用for循環來嘗試所有的可能性。每當當前的步驟step完成后就進入下一個step(step=step-1),下一步的解決辦法和當前的解決辦法是一致的。
下面的代碼就是深度優先搜索的基本模型:
1 void dfs(int step) { 2 Step 1. 判斷邊界 3 Step 2. 嘗試每一種可能 for(i=1; i<max; i++) { 4 繼續下一步 dfs(step - 1) 5 } 6 step 3.返回 7 }
我摘了一段WiKi上的解釋:深度優先搜索算法(英語:Depth-First-Search,簡稱DFS)是一種用於遍歷或搜索樹或圖的算法。沿着樹的深度遍歷樹的節點,盡可能深的搜索樹的分支。當節點v的所在邊都己被探尋過,搜索將回溯到發現節點v的那條邊的起始節點。這一過程一直進行到已發現從源節點可達的所有節點為止。如果還存在未被發現的節點,則選擇其中一個作為源節點並重復以上過程,整個進程反復進行直到所有節點都被訪問為止。
通俗一點講,DFS的思想是從一個頂點V0開始,沿着一條路一直走到底,如果發現不能到達目標解,那就返回到上一個節點,然后從另一條路開始走到底。
DFS適合此類題目:給定初始狀態跟目標狀態,要求判斷從初始狀態到目標狀態是否有解。
本文對DFS算法講解的並不算深刻,希望后面還有類似的文章對此加以鞏固。
知識擴展
利用這種算法,我們還可以解決一種問題:請將1~9依次填入方格中,使得能夠將這個等式(口口口 + 口口口 = 口口口)成立,其中每個空格只能填一個數,使用過的數將不能再使用,請問有多少種這樣的填寫方法。
其實上面這種也可以認為是將9張撲克牌投入到9個箱子中,道理是一樣的,就是邊界點不一樣,這里的邊界點就改為等式成立就好了,順帶,我們也把這個算法給寫一下,套路和上面基本是一樣的。
1 public void dfs2(int step) { 2 // 如果站在了第10箱子的位置,則表示前面的箱子里都已經放好了撲克牌 3 if(step == 10) { 4 // 判斷是否滿足等式:口口口 + 口口口 = 口口口 5 if(box[1] * 100 + box[2] * 10 + box[3] 6 + box[4] * 100 + box[5] * 10 + box[6] 7 == box[7] * 100 + box[8] * 10 + box[9]) { 8 total++; 9 System.out.print(String.format("%d%d%d + %d%d%d = %d%d%d", 10 box[1], box[2], box[3], box[4], box[5], box[6], box[7], box[8], box[9])); 11 System.out.println(""); 12 } 13 14 // 打印完成后,回到上一次遞歸調用dfs2的地方 15 return; 16 } 17 18 // 此時分別站在第n個箱子面前,決定要放哪一張撲克牌 19 // 撲克牌數值從1開始 20 for(int i = 1; i <= 9; i++) { 21 22 // 判斷撲克牌是否還在手中,這里用了桶 23 if(book[i] == 0) { 24 25 // 在手中的話,則將牌放到第Step個箱子中 26 box[step] = i; 27 // 把桶做上標記,表示這個撲克牌已經投出去了 28 book[i] = 1; 29 30 // 第step個箱子已經放好撲克牌,接着往后走一步,嘗試到下一個箱子里放撲克牌 31 dfs2(step+1); 32 33 // 如果走到最后的箱子發現已經放完一輪了,則回來的時候依次將牌收到手中,並把桶清理干凈 34 // 這一步很重要,因為不清理的話,后續就不能進行操作了 35 book[i] = 0; 36 } 37 } 38 return; 39 }
參考資料
1、《啊哈!算法》/ 啊哈磊著. 人民郵電出版社