###### tags: `MDCPP` #### MDCPP進階組講義05 ## 遞迴枚舉(回溯法) 作者:LittlePants ---- ---- ## 遞迴枚舉可以解決一切 ## 但必須等到世界毀滅 BY 吳邦一 ---- ## 甚麼是枚舉? - 就是把所有可能都列出來 - 是最暴力的作法 - 但每看到一個題目可以先想想要怎麼枚舉 - 再想想看有沒有更好的方法 ---- ## 為甚麼要用遞迴來枚舉? - 因為很好寫 - 因為可以增加剪枝條件,時間複雜度不比不用遞迴差 --- ## 常見的枚舉 --- ## 子集枚舉(Subset) ---- ## 甚麼是子集? - 給你若干個物品,你可以從裡面選任意個(也可以不選) - 你選出來的那些物品就稱為一個子集 ---- - 假設有 n 個物品,我們就會有 $2^n$ 種選法 - 因為每個物品只有選或不選兩種狀態 ---- ## 要怎麼枚舉? - 因為我們要枚舉所有可能,所以時間複雜度一定會大於 $O(2^n)$ - 所以通常只有 n 不大於 30 的時候會用 - 每種物品都只有選或不選 - 我們可以畫一棵遞迴決策樹來看看他是如何運作的 ---- ## 例題 [Apple Division](https://cses.fi/problemset/task/1623) ---- ## 題目大意 有 $n$ 堆蘋果,$p_1\ p_2$ ~ $p_n$。 你要把蘋果分成任意兩堆,使兩堆的蘋果數量差最少。 $n <= 20$, $p_i <= 10^9$ ---- ## 想法 - 因為 $n$ 非常小會很直覺得想到可以枚舉 - 如果 $p_i$ 範圍沒這麼大,可以使用背包dp ---- ## 解題思路 - 我們可以直接枚舉子集 - 但題目說要分成兩堆 - 我們只需要把 全部減掉現在枚舉的子集 就會得到另一堆了 ---- ### 參考Code ```cpp= int a[25], n, ans, total; void dfs(int u, int sum){ if(ans == 0 || 2 * sum > total + ans) return; // prune 剪枝 if(u == n){ ans = min(ans, abs(2 * sum - total)); // 枚舉完所有物品 遞迴結束條件 return; } dfs(u + 1, sum + a[u]); // 選 dfs(u + 1, sum); // 不選 } signed main(void){ ios_base::sync_with_stdio(0); cin.tie(0); cin >> n; for(int i = 0; i < n; i++) cin >> a[i], total += a[i]; ans = 1e18; dfs(0, 0); cout << ans << '\n'; } ``` --- ## 更多例題 [NPSC初賽 分裁判問題](http://mdcpp.mingdao.edu.tw/problem/C014) [女中judge 以物易物](http://www.tcgs.tc.edu.tw:1218/ShowProblem?problemid=b040) [At coder 切竹子](https://atcoder.jp/contests/abc119/tasks/abc119_c) --- ## 枚舉排列 ---- ## 例題 [小數的密碼](http://www.tcgs.tc.edu.tw:1218/ShowProblem?problemid=b037) ---- ---- ## 題目大意 密碼是 n 位數,且只包含數字 1 ~ m 按照字典序排列,輸出所有密碼的可能 $n <= 10$, $m <= 9$ ---- ## 思考 先想一下複雜度,有 n 位數每個位數都有 m + 1 種可能 所以複雜度 $O(n^{m+1})$ 感覺不太對?! 這樣是 $O(10^{10})$ ㄟ? 應該是出題者複雜度沒算好,其實測資很弱 ---- ## 解題思路 - 這個感覺用for迴圈就可以解ㄟ,但如果你試了就會發現困難重重 - 而且這題要求按照字典序排列,用遞迴寫會很方便 - 我們可以使用遞迴搭配for迴圈來完成這題 ---- ### 參考Code ```cpp= #include<iostream> using namespace std; int n, m, path[100001]; void backtrack(int level){ // level 代表現在枚舉到第幾位數 if(level == n){ // 終止條件 順便輸出 for(int i = 0 ; i < n; i++) cout << path[i]; cout << '\n'; return; } for(int i = 0; i <= m; i++){ path[level] = i; backtrack(level + 1); } } int main(void){ cin >> n >> m; backtrack(0); } ``` ---- ---- ## 變化題 [全排列](http://www.tcgs.tc.edu.tw:1218/ShowProblem?problemid=b038) ---- ## 想法 跟上一題的差別就在於數字不能重複出現 所以就開一個陣列紀錄那些數字已經出現過就好了 ---- ```cpp= #include<iostream> #include<bitset> using namespace std; int n, path[15]; bitset<15> used; // 用來記錄那些數字被用過了 void backtrack(int level){ if(level == n){ for(int i = 0 ; i < n; i++) cout << path[i]; cout << '\n'; return; } for(i = 1; i <= n; i++){ if(used[i]) continue; used[i] = 1; backtrack(level + 1); used[i] = 0; } } int main(void){ cin >> n; backtrack(0); } ``` ---- ## 練習題 [全排列 MD judge](http://mdcpp.mingdao.edu.tw/problem/B010) ---- 參考 Code :::spoiler ```cpp= #include<iostream> #include<bitset> using namespace std; int n, path[20], lim[20]; bitset<20> used, f; void backtrack(int level){ if(level == n){ for(int i = 0 ; i < n; i++) cout << path[i] << " "; cout << '\n'; return; } int t = 1; if(f[level] == 0){ t = lim[level]; f[level] = 1; } for(int i = t; i <= n; i++){ if(used[i]) continue; used[i] = 1; backtrack(level + 1); used[i] = 0; } } int main(void){ ios_base::sync_with_stdio(0); cin.tie(0); cin >> n; for(int i = 0; i < n; i++) cin >> lim[i]; backtrack(0); } ``` ::: --- ## 八皇后問題 ---- ## 例題 [Chessboard and Queens](https://cses.fi/problemset/task/1624) ---- ## 觀察棋盤 - 同一直行橫列都很好判斷 - 但斜線怎麼辦? ---- - 其實規律應該不難發現(可以把它當作直角座標系) - 圖片 ---- - 知道了這些條件之後就只需要枚舉就可以了耶! - 遇到可以擺皇后的地方就先擺,擺到不能擺為止 - 如果擺滿 n 個那就是符合條件 ---- ## 策略 - 所以要一格一格枚舉嗎? - 其實不用先枚舉一列,再從裡面選一個出來就好 - 如果這一列已經放了就直接到下一列 - 如果 1 ~ n 列都有擺,那就是一組解 ---- 參考 Code :::spoiler https://cses.fi/paste/7c12490e107cd16f157639/ ::: ---- ## 額外練習 1. [數獨](https://zerojudge.tw/ShowProblem?problemid=d263) 2. [得分最多的皇后](http://mdcpp.mingdao.edu.tw/problem/b039) --- ### 遞迴枚舉的經典題如下 - 子集枚舉(subset) - 排列組合的枚舉 - 八皇后問題 - 需要取消操作的遞迴(走迷宮問題) - 需要得迴圈層數是變數 ---- ## 總結 雖然枚舉是最差的方法,但逼不得已時還是要會用 而且枚舉常常有助於你對題目的了解 對一個題目先了解怎麼枚舉,再去優化他 枚舉搭配一些技巧,其實複雜度也不會到太差 --- ## 感謝觀看
{"metaMigratedAt":"2023-06-15T17:01:01.921Z","metaMigratedFrom":"YAML","title":"MDCPP 遞迴枚舉講義","breaks":true,"slideOptions":"{\"theme\":\"solarized\",\"transition\":\"slide\"}","contributors":"[{\"id\":\"853e1517-7034-4077-803a-7bb83a49f00a\",\"add\":5180,\"del\":485}]"}
    594 views