用DFS 解決全排列問題的思想詳解


首先考慮一道奧數題目:

□□□ + □□□ = □□□,要將數字1~9分別填入9個□中,使得等式成立。例如173+286 = 459。請輸出所有合理的組合的個數。

我們或許可以枚舉每一位上所有的數,然后判斷每一位上的數需要互不相等且滿足等式即可,但是用代碼寫出來需要聲明9個變量且判斷。
那么我們把這個問題考慮為一個求這個9個數的全排列問題,即可得到更優雅的解答方式。
首先我們考慮一個經典的全排列問題(《啊哈,算法》):

輸入一個數,輸出1~n的全排列。

現在我們考慮有1、2、3的3張撲克牌和編號為1、2、3的3個盒子,需要將這3張撲克牌放到3個盒子里,求其所有可能性。

  1. 首先我們考慮1號盒子,我們約定每到一個盒子面前都按數字遞增的順序擺放撲克牌。於是把1號撲克牌放到1號盒子中。
  1. 接着考慮2號盒子,現在我們手里剩下2號和3號撲克牌,於是我們可以把2號撲克牌放入2號盒子中。於是在3號盒子只剩一種可能性,我們繼續把3號撲克放入3號盒子。此時產生了一種排列——{1,2,3}。
  2. 接着我們收回3號盒子中的3號撲克牌,嘗試一種新的可能,此時發現別無他選。於是選擇回到2號盒子收回2號撲克。
  3. 在2號盒子中我們放入3號撲克,於是自然而然的在3號盒子中只能放入2號撲克。此時產生另一種排列——{1,3,2};
  4. 重復以上步驟就能得到數字{123}的全排列。

