[簡報](https://hackmd.io/@Lssh-Algorithm/Syn6N7izc) # 資料結構 --- ## STL ---- ## STL - `vector` - `tuple` - `algorithm` - `set` - `map` - `priority_queue` - `stringstream` ---- ### vector 多功能的陣列 ```cpp [] #include <vector> vector<int> v; v.size(); // v 的長度 v.resize(n); // 調整大小為 n v.clear(); // 初始化 v v.push_back(val); // 放 val 在 v 後面 v.begin(); v.end(); // v 的 iterator ``` ---- ### vector | vector | array | | --- | --- | | 可更動大小 | 不能更動大小 | | 宣告較複雜 | 宣告簡單 | | 功能多 | 很單純的陣列 | ---- ### vector #### construct - `vector<int> v(n, 0);` - `int`: 陣列中要放的型別 - `v`: 名字 - `n`: 長度 - `0`: 初始值 - 沒放的話則會是預設值 - 二維陣列 ``` vector<vector<int> > v(n,vector<int> (n, 0)); ``` ---- ### vector #### `push_back()` v.s. `emplace_back()` - `v.push_back(val);` - 把 `val` 的值**複製**進去 - `v.emplace_back(val);` - 把 `val` 做為**參數**來宣告 - 速度上幾乎**沒差** - 除非你 `push_back()` 一個很大的東西 ---- ### vector #### `push_back()` v.s. `emplace_back()` - `emplace_back()` 的好處在於它可以減少 「宣告」的繁瑣 ```cpp [|1|3-4|6-7] vector<pair<int, int> > arr; int main() { arr.push_back(make_pair(2, 5)); // 要先產生 pair 才可以被複製 arr.emplace_back(2, 5); // 簡潔許多 return 0; } ``` ---- ### vector #### `resize()` - `v.resize(n)` - 把 `v` 長度設為 $n$ - `v.resize(n, 0)` - 如果 `v` 需要**增加長度**,值會是 $0$ - 已存在的值**不會**被改變!! - `v.assign(n, 0)` - 把 `v` 初始化成長度為 $n$ 且值為 $0$ ---- ### vector #### `resize()` v.s. `reserve()` - `resize(n)` 可以把陣列長度設為 $n$ - `reserve(n)` 在陣列後面預留 $n$ 的空間 - `v` 的 `size` 仍不變 - 用來避免 `push_back()` 時, 可能需要花 $\mathcal{O}(n)$ 的時間搬移 `v` 到連續記憶體更長的地方 ---- ### vector #### 請不要用 `erase()`, `insert()` - vector 是連續儲存的,所以**移除**或**插入**在中間會需要 $\mathcal{O}(n)$ 的時間。 ---- ### vector #### iterator - `v.begin()`: 回傳指向第 0 個的 iterator - `v.end()`: 回傳指向最後一項的下一項的 iterator 所以 `v.begin()` 到 `v.end()` 是**左閉右開** - `v.rbegin()`: 回傳指向最後一項的 iterator - `v.rend()`: 回傳指向第 0 項的前一項的 iterator ---- ### vector #### iterator for 迴圈 ```cpp [|5|7-9] #include <vector> vector<int> v(10); int main() { for (int &i: v) cin >> i; // 輸入 10 個東西到 v for (auto it = v.begin(); it != v.end(); ++it) { cout << *it << '\n'; // 記得加 * 在變數前面來取值 } } ``` ---- ### tuple 適合儲存一組資料的東西(例如: x, y 座標) ``` cpp [|4|5|6|4, 8|5, 9] #include <tuple> #include <string> #include <vector> tuple<int, long long, string> t = {13, 53, "hello"}; pair<int, long long> p = {11, 42}; pair<int, vector<int> > pp; // 要存 vector 也可以 get<0>(t) // 回傳 t 的第 0 個東西 (13) p.first; p.second; // 分別回傳 p 的第 0 項跟第 1 項 ``` ---- ### tuple #### construct - `tuple` 最多可以存 10 樣東西 - `pair` 是特化版的 `tuple` ,只能存兩種東西 - `pair<int, long long> p = {11, 42};` - `make_pair(11, 42);` ---- ### algorithm ```cpp [] #include <algorithm> ``` - `sort()` - `lower_bound()`, `upper_bound()` - `nth_element()` - `next_permutation()` ---- ### algorithm #### `sort()` $$ \mathcal{O}(n\lg n) $$ - `sort(v.begin(), v.end());` - 把 `v.begin()` 到 `v.end()` 之間排序 - `sort(v.begin(), v.end(), cmp);` - 依照自訂的 `cmp` 函式來排序 ---- ### algorithm #### 自訂 `cmp` ```cpp [|3|3,8|3-6,9|3,10] #include <algorithm> #include <vector> vector<int> v = {1, 3, 5, 2, 4}; bool cmp(const int &x, const int &y) { return x > y; } int main() { sort(v.begin(), v.end()); // 由小到大(預設) sort(v.begin(), v.end(), cmp); // 由大到小(利用 cmp 函式) sort(v.rbegin(), v.rend()); // 也是由大到小(倒著排序) } ``` ---- ### algorithm #### `lower_bound()`, `upper_bound()` <font size=6> 二分搜 $$ \mathcal{O}(\lg n) $$ - `vector` 要事先排序好 - `lower_bound(v.begin(), v.end(), val);` - 在範圍找出 $\geq$ `val` 中,最小的 iterator - `upper_bound(v.begin(), v.end(), val);` - 在範圍找出 $\gt$ `val` 中,最小的 iterator - `{1, 2, 3, 3, 5}` `-------^-----^-` </font> ---- ### algorithm #### `nth_element()` $$ \mathcal{O}(n) $$ - `nth_element(v.begin(), v.begin() + val, v.end());` - 在範圍中,找出第 `val` 項 - 第 `val` 項之前的值都會 $\leq$ 第 `val` 項 - 第 `val` 項之後的值都會 $\geq$ 第 `val` 項 - 可以放 `cmp` - `{1, 5, 4, 2, 3} -> {1, 2, 3, 5, 4}` ---- ### algorithm #### `next_permutation()` $$ \mathcal{O}\Big(\frac{n}{2}\Big) $$ - `next_permutation(v.begin(), v.end());` - 找出下一**字典序**大的排列 - `prev_permutation()` 可以找下一小的 - 可以放 `cmp` - `{1, 2, 3, 4, 5} -> {1, 2, 3, 5, 4}` ---- ### set 沒錯,就是數學課教的**集合** ```cpp [] #include <set> set<int> s; s.insert(val); // 在 s 中放入 val s.erase(val); // 把 val 從 s 中移除 s.clear(); // 清空 s s.size(); // s 的大小 s.find(val); // 找 val 的 iterator s.count(val); // 找 val 在 s 中的數量 s.begin(); s.end(); // iterator ``` ---- ### set - 值**不能重複** (`multiset` 支援插入重複的值) - 會自動排序 - 可以快速取最大、最小值 - `unordered_set` 則不會排序 - 不能隨機取值 - 就是不行用 `s[2]` 來找第 2 小的東西啦 - 可以用 iterator 迭代 ---- ### set #### construct - `set<int> s;` - `set<int, cmp> sc;` - 依照 `cmp` 去排序 - 但是跟普通宣告 `cmp` 的方式不太一樣... ---- ### set #### construct ```cpp [|9|3-7,10] #include <set> struct cmp { bool operator() (const int &a, const int &b) const { return a > b; } }; set<int> s; // 由小到大排列 set<int, cmp> sc; // 由大到小排列 ``` ---- ### set #### `insert()` - `s.insert(val);` - 放 `val` 進 `s` 裡面 - `s.emplace(val);` - 和 vector 一樣 ---- ### set #### `erase()` - `s.erase(val);` - 在 set 中移除值為 `val` 的東西 - 回傳被移除後的個數 - `val` 也可以是 iterator - 回傳被移除後的下一個 iterator (下一個比它大的) ---- ### set #### `erase()` 毒瘤操作 ```cpp [|5|6-10|7-8|6-10] #include <set> set<int> s; int main() { s.emplace(1); s.emplace(2); s.emplace(3); for (auto it = s.begin(); it != s.end(); ) { if (*it == 2) it = s.erase(it); // 如果 it 指向 2,erase 以後會回傳 3 的 iterator else it++; } } ``` ---- ### set #### `find()` - `s.find(val);` - 回傳 `val` 的 iterator - 如果 `val` 不存在,則會回傳 `s.end()` ---- ### set #### `count()` - `s.count(val);` - 回傳 `val` 的個數 - set 同樣的值只能有一個,所以只會回傳 0 或 1 ---- ### set #### `iterator` - `s.begin()` 指向第 0 項(最小值) - `s.rbegin()` 指向最後一項(最大值) - 跟 vector 一樣,可以用 for 迴圈迭代 ---- ### map 跟字典一樣,問一個答一個 ```cpp [] #include <map> map<int, string> m; // <key, value> m[1] = "NCKU"; m[5] = "YYDS"; // 定義 m.clear(); // 清空 m.find(val); // 回傳 key = val 的 iterator m.count(val); // 回傳 key = val 的個數 m.begin(); m.end(); // iterator ``` ---- ### map - 每個元素一個 pair (`{key, value}`) - key 不能重複 (`multimap` 支援 `key` 重複) - 會自動排序 (依據 `key` 進行排序) - `unordered_map` 則不會排序 ---- ### map #### usage ```cpp [] #include <map> map<int, int> m; m[2] = 7; // 定義 m[4]++; // 如果索取的 key 未被定義,則會使用預設 // m[4] = 1 ``` ---- ### map #### `find()` - `m.find(2);` - 尋找 `key` $=$ 2 的元素 - 回傳 `pair {2, 7}` ---- ### map #### iterator ```cpp [] #include <map> map<int, int> m; for (pair<const int, int> &i: m) { cout << "key: " << i.first << ' '; cout << "value: " << i.second << '\n'; } ``` ---- ### priority_queue 跟 queue 有甚麼差別呢? ```cpp [] #include <queue> priority_queue<int> pq; pq.empty(); // 是否空了 pq.size(); // 大小 pq.top(); // pq 中,值最大的 pq.pop(); // pq 中,移除值最大的 pq.push(val); // 把 val 放入 pq 中 ``` ---- ### priority_queue #### construct <font size=6> - `priority_queue<int> pq;` - 預設是**從大到小**排列!!! - 可以使用 cmp - `priority_queue<T, vector<T>, less<T> >` - 預設為 `less<T>`,可以使用 `greater<T>` </font> ---- ```cpp struct cmp { bool operator() (int &a, int &b) { return a > b; // 跟前面的反過來,要小心 } }; priority_queue<int, vector<int>, cmp> pq; ``` ---- ### stringstream #### 字串分割、轉換好幫手 ```cpp [] #include <sstream> stringstream ss; int a, b; ss << "12 53"; // ss 寫入 "12 53" ss >> a >> b; // 從 ss 讀出,a = 12, b = 53 ss.clear(); // 清除 flag ss.str("YYDS"); // 把 ss 的內容設定為 "YYDS" ``` ---- ### stringstream #### construct - `stringstream ss` - stringstream 宣告速度不快,所以請避免重複宣告,多資源回收~ ---- ### stringstream #### 寫入讀出 - `ss << "Hello";` - 寫入 - `ss >> s;` - 讀出 - 其實就跟 `cin` 、 `cout` 一樣啦 ---- ### stringstream #### `clear()` `ss.clear()` 它只會初始化 `ss` 裡的 `flag`,`buffer` 不會清空。 #### 不會清空!!!<!-- .element: class="fragment" data-fragment-index="1" --> ### 不會清空!!!<!-- .element: class="fragment" data-fragment-index="2" --> ## 不會清空!!! <!-- .element: class="fragment" data-fragment-index="3" --> ---- ### stringstream #### `str()` - `ss.str("YYDS");` - 把 ss 的內容設成 "YYDS" - `ss.str();` - 回傳目前 ss 裡的字串 --- ## DSU ---- ### DSU 有些集合,他們互相交集為空集合, 則這些集合稱為並查集,又稱互斥集。 Note: 這些互相沒有交集的集合,彼此的聯集正好是整個宇集,也就是所有元素都恰好出現在某個集合內,不重複出現,那這些集合恰好可以用來表示點與點之間的集合關係。 ---- ### 例子 學校裡有一些幫派,每個人只會屬於一個幫派,若某人不加入別人,代表他自己就是一個單人幫派。 幫派之間會互相鬥爭,贏得一方可以把對方所有人納入自己的幫派,也就是把兩個幫派合併成一個。 這個例子可以用 DSU 來解決嗎? ---- ### 老大 剛剛說的幫派是一群人組成的,我們只知道他是一個群體,但要怎麼表示這個群體最簡單呢? 我們可以從這個幫派之中,隨便挑一個人來當老大,老大的作用就是用來代表這群體。 note: 但現在遇到了問題,如果幫派每打架一次,輸的那方所有人都要換成對方的老大,時間複雜度是 $\mathcal{O}(n)$ 為了方便,我們讓每個人心目中都各自存在一個老大。自己的老大是別人,表示你不是這個群體的代表,若自己的老大是自己,代表你就是這個幫派真正的老大,也就是代表這個幫派所有人的”關鍵人”。 我們只要透過不斷詢問老大的老大,直到問到有人的老大是自己後,才可以確定這個群體的代表人是誰。 ---- 但這樣會比較快嗎? 最差狀況還是 $\mathcal O(n)$ 查詢 note: 最差的狀況,群體內每個人的老大都有另一個老大,除了最終的老大。 這樣為了找到代表此群體的最終老大,對最下面人來說,他要查詢的時間複雜度是 $\mathcal{O}(n)$。 我們稍後會介紹如何實做能解決這個不夠好的複雜度。 ---- ### 資料結構 - `parent[n] (boss[n])` 用來表示你的上層是誰,用 `parent[i] == i` 來偵測自己是否是最上層。 - `size[n]` 用來紀錄集合的大小,只有在 index 是最終老大時才有意義。 ---- ### 精簡版陣列 - `pa_sz[n]` ```cpp= if (parent[i] != i) pa_sz[i] = parent[i]; else pa_sz[i] = -size[i]; ``` Note: 這是精簡版,把上述兩個陣列融合。 仔細注意可以發現,parent 在不是最終老大時存的是別人的 index,而最終老大的判斷方法則是用 parent[i] == i。 若我們能區分是否是最終老大,則最終老大的 parent 值存什麼也不是那麼重要ㄌ,反正我知道他是最終老大就好。 舉例來說,我可以讓每個最終老大的 parent 值都存 -1。這樣也可以達成判斷的效果。 既然如此,我們讓 pa_sz[i], 如果 i 不是最終老大,存的就是他的上層老大, 否則 i 已經是最終老大了,則讓 pa_sz[i] 存負的集合 size, 因為剛剛說過,size[i] 只有在 i 是最終老大時才是有效的 size。 我們只要判斷 parent 值是正,就知道他是最終老大, 若是負的,則取絕對值後就是這個集合的 size。 如此一來,我們只要用一個陣列就可以知道這個dsu的整體架構,且可以同時儲存他的大小。 ---- ### find - 用來尋找最終老大。剛剛說過,一個一個找,最差情況會花費 $\mathcal{O}(n)$ 的時間。 ```cpp= [|3|2,3|] int find(int index) { if(parent[index] == index) return index; return find(parent[index]); // 一路往上找 } ``` Note: 那找n次就會花費O(n^2) 太慢ㄌ ---- ### find 路徑壓縮 路徑壓縮就是在找老大的過程中 順便把路徑上所有點的老大也設成最終老大 ```cpp= [|3|2,3|] int find(int index) { if(parent[index] == index) return index; return parent[index] = find(parent[index]); // 路徑壓縮 } ``` Note: 路徑壓縮是什麼?原本要找很多層的人,在往上尋找的過程,順便把自己的老大設成比較高位的老大,最好是最終老大,因為這樣代表他下次呼叫 `find()` 時只需要花很少步或是一步就可以找到最終老大。 所以我們在 return 前把當前這個人的老大設成他老大的老大,若每個人都這樣設定,因為遞迴的中止條件是遇到最終老大,再 return 回來當其他人的老大,所以在執行一次`find()` 之後,當中詢問過的節點的老大都會設成最終老大,下次不管是問誰,只要路徑經過這些人,都可以馬上到達最終老大。 均攤複雜度是$\mathcal{O}(n\,\lg\,n)$,具體我不會估計,大概是每次壓縮平均只會壓縮 $\lg\,n$ 層。 ---- ### union 合併兩個集合a, b ```cpp= [|2,3|2-4] void Union(int a, int b) { int fa = find(a), fb = find(b); size[fa] += size[fb]; parent[fb] = fa; } ``` ---- ### 啟發式合併 把小的集合接在大的集合下面,之後find會更快 ```cpp= [|3|] void Union(int a, int b) { int fa = find(a), fb = find(b); if(size[fb] > size[fa]) swap(fa, fb); size[fa] += size[fb]; parent[fb] = fa; } ``` ---- ### 查詢時間複雜度 都沒有:$O(n)$ 路徑壓縮:$O(\lg n)$ 啟發式合併:$O(\lg n)$ 啟發式合併+路徑壓縮:$O(\alpha(n))$ 反阿卡曼函數 ---- ### 課後練習 [Kattis - Ladice](https://open.kattis.com/problems/ladice) [Kattis - Association for Control Over Minds](https://open.kattis.com/problems/control) [Codeforces - Social Network](https://codeforces.com/contest/1609/problem/D) note: 大家要去練習才知道要怎麼實際應用喔 --- ## Heap ---- ### Heap 樹狀資料結構 - min heap - max heap ---- ### 功能介紹 - `push()` - 將元素加入 heap 中 - `top()` - 從 heap 中取出最小元素 - `pop()` - 將 heap 中最小的元素去除 ---- ### 如何實現? 把資料擺成 Complete Binary Tree<!-- .element: class="fragment" data-fragment-index="1" --> 保證子樹一定不小於自己,但左右子樹大小不保證<!-- .element: class="fragment" data-fragment-index="2" --> ---- ### 示意圖 ![](https://i.imgur.com/MIZN7Da.jpg) ---- ### 樹的儲存方法 - 根節點 `idx=0` - 對於所有節點,若節點編號為 `i`,則左節點編號為 `2*i+1`,且右節點編號為 `2*i+2` ![](https://i.imgur.com/O9Iwk15.jpg =600x) ---- ### top ---- #### top - 獲得最小值 ![](https://i.imgur.com/Sc6mBrV.jpg) ---- ### push ---- #### push - 把 `0` 加入 heap 中 ![](https://i.imgur.com/MIZN7Da.jpg) ---- #### push - 把 `0` 加入 heap 中 ![](https://i.imgur.com/JMWYWUT.jpg) ---- #### push - 把 `0` 加入 heap 中 ![](https://i.imgur.com/fUUKC0B.jpg) ---- #### push - 把 `0` 加入 heap 中 ![](https://i.imgur.com/MKI6hVu.jpg) ---- #### push - 把 `0` 加入 heap 中 ![](https://i.imgur.com/NhjQRLk.jpg) ---- ### pop ---- #### pop ![](https://i.imgur.com/wqST8lV.jpg) ---- #### pop ![](https://i.imgur.com/fewUViD.jpg) ---- #### pop ![](https://i.imgur.com/OJKoYTg.jpg) ---- #### pop ![](https://i.imgur.com/F8czb8H.jpg) ---- #### pop ![](https://i.imgur.com/zO4nO0J.jpg) ---- #### pop ![](https://i.imgur.com/pH5woi1.jpg) ---- ### 複雜度分析 - top 的複雜度為 $\mathcal{O}(1)$ - push,pop 的複雜度借為 $\mathcal O(\lg\, n)$ ---- ### 實作 - [priority_queue](https://hackmd.io/@Lssh-Algorithm/Syn6N7izc#/1/35) ---- ### Problems [Kattis - Jane Eyre](https://open.kattis.com/problems/janeeyre) [Kattis - Continuous Median](https://open.kattis.com/problems/continuousmedian)
{"metaMigratedAt":"2023-06-16T22:02:37.544Z","metaMigratedFrom":"YAML","title":"STL+Heap+DSU","breaks":true,"contributors":"[{\"id\":\"60f87ada-c8bc-4f5d-9b91-2a0d3103440d\",\"add\":12969,\"del\":117}]"}
    462 views
   Owned this note