# 枚舉 {%hackmd @themes/dracula %} ## 何謂枚舉 * 把所有可能性找出來 * 最慢、最直接的方法(暴力法) > APCS p3, p4 的第一個子題通常能用枚舉偷分 ## 常見方法 * 迴圈 * 遞迴 * 二進制 ## 迴圈 * 就是全部跑一次 ### 例題 #### 有幾個6 獨眼怪想知道 0~N 的數字裡面,共有幾個數字 6 在各個數字其中。 **INPUT** 輸入一個數字 N (N<=106)。 **OUTPUT** 輸出 0~N 中 6 的個數。 :::spoiler AC code ```cpp= #include <iostream> using namespace std; int count_six(int n){ int count = 0; while(n > 1){ if(n % 10 == 6){ count += 1; } n /= 10; } return count; } int main(){ int n, s = 0; cin >> n; if(n < 6) cout << 0; else{ for(int i = 6; i <= n; i++){ s += count_six(i); } cout << s; } } ``` ::: ## 遞迴 ### 例題 #### 獨眼怪試密碼 獨眼怪忘記自己保險庫的密碼了! 他只記得密碼共N個數字,且都介於0~M 之間。 **INPUT** 輸入只有一行 N, M (2<=N<=10, 1<=M<=9) 。 **OUTPUT** 請依照字典序輸出所有密碼可能讓獨眼怪參考。 :::spoiler AC code ```cpp= #include <iostream> using namespace std; int n, m, ans[15]; void find(int pos){ if(pos == n){ for(int i = 0; i < n; i++) cout << ans[i]; cout << "\n"; return; } for(int i = 0; i <= m; i++){ ans[pos] = i; find(pos+1); } } int main(){ cin >> n >> m; find(0); } ``` ::: ## 二進制(子集合) * 枚舉所有子集合的可能性 * 挑選東西 => 選 / 不選 * 分成兩種可能性,往下遞迴 * 複雜度:O(2^n) ### 例題 #### 分蘋果 有 N 顆蘋果裝在袋子裡,每顆蘋果都有甜美度,獨眼怪想要把它分成兩堆(其中一堆可以0顆),使得這兩堆得蘋果甜美度相差最少。 **INPUT** 輸入有兩行,第一行為正整數 N(N≤20),代表有幾顆蘋果。 第二行為 N 個整數代表甜美度,甜美度≤100。 **OUTPUT** 請輸出兩堆蘋果甜美度最少會差多少? #### 想法 * 把全部的情形都舉出來就好了 :::spoiler AC code ```cpp= #include <iostream> using namespace std; int n, a[25], s = 0, ans = 10000; void comb(int sum1, int id){ if(id == n){ int sum2 = s - sum1; ans = min(ans, abs(sum1 - sum2)); return; } comb(sum1 + a[id], id+1); // 拿 comb(sum1, id+1); // 不拿 } int main(){ cin >> n; for(int i = 0; i < n; i++){ cin >> a[i]; s += a[i]; } comb(0, 0); cout << ans; } ``` ::: ## 剪枝 ### 何謂剪枝 * 一種想法 * 遞迴過程中,如果繼續遞迴下去也不可能是答案,那就直接`return` ## 例題 ### p4. 數獨 輸入一個 9∗9 的數獨,如果數字是0代表還沒填入數字。 請找到字典序最小的解 **INPUT** 輸入有9行,每行有9個數字,數字之間用空格隔開 **OUTPUT** 輸出字典序最小的解,保證一定有解 #### 想法 * 終止條件:填到最後一格 :::spoiler AC code ```cpp= #include <iostream> using namespace std; int board[9][9]; bool check(int x, int y, int num) { // 檢查 for (int i = 0; i < 9; i++) { // 橫的和直的 if (board[x][i] == num || board[i][y] == num) { return false; } } int startX = (x / 3) * 3; int startY = (y / 3) * 3; for (int i = startX; i < startX + 3; i++) { // 小正方形 for (int j = startY; j < startY + 3; j++) { if (board[i][j] == num) { return false; } } } return true; } bool solve(int x, int y) { if (x == 9) { return true; } int nx = x, ny = y + 1; if (ny == 9) { // 檢查下一格有沒有要換行 nx++; ny = 0; } if (board[x][y] != 0) { // 如果已經有答案就接下一個 return solve(nx, ny); } for (int num = 1; num <= 9; num++) { // 每個數字都試過 if (check(x, y, num)) { board[x][y] = num; // 填進去 if (solve(nx, ny)) { // 如果這格這樣填下一格可以就一個遞迴安餒 return true; } board[x][y] = 0; // 回復 } } return false; } int main() { for (int i = 0; i < 9; i++) { for (int j = 0; j < 9; j++) { cin >> board[i][j]; } } if (solve(0, 0)) { for (int i = 0; i < 9; i++) { for (int j = 0; j < 9; j++) { cout << board[i][j]; if(j < 8) cout << " "; } cout << "\n"; } } } ``` ::: ### p5. N-Queen 給你一個大小為 N∗N 的棋盤,你要在上面擺皇后 我們稱一個 N∗N 的盤面合法的條件如下: 1.對於任兩個皇后,橫、直、斜的不能相互連線 (詳情觀察範例測資) 2.共要擺放 N 個皇后 **INPUT** 輸入僅有一個正整數 N(≤10) 代表棋盤大小為 N∗N。 **OUTPUT** 請輸出所有合法盤面,並輸出解法數,依照字典序大小輸出,皇后為Q,空格為x。 輸出格式詳見範例輸出,所有合法盤面之後皆須接一個空行。 #### 想法 * 終止條件:擺滿了n個皇后 :::spoiler AC code ```cpp= #include <iostream> using namespace std; int n, board[15][15], cnt = 0, solutions = 0; bool check(int x, int y){ for(int i = 0; i < n; i++){ if(board[x][i] == 1 || board[i][y] == 1) return false; } for(int i = x, j = y; i >= 0 && j >= 0; i--, j--){ if(board[i][j]) return false; } for(int i = x, j = y; i < n && j < n; i++, j++){ if(board[i][j]) return false; } for(int i = x, j = y; i >= 0 && j < n; i--, j++){ if(board[i][j]) return false; } for(int i = x, j = y; i < n && j >= 0; i++, j--){ if(board[i][j]) return false; } return true; } void solve(int x){ if(cnt == n){ for(int i = 0; i < n; i++){ for(int j = 0; j < n; j++){ if(board[i][j] == 1) cout << "Q"; else cout << "x"; } cout << "\n"; } cout << "\n"; solutions++; } for(int i = 0; i < n; i++){ if(check(x, i)){ board[x][i] = 1; cnt++; solve(x + 1); board[x][i] = 0; cnt--; } } } int main(){ cin >> n; solve(0); cout << solutions; } ``` ::: ### p6. 字串MEX 給你一個僅由小寫英文字母構成的字串 S,你想知道這個字串的 MEX。 一個字串的 MEX 定義是: 1.最短且未出現在字串 S 當中的小寫英文字串。 2.如果有多個一樣短的小寫英文字串未出現在字串 S 請輸出字典序最小的。 **INPUT** 輸入第一行為一個整數 T(≤1000) 代表有 T(≤1000) 筆測資。 每筆測資有兩行,第一行為一個整數 N(N≤1000) 代表字串長度。 第二行為一個字串 S。 **OUTPUT** 請輸出其中一種解,結尾不要空格。 #### 想法 * 枚舉字串長度,由短到長 :::spoiler AC code ```cpp= #include <iostream> #include <string> using namespace std; int n; string s; bool check(string r){ // 剪枝 int len = r.size(); for(int i = 0; i < n-len+1; i++){ if(r == s.substr(i, len)) return false; } return true; } bool search(int len, string now=""){ if(len == int(now.length())){ if(check(now)){ cout << now << '\n'; return true; } else return false; } for(char c = 'a'; c <= 'z'; c++){ now += c; if(search(len, now)){ return true; } now.pop_back(); } return false; } void solve(){ for(int i = 1; i <= n; i++){ // 枚舉長度 1~n 的字串 if(search(i)){ return; } } } int main(){ int t; cin >> t; while(t--){ cin >> n >> s; solve(); } } ``` ::: ## 結語 ### 解題過程 1. 確定枚舉對象、枚舉範圍和判定條件 2. 終止條件!! 3. 枚舉可能的解 4. 驗證,不合就剪掉