現在我們用C語言代碼描述往每個小盒子中放入所有可能撲克牌的步驟:
for(int i = 1; i <= n; i++){ a[step] = i; //將i號撲克牌放入第step個盒子中 }
a是一個裝入了所有小盒子的數組,變量step表示當前正處於第step號小盒子前。i則表示撲克牌的序號。現在我們需要考慮另外一個問題,則如果一張撲克牌已經被放入別的盒子中,則不能再被放入當前盒子。因此需要一個book數組標記哪些牌已經被使用。此時我們完善上述代碼。
for(int i = 1; i <= n; i++){ if(book[i] == 0){ a[step] = i; //將i號撲克牌放入第step個盒子中 book[i] = 1; // 置1表示第i號撲克牌不在手中 } }

現在對於step號盒子已經處理完,那么我們要考慮step+1號盒子。第step+1個的盒子的處理方式與第step個盒子的處理方式完全一樣。因此,我們可以對上述操作做一個封裝。
void dfs(int step){ //step表示當前要處理的盒子 for(int i = 1; i <= n; i++){ if(book[i] == 0){ a[step] = i; //將i號撲克牌放入第step個盒子中 book[i] = 1; // 置1表示第i號撲克牌不在手中 } } }

於是我們重新回想文章開頭闡述的放置撲克牌的思路:我們在當前盒子放置完第i個撲克牌之后,便立即處理下一個盒子。於是:
void dfs(int step){ //step表示當前要處理的盒子 for(int i = 1; i <= n; i++){ if(book[i] == 0){ a[step] = i; //將i號撲克牌放入第step個盒子中 book[i] = 1; // 置1表示第i號撲克牌不在手中 dfs(step+1); //遞歸調用 book[i] = 0; // 非常重要,收回該盒子中的撲克牌才能進行下一次嘗試。 } } }

需要注意到的是,我們需要收回每一次嘗試的撲克牌i,才能進行下一次嘗試。現在需要考慮最后一個問題,那就是什么時候得到一個滿足要求的排列,也就是考慮終止條件。這里很容易得到,當我們處理完成第n個盒子的時候,就已經得到一個符合要求的排列了。加上終止條件的代碼如下:
void dfs(int step){ //step表示當前要處理的盒子 if(step == n+1){ //輸出排列 for(i = 1; i <= n; i++) printf("%d", a[i]); printf("\n"); return; } for(int i = 1; i <= n; i++){ if(book[i] == 0){ a[step] = i; //將i號撲克牌放入第step個盒子中 book[i] = 1; // 置1表示第i號撲克牌不在手中 dfs(step+1); //遞歸調用 book[i] = 0; // 非常重要,收回該盒子中的撲克牌才能進行下一次嘗試。 } } }
現在深度優先搜索(DFS)的基本模型展現在我們眼前。其核心在於,在當前步驟要把每一種可能性都嘗試一遍(使用for循環),解決完當前步驟后進入下一步。而下一步的解決方式完全等同於當前步驟的解決方法。於是可以總結出DFS的基本模型:
void dfs(int step){ *判斷結束邊界* 嘗試每一種可能 for(i = 1; i <= n; i++){ 嘗試下一步 dfs(step + 1); } return; }


好了,現在我們總結出來了DFS的基本框架,這個框架可以用於解決基於全排列所給出的一系列算法題。
下面列出一道《劍指offer》中的面試題:

輸入一個字符串,按字典序打印出該字符串中字符的所有排列。例如輸入字符串abc,則打印出由字符a,b,c所能排列出來的所有字符串abc,acb,bac,bca,cab和cba。

輸入描述:輸入一個字符串,長度不超過9(可能有字符重復),字符只包括大小寫字母。

我們可以看到這道題目似乎和上面一開始說到的朴素的數字排列完全一致,但是我們要考慮到的是,輸入的字符串中可能包含了字符重復。 標准解法如下:
PermutationHelp(vector<string> &ans, int k, string str) { if(k == str.size() - 1) // 結束條件 ans.push_back(str); unordered_set<char> us; //記錄出現過的字符 sort(str.begin() + k, str.end()); //保證按字典序輸出 for(int i = k; i < str.size(); i++){ if(us.find(str[i]) == us.end())//只和沒交換過的換 { us.insert(str[i]); swap(str[i], str[k]); PermutationHelp(ans, k + 1, str); swap(str[i], str[k]); //復位 } } } vector<string> Permutation(string str) { vector<string> ans; PermutationHelp(ans, 0, str); return ans; }
可以看到,這里沿用了DFS的基本模型。k為當前步驟的指示器。為了解決字符重復問題,使用了std::unorder_set 容器存儲已經交換過的元素。

例如我們輸入為: {a,a,b,c,d}時,當k = 0, i = 0時, us.find(str[i]) == us.end()的結果為true,因為此時us中元素個數為0,此時將a放入無序集合中;而當k = 0, i = 1時,上述判斷結果為false,此時不進行交換,i的值直接加1。


接下來我們解決一開始的奧數題似乎是易如反掌了:

□□□ + □□□ = □□□,要將數字1~9分別填入9個□中,使得等式成立。例如173+286 = 459。請輸出所有合理的組合的個數。

我們只需要在dfs的基礎上修改一下結束條件中的代碼即可:
int total = 0; void dfs(int step){ //step表示當前要處理的盒子 if(step == 10){ //只有9個盒子 //判斷是否滿足等式 if(a[1] * 100 + a[2] * 10 + a[3] + a[4] * 100 + a[5] * 10 + a[6] == a[7] * 100 + a[8] * 10 + a[9]){ //滿足要求,打印 total += 1; ........// 省略打印代碼 } return; } for(int i = 1; i <= n; i++){ if(book[i] == 0){ a[step] = i; //將i號撲克牌放入第step個盒子中 book[i] = 1; // 置1表示第i號撲克牌不在手中 dfs(step+1); //遞歸調用 book[i] = 0; // 非常重要,收回該盒子中的撲克牌才能進行下一次嘗試。 } } }
這里需要注意,最后輸出的total需要除以2,因為 173 + 286 和 286 + 173 是同一種結果。


同樣的,下面這道題目:

輸入一個含有8個數字的數組,判斷有沒有可能把這8個數字分別放到正方體的8個頂點上,使得正方體上三組相對的面上的4個頂點的和相等。

這道題與上面的奧數題類似,相當於需要得到8個數字的所有排列。如圖,假設8個頂點分別是a1,a2,a3,a4,a5,a6,a7,a8。 接着判斷有沒有某一個排列符合題目所給的條件,即:
a1+a2+a3+a4 = a5+a6+a7+a8 && a1 + a3 + a5 + a7 = a2 + a4 + a6 + a8 && a1 + a2 +a5 +a6 = a3 + a4 +a7 + a8
成立。

 
正方體.png


本文轉載於:https://www.jianshu.com/p/897f2a9e33cd


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM