# Ch16 回溯法 > 搭配[green judge解題系統](http://www.tcgs.tc.edu.tw:1218/) > Special thanks to [台中女中sagit老師](http://www.tcgs.tc.edu.tw/~sagit/index.htm) \<\(\_ \_\)\> ## > 上一章:[動態規劃](https://hackmd.io/s/ByZgGkLpW) > 下一章:[二分搜尋法](https://hackmd.io/s/ByCZ5Yoa7) > 回目錄:[國立科學園區實驗中學C++程式語言自學講義](https://hackmd.io/s/B18yT_i5Z) ## <font color='darkblue'>回溯法的用途</font> 回溯法適用於想要窮舉所有可能的問題上 例如: 1.N格的密碼鎖,每格都是0至9的數字,列出所有密碼的可能 2.N格的密碼鎖,每格都是0至9的數字且互不重複,列出所有密碼的可能 看起來好像可以用迴圈解決 例如N是4的時候,第一題可以寫成 ```cpp= for(int i=0;i<10;i++){ for(int j=0;j<10;j++){ for(int k=0;k<10;k++){ for(int l=0;l<10;l++){ cout<<i<<j<<k<<l<<endl; } } } } ``` 第二題可以寫成 ```cpp= for(int i=0;i<10;i++){ for(int j=0;j<10;j++){ for(int k=0;k<10;k++){ for(int l=0;l<10;l++){ if(i!=j&&i!=k&&i!=l&&j!=k&&j!=l&&k!=l){ cout<<i<<j<<k<<l<<endl; } } } } } ``` 但是,如果N是太大的數字 不可能真的就寫出N個迴圈來 而且,因為題目不知道N是多少 因此沒辦法在寫程式的時候就固定某數量的迴圈 ## <font color='darkblue'>把迴圈放進遞迴裡</font> 以上迴圈包迴圈的作法雖然是對的 但苦於不知道總共要用幾個迴圈 那麼解決的方法就是把迴圈放進遞迴裡面 只要一開始知道總共要遞迴到幾層 就能利用遞迴創造出像上面那樣迴圈包迴圈的效果 例如 ```cpp= int ans[100]; void permute(int idx) { if(idx==n){ for(int i=0;i<n;i++){ //印答案 cout<<ans[i]; } cout<<endl; return; } for(int i=0;i<10;i++){ //列舉第idx格可以是哪些數字 ans[idx]=i; permute(idx+1); } } int main() { cin>>n; permute(0); } ``` 函式裡面放的是一個迴圈 把答案的第idx個位置從0到9都試過一遍 接著呼叫下一次遞迴來列舉第idx+1個位置的所有可能 ans的陣列存放的是列舉出來的答案 等到每一格都放好數字了 就可以把答案印出來 <font color="darkgreen"> 【學生練習題】</font> > - [ ] [Green Judge b037: 小數的密碼 ](http://www.tcgs.tc.edu.tw:1218/ShowProblem?problemid=b037) ## <font color='darkblue'>不能重複的問題</font> 如果題目要求每格都互不重複 那麼在印出答案時檢查兩兩有沒有相等實在有點太麻煩了 例如總共有10格 那就要檢查$C^{10}_2=45$次了 實在不優 因此可以開一個陣列來記錄每個數字是不是已經被用過了 如果已經被用過了那就不要用了 <font color='darkorange'>【例題】</font> > 密碼是N個不重複的數字 > 每個數字都是0到9之間的數字 > 請列出所有可能 ```cpp= bool used[10]; int ans[100]; void permute(int idx) { if(idx==n){ for(int i=0;i<n;i++) cout<<ans[i]; cout<<endl; return; } for(int i=0;i<10;i++){ if(used[i]==0){ //只有「數字i」沒有被使用過時才可以把i放在這格使用 used[i]=1; //把「數字i」標記成已使用 permute(idx+1); used[i]=0; //把「數字i」標記成未使用 } } } int main() { cin>>n; memset(used, 0, sizeof(used)); // 把 used 陣列全洗成 0 permute(0); } ``` <font color="darkgreen"> 【學生練習題】</font> > - [ ] [Green Judge b038: 法老王的石獅子 ](http://www.tcgs.tc.edu.tw:1218/ShowProblem?problemid=b038) ## <font color='darkblue'>經典題型:八皇后</font> 在8\*8的棋盤上放8隻皇后棋 這8隻皇后兩兩不能在同一行、不能在同一列、也不能在同一條斜線上 不然這兩隻皇后會互相殘殺 請列出總共 8! 種排法裡面 所有「不會有皇后互相殘殺」的排法 首先 第一行的皇后可以放在1至8的任何一個位置 第二行的皇后也是試試看1至8,但需要避開會和第一隻皇后殘殺的位置 第三行的皇后則須要避開會和第一隻或第二隻皇后殘殺的位置 因此 我們需要三個陣列來記錄 「這一列是不是已經被某隻皇后佔據了」 「這一條斜線(/)是不是已經被某隻皇后佔據了」 「這一條斜線(\\)是不是已經被某隻皇后佔據了」 ```cpp= bool col_used[100]; bool left_diag_used[100]; bool right_diag_used[100]; int ans[15]; void queen(int row) { if(row==n+1){ 印出答案 return; } for(int col=1;col<=n;col++){ // 如果這個直線或斜線被占據了,就跳過 if(col_used[col]==1) continue; if(left_diag_used[row+col]==1) continue; if(right_diag_used[n+row-col]==1) continue; // 放上本隻皇后 ans[row]=col; // 標示出這個直線和斜線都被占據了 col_used[col]=1; left_diag_used[row+col]=1; right_diag_used[n+row-col]=1; // 繼續遞迴 queen(row+1); // 取消,標示出這個直線和斜線都沒被佔據 col_used[col]=0; left_diag_used[row+col]=0; right_diag_used[n+row-col]=0; // 繼續迴圈,試試看把本隻皇后放到別的位置看看 } } int main() { cin>>n; memset(col_used, 0, sizeof(col_used)); memset(left_diag_used, 0, sizeof(left_diag_used)); memset(right_diag_used, 0, sizeof(right_diag_used)); queen(0); } ``` <font color="darkgreen"> 【學生練習題】</font> > - [ ] [Green Judge b039: 最終兵器X ](http://www.tcgs.tc.edu.tw:1218/ShowProblem?problemid=b040) ## <font color='darkblue'>經典題型:多個物品內挑數個達成特定目標</font> 有N個物品 每個都可以選擇拿或不拿 所有被選到的物品希望達成某個特定目標 用遞迴函式把這2^N種組合都列出來 每次都檢查是否達成目標即可 <font color='darkorange'>【例題】</font> >有N支放大鏡,每一支的放大倍數都不同 >想要選若干支起來達成M的放大倍率 >請列出所有的選法 用current來表示目前已經放大幾倍了 在函式裡面把「拿第idx支」和「不要拿第idx支」都試試看 ```cpp= bool selected[30]; int magnify[30]; void solve(int idx, int current) { //要拿這支 selected[idx]=1; solve(idx+1, current*magnify[idx]); //不要拿這支 selected[idx]=0; solve(idx+1, current); } int main() { cin>>n; cin>>goal; for(int i=0;i<n;i++) cin>>magnify[i]; solve(0, 1); } ``` 接著考慮邊界條件 1. 放大倍率達到目標了(current==goal) 2. 每一支都試過了(idx==n) ```cpp= bool selected[30]; int magnify[30]; void solve(int idx, int current) { if(current==goal){ //放大倍率達到目標 // 印出選了哪幾支 for(int i=0;i<n;i++){ if(selected[i]) cout<<magnify[i]<<" "; } cout<<endl; return; } if(idx==n) return; //每一支都試過了 //要拿這支 selected[idx]=1; solve(idx+1, current*magnify[idx]); //不要拿這支 selected[idx]=0; solve(idx+1, current); } ``` 這時可以再想想 有沒有甚麼地方可以提早知道沒希望了、提早放棄呢? 當目前的放大倍率已經超過目標時 不管後面的怎麼選都不可能成功 因此可以加上檢查current>goal ```cpp= bool selected[30]; int magnify[30]; void solve(int idx, int current) { if(current==goal){ //放大倍率達到目標 for(int i=0;i<n;i++){ if(selected[i]) cout<<magnify[i]<<" "; } cout<<endl; return; } if(idx==n) return; //每一支都試過了 if(current>goal) return; //沒希望了 //要拿這支 selected[idx]=1; solve(idx+1, current*magnify[idx]); //不要拿這支 selected[idx]=0; solve(idx+1, current); } ``` <font color="darkgreen"> 【學生練習題】</font> > - [ ] [Green Judge b040: 以物易物 ](http://www.tcgs.tc.edu.tw:1218/ShowProblem?problemid=b040) ## > 上一章:[動態規劃](https://hackmd.io/s/ByZgGkLpW) > 下一章:[二分搜尋法](https://hackmd.io/s/ByCZ5Yoa7) > 回目錄:[國立科學園區實驗中學C++程式語言自學講義](https://hackmd.io/s/B18yT_i5Z)