【C++】競程筆記(枚舉) === [TOC] 程式碼範例參考:NTUCPC Guide,此筆記僅為個人學習用途。 --- 枚舉就是窮舉法,把所有東西都列出來。 字典序(Lexicographic order) --- 字典序,就是依照字典中出現的先後順序進行排序。 主要是用來比較和已排序序列(特別是字串)的規則,按照類似於字典中單字排列的方式進行運算。 字典序是一種排序方法,其核心思想是透過比較兩個序列的元素,從第一個元素開始,逐步向後,直到找到第一個不同的元素,然後根據這個元素的順序決定兩個序列的先後。 如果一個序列是另一個序列的前綴,則較短的序列排在前面。這種排序方式與我們在字典中查找單字的順序類似,因此被稱為「字典序」。 1. 比較開頭字元 像是 "apple" 和 "banana": 第一個字元:'a' vs 'b' 'a' 在字母表中排在 'b' 之前,因此 "apple" < "banana"。 2. 若前面字元都相同,則遍歷找兩字串中字元不同處去比較 像是 "apple" 和 "apricot": 前兩個字元相同:'a' = 'a', 'p' = 'p' 第三個字元:'p' vs 'r' 'p' 在 'r' 之前,因此 "apple" < "apricot"。 3. 一字串較短,另一字串較長 像是 "app" 和 "apple": 前三個字符相同:'a' = 'a', 'p' = 'p', 'p' = 'p' "app" 到此結束,而 "apple" 還有後續字元,因此 "app" < "apple"。 ### 字典序的比較規則 --- 1. 兩字串首元素開始比較 2. 逐個元素比較:如果目前的元素相同,則繼續比較下一個元素。 3. 遇到不同元素時決定順序:如果在某個位置上,兩個元素不同,則較小元素所在的序列排在前面(「較小」取決於元素的順序,例如字母表順序或數值大小)。 4. 處理前綴情況:如果一個序列是另一個序列的前綴(即較短序列的所有元素與較長序列的前綴完全相同),則較短的序列排在前面。 next_permutation() --- 用已排序(由小到大)的資料,產生下一組排列。 語法:`std::next_permutation(first, last);` > Returns true if the container could be rearranged to the to the lexicographical larger permutation. From : [GeeksForGeeks](https://www.geeksforgeeks.org/stdnext_permutation-prev_permutation-c/) 若容器可重新排列為按字典序更大的排列,則回傳 true。 ```cpp= #include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { vector<int> vec = {1, 2, 3}; do { for (int n : vec) cout << n << " "; cout << endl; } while (next_permutation(vec.begin(), vec.end())); return 0; } ``` 輸出結果: ``` 1 2 3 1 3 2 2 1 3 2 3 1 3 1 2 3 2 1 ``` `3! = 1*2*3 = 6`,所以有六種情形,與輸出相符。 prev_permutation() --- 對已降冪排序(由大到小)的資料,產生上一組排序。 語法:`std::prev_permutataion(first, last)` > Returns true if the container could be rearranged to the to the lexicographical smaller permutation. From : [GeeksForGeeks](https://www.geeksforgeeks.org/stdnext_permutation-prev_permutation-c/) 若容器可重新排列為字典序較小的排列,則回傳 true。 ```cpp= #include <bits/stdc++.h> using namespace std; int main() { vector<int> v = {2, 1, 3}; do { for (auto i: v) cout << i << " "; cout << endl; } while (prev_permutation(v.begin(), v.end())); return 0; } ``` 輸出結果: ``` 2 1 3 1 3 2 1 2 3 ``` 例題:彈珠配置 --- Source:https://zerojudge.tw/ShowProblem?problemid=d913 暴力窮舉法 ```cpp= #include <iostream> #include <vector> #include <algorithm> using namespace std; bool is_solution(const vector<int>& perm, const vector<vector<int>>& guesses, const vector<int>& counts) { for (int i = 0; i < 6; i++) { int match_count = 0; for (int j = 0; j < 6; j++) { if (perm[j] == guesses[i][j]) { match_count++; } } if (match_count != counts[i]) { return false; } } return true; } int main() { vector<vector<int>> guesses(6, vector<int>(6)); vector<int> counts(6); for (int i = 0; i < 6; i++) { for (int j = 0; j < 6; j++) { cin >> guesses[i][j]; } cin >> counts[i]; } // 從最小排列 {1,2,3,4,5,6} 開始 vector<int> perm = {1, 2, 3, 4, 5, 6}; do { // 若經函數處理過後發現是正確排列, 則輸出 if (is_solution(perm, guesses, counts)) { for (int num : perm) { cout << num << " "; } return 0; } } while (next_permutation(perm.begin(), perm.end())); return 0; } ``` 這個題目很像最近很紅的 minecraft 猜方塊遊戲。老闆心中會秘密選定一個特定的排序,然後叫你去猜排序,而老闆只會告訴你有幾個正確的位置,但不會告訴你是哪一個位置正確。 假設先猜 1 2 3 4 5 6,而正確答案為 4 3 2 1 5 6,老闆會告訴你兩個數字對了。 有關題目敘述,輸入部分前六個數字是 1 2 3 4 5 6 的排列,第七個數字為彈珠在正確位置的個數。 然後輸出部分,則為字典序最小的排列。 再來因為 6! 排列,可能性只有 720 種,所以可以直接暴力解。 有關以上程式,`is_solution` 函數中的參數解釋如下: * perm:目前測試的排列。 * guesses:前六次猜測的排列。 * counts:每一個猜測的正確位置數。 裡面用了雙層迴圈,主要用 perm 與 guesses 的六次猜測排列做比較,如果 `perm[j]` 跟 `guesses[i][j]` 的位置相同,則 `match_count ++`,然後經過一輪測試過後,發現 `match_count != count`(當前測試排列的正確位置數與彈珠的正確位置數不相等),就直接 false,換下一組排列,依此類推。 集合(Set) --- 若要列舉 $\{0, 1, \cdots, N - 1\}$ 的一個子集合,只需列舉 $0$ 到 $2^{N-1}$ 的所有整數。 C++ STL 中的 `<set>` 為一個關聯式容器,`<set>` 容器中儲存的元素型態必須符合以下條件: 1. 元素型態必須可以比較大小。 2. 元素型態必須可以被複製和指定。 :::info 在 C++ STL 中,關聯式容器(associative containers)是一類特殊的容器,透過鍵(key)、值(value)的關聯方式來儲存和管理元素。與序列式容器(如 vector、list)按照元素插入的順序來存儲元素不同,關聯式容器中的元素是根據 key 自動排序的,使得查找、插入和刪除操作能在時間複雜度 $O(log n)$ 內完成。 ::: 特性: * 唯一性:set 中的元素是唯一的,重複的元素將被忽略。 * 自動排序:元素會依照特定的排序準則(預設為升序)自動排列。 * 不可修改:一旦元素插入 set,其值不可直接修改,如需更改,需先刪除舊元素,再插入新元素。 語法: ```cpp #include <set> ``` ```cpp set<T, comp> s; ``` T:集合中元素的資料型態。 s:集合的名稱。 comp:為 binary predicate function(二元謂詞函數:有兩個參數並回傳 bool 的函數或函數物件),告訴 set 如何比較兩個元素。這東西是 optional 的,如果沒有提供,則集合按升序排序。 ```cpp= #include <bits/stdc++.h> using namespace std; int main() { // 建立空集合 // Creating an empty set set<int> s1; // 建立一個來自初始化列表的集合 // Creating a set from an initializer list set<int> s2 = {5, 1, 3, 2, 4}; for (auto i : s2) cout << i << " "; return 0; } ``` output: ``` 1 2 3 4 5 ``` E.g. Source:https://www.geeksforgeeks.org/set-in-cpp-stl/ 常用運算: 假設集合名字叫 set1。 * set1.insert(element):插入某元素。 * set1.erase(element):刪除某元素。 * set1.find(element):查找某元素。 * set1.size():回傳容器 set1 的長度(意即總元素數量)。 * set1.empty():檢查容器 set1 是否為空。 參考資料 === [枚舉 - NTUCPC Guide](https://guide.ntucpc.org/BasicAlgorithm/enumerate/) [C++ 容器类 <set> | 菜鸟教程](https://www.runoob.com/cplusplus/cpp-libs-set.html) [Set in C++ STL - GeeksforGeeks](https://www.geeksforgeeks.org/set-in-cpp-stl/)