什麼是資料結構?
Queue
Stack
Deque
Linked-list
Standard Template Library
Vector, Pair, Stack, Queue, Deque
Heap (priority_queue)
Set
Map
Disjoint Set
理解常見資料結構的運作原理
能夠實作基礎的資料結構
獲得好用的工具 - STL
擔任這次營隊裡最簡單的課程
王政祺,casperwang
CKMSC36th => NTU CSIE B10
2020, 2021 資奧二階選訓營
2017 APCS 5/4 + 2020 APCS 4/5
Data Structure
是電腦中儲存、組織資料的方式
好的資料處理方式,能讓程式節省時間與空間
例如:「陣列」就是一種資料結構
Algorithms + Data Structures = Programs
Niklaus Wirth,
the designer of Pascal
Stack、Queue、Deque
Linked-list
Heap、Set、Map
disjoint set
......
堆疊
Empty
堆疊
加入新資料
堆疊
加入新資料
堆疊
刪除最頂端資料
堆疊
加入新資料
堆疊
加入新資料
堆疊
刪除最頂端資料
堆疊
刪除最頂端資料
堆疊
刪除最頂端資料
存取排在 stack 最頂端的資料
刪除排在 stack 最頂端的資料
新增資料到 stack 的最頂端
top() 回傳 stack 最頂端的值
pop() 刪除 stack 最頂端的資料
push() 將一個新的值加入 stack
size() 回傳 stack 的大小
#16
假設任意時刻 stack 裡的資料筆數不會超過陣列大小
如果 now = 0, 代表 stack 是空的
push 時 now++
pop 時 now--
9 | 9 | 8 | 2 | 4 | 4 | 3 |
---|
now
如果 now = 0, 代表 stack 是空的
push 時 now++
pop 時 now--
9 | 9 | 8 | 2 | 4 | 4 | 3 | 5 |
---|
now
如果 now = 0, 代表 stack 是空的
push 時 now++
pop 時 now--
9 | 9 | 8 | 2 | 4 | 4 | 3 |
---|
now
struct Stack{
int arr[MAXN], now;
Stack() : now(0) {}
int top() { //回傳stack最頂端的值
return arr[now-1];
}
void pop() { //刪除stack最頂端的資料
now--;
}
void push(int val) { //將一個新的值加入stack的最頂端
arr[now++] = val;
}
int size() { //回傳stack的大小
return now;
}
};
只能從最頂端存取、刪除、新增資料
後進先出(Last In First Out, LIFO)
佇列
存取排在queue最前端的資料
刪除排在queue最前端的資料
新增資料到queue的最後端
front() 回傳 queue 最前端的值
pop() 刪除 queue 最前端的資料
push() 將一個新的值加入 queue 的最後端
size() 回傳 queue 的大小
#12
用變數 head, tail 紀錄目前 queue 的開頭、結尾位置
push 時 tail++
pop 時 head++
用變數 head, tail 紀錄目前 queue 的開頭、結尾位置
push 時 tail++
pop 時 head++
9 | 7 | 9 | 3 | 2 | 3 | 8 |
---|
Head
Tail
用變數 head, tail 紀錄目前 queue 的開頭、結尾位置
push 時 tail++
pop 時 head++
7 | 9 | 3 | 2 | 3 | 8 |
---|
Head
Tail
用變數 head, tail 紀錄目前 queue 的開頭、結尾位置
push 時 tail++
pop 時 head++
7 | 9 | 3 | 2 | 3 | 8 | 4 |
---|
Head
Tail
如果 tail 碰到末端,就從頭開始循環
head 也是比照辦理
7 | 9 | 3 | 2 | 3 | 8 | 4 |
---|
Head
Tail
如果 tail 碰到末端,就從頭開始循環
head 也是比照辦理
7 | 9 | 3 | 2 | 3 | 8 | 4 | 6 |
---|
Head
Tail
如果 tail 碰到末端,就從頭開始循環
head 也是比照辦理
9 | 3 | 2 | 3 | 8 | 4 | 6 |
---|
Head
Tail
如果 tail 碰到末端,就從頭開始循環
head 也是比照辦理
2 | 9 | 3 | 2 | 3 | 8 | 4 | 6 |
---|
Head
Tail
struct Queue{
int arr[MAXN], head, tail;
Queue() : head(0), tail(0) {}
int front() { //回傳queue最前端的值
return arr[head];
}
void pop() { //刪除queue最前端的資料
head++;
if (head == MAXN) head = 0;
}
void push(int val) { //將一個新的值加入queue的最後端
arr[tail++] = val;
if (tail == MAXN) tail = 0;
}
int size() { //回傳queue的大小
return (tail + MAXN - head) % MAXN;
}
};
只能從最前端存取、刪除資料
只能從最後端新增資料
先進先出(First In First Out, FIFO)
(Double-ended queue)
雙端佇列
有些人喜歡念成「de-queue」
有些人喜歡念成「de-queue」
usually pronounced like "deck" —— by CPP reference
存取、刪除排在deque最前端的資料
存取、刪除排在deque最後端的資料
新增資料到deque的最前端、最後端
struct Deque{
int arr[MAXN], head, tail;
Deque() : head(0), tail(0) {}
int front() { //回傳deque最前端的值
return arr[head];
}
int back() { //回傳deque最後端的值
if (tail == 0) tail = MAXN
return arr[tail-1];
}
void pop_front() { //刪除deque最前端的資料
head++;
if (head == MAXN) head = 0;
}
void pop_back() { //刪除deque最後端的資料
if (tail == 0) tail = MAXN;
tail--;
}
void push_front(int val) { //將一個新的值加入deque的最前端
if (head == 0) head = MAXN-1;
arr[head--] = val;
}
void push_back(int val) { //將一個新的值加入deque的最後端
arr[tail++] = val;
if (tail == MAXN) tail = 0;
}
int size() { //回傳deque的大小
return (tail + MAXN - head) % MAXN;
}
};
front(), back() 詢問
pop_front(), pop_back() 刪除
push_front(), push_back() 加入
size()
#23
給定一個僅包含 '('、')' 的字串,問其是否為合法括弧字串。
範例:
" ( ) ( ( ) ( ) ) " 是一個合法括弧字串
" ( ) ( ( ( ) ( ) " 不是一個合法括弧字串
#17
什麼樣的字串是合法括弧字串?
「每個左括弧都能夠找到右括弧與其互相配對,且不會有多餘的右括弧沒有配對到」
由左到右把字元加到stack看看
遇到 '(' 就 push
遇到 ')' 就 pop
由左到右把字元加到stack看看
遇到 '(' 就 push
遇到 ')' 就 pop
如果 pop 的時候發現 stack 空了 => 非法字串
如果 stack 最後不是空的 => 非法字串
由左到右把字元加到stack看看
遇到 '(' 就 push
遇到 ')' 就 pop
如果 pop 的時候發現 stack 空了 => 非法字串
如果 stack 最後不是空的 => 非法字串
其實甚至不需要 stack !?
給定一個包含'('、')'、'+'、'-'的運算式,計算該運算式的答案。
(保證該運算式合法)
範例:
#18
兩個 stack,一個紀錄符號、一個紀錄數值
碰到 '(' 或結尾再做事!
11 - ( 2 - ( 3 + 2 ) ) + 5
兩個 stack,一個紀錄符號、一個紀錄數值
碰到 '(' 或結尾再做事!
5 |
---|
11 - ( 2 - ( 3 + 2 ) ) + 5
兩個 stack,一個紀錄符號、一個紀錄數值
碰到 '(' 或結尾再做事!
+ |
---|
5 |
---|
11 - ( 2 - ( 3 + 2 ) ) + 5
兩個 stack,一個紀錄符號、一個紀錄數值
碰到 '(' 或結尾再做事!
) | + |
---|
5 |
---|
11 - ( 2 - ( 3 + 2 ) ) + 5
兩個 stack,一個紀錄符號、一個紀錄數值
碰到 '(' 或結尾再做事!
) | ) | + |
---|
5 |
---|
11 - ( 2 - ( 3 + 2 ) ) + 5
兩個 stack,一個紀錄符號、一個紀錄數值
碰到 '(' 或結尾再做事!
) | ) | + |
---|
2 | 5 |
---|
11 - ( 2 - ( 3 + 2 ) ) + 5
兩個 stack,一個紀錄符號、一個紀錄數值
碰到 '(' 或結尾再做事!
+ | ) | ) | + |
---|
2 | 5 |
---|
11 - ( 2 - ( 3 + 2 ) ) + 5
兩個 stack,一個紀錄符號、一個紀錄數值
碰到 '(' 或結尾再做事!
+ | ) | ) | + |
---|
3 | 2 | 5 |
---|
11 - ( 2 - ( 3 + 2 ) ) + 5
遇到 '(' 了,開始一路計算到直到碰到 ')'
每次把一個運算符號跟兩個數字取出,算完的結果丟回去
+ | ) | ) | + |
---|
3 | 2 | 5 |
---|
11 - ( 2 - ( 3 + 2 ) ) + 5
遇到 '(' 了,開始一路計算到直到碰到 ')'
每次把一個運算符號跟兩個數字取出,算完的結果丟回去
) | + |
---|
5 | 5 |
---|
11 - ( 2 - ( 3 + 2 ) ) + 5
繼續做一樣的事情
- | ) | + |
---|
5 | 5 |
---|
11 - ( 2 - ( 3 + 2 ) ) + 5
繼續做一樣的事情
- | ) | + |
---|
2 | 5 | 5 |
---|
11 - ( 2 - ( 3 + 2 ) ) + 5
又碰到 '(' 了
- | ) | + |
---|
2 | 5 | 5 |
---|
11 - ( 2 - ( 3 + 2 ) ) + 5
又碰到 '(' 了
+ |
---|
-3 | 5 |
---|
11 - ( 2 - ( 3 + 2 ) ) + 5
繼續做一樣的事情
- | + |
---|
-3 | 5 |
---|
11 - ( 2 - ( 3 + 2 ) ) + 5
繼續做一樣的事情
- | + |
---|
11 | -3 | 5 |
---|
11 - ( 2 - ( 3 + 2 ) ) + 5
碰到結尾
- | + |
---|
11 | -3 | 5 |
---|
11 - ( 2 - ( 3 + 2 ) ) + 5
碰到結尾
+ |
---|
14 | 5 |
---|
11 - ( 2 - ( 3 + 2 ) ) + 5
得到答案
19 |
---|
11 - ( 2 - ( 3 + 2 ) ) + 5
數字不是個位數怎麼辦?
開一個變數紀錄目前的 digit 是10的幾次方,先處理好數字再丟進 stack 中
遇到 '(' 往前計算的時候不是只算一次,是一路到 ')' 並將其 pop 出來為止!
for (int i = str.size()-1; i >= 0; i--) { // 由後往前!
if (str[i] >= '0' && str[i] <= '9') {
num = num + k * (str[i] - '0');
if (i-1 >= 0 && str[i-1] >= '0' && str[i-1] <= '9') {
k = k * 10; // 如果下一位還是數字的話
} else {
stk_num.push(num);
num = 0, k = 1;
}
} else if (str[i] == '+' || str[i] == '-' || str[i] == ')') {
stk_sym.push(str[i]); // 遇到正常運算符號就 push 進 stack
} else if (str[i] == '(') {
calc(); // 遇到 '(' 就回推
}
}
calc(); // 結尾回推
cout << stk_num.top() << '\n';
void calc() {
int a = stk_num.top(), b;
stk_num.pop();
while (stk_sym.size() && stk_sym.top() != ')') {
b = stk_num.top();
stk_num.pop();
if (stk_sym.top() == '+') {
a = a + b;
} else if (stk_sym.top() == '-') {
a = a - b;
}
stk_sym.pop();
}
if (stk_sym.size()) stk_sym.pop();
// 若並非結尾,則將 ')' pop 出來
stk_num.push(a); // 將運算結果放回 stack 中
}
給你一張長條圖每個位置的高度,問你能畫出的最大矩形面積。
(\(N \leq 10^5\),高度 \(\leq 10^9\) )
範例:
8
5
#19
給你一張長條圖每個位置的高度,問你能畫出的最大矩形面積。
(\(N \leq 10^5\),高度 \(\leq 10^9\) )
範例:
9
給你一張長條圖每個位置的高度,問你能畫出的最大矩形面積。
(\(N \leq 10^5\),高度 \(\leq 10^9\) )
範例:
9
不知道從何下手的時候,可以先從複雜度較差的解開始想!
枚舉每段區間,然後看高度最高可以是多少
正確性?
複雜度?
枚舉每段區間,然後看高度最高可以是多少
正確性?應該不用懷疑吧
複雜度?\(O(N^3)\)
區間總共有 \(\frac{N(N+1)}{2}\) 個(why?
高度至多只能到最矮的那個 \(\Rightarrow\) 掃一遍區間找最小值
「一塊區間的高度至多只能到最矮的那個」
\(\Rightarrow\) 重點不是區間,是「最矮的那個」
「一塊區間的高度至多只能到最矮的那個」
\(\Rightarrow\) 重點不是區間,是「最矮的那個」
「一塊區間的高度至多只能到最矮的那個」
\(\Rightarrow\) 重點不是區間,是「最矮的那個」
「一塊區間的高度至多只能到最矮的那個」
\(\Rightarrow\) 重點不是區間,是「最矮的那個」
\(\Rightarrow\) 從枚舉區間,變成枚舉每個 bar 的高度
「一塊區間的高度至多只能到最矮的那個」
\(\Rightarrow\) 重點不是區間,是「最矮的那個」
\(\Rightarrow\) 從枚舉區間,變成枚舉每個 bar 的高度
\(\Rightarrow\) 如果我是最低的,那往左往右最多可以延伸多少?
「一塊區間的高度至多只能到最矮的那個」
\(\Rightarrow\) 重點不是區間,是「最矮的那個」
\(\Rightarrow\) 從枚舉區間,變成枚舉每個 bar 的高度
\(\Rightarrow\) 如果我是最低的,那往左往右最多可以延伸多少?
\(\Rightarrow\) 只要分別找到左右兩邊第一個比我小的!
給定序列,對每一項分別找到左右離他最近且比他小的值。
(\(N \leq 10^5\),值域 \(\leq 10^9\) )
給定序列,對每一項分別找到左右離他最近且比他小的值。
(\(N \leq 10^5\),值域 \(\leq 10^9\) )
其實等價於對每一項找到左邊離他最近且比他小的值,然後再把序列反轉過來做一次
複雜度?
給定序列,對每一項分別找到左右離他最近且比他小的值。
(\(N \leq 10^5\),值域 \(\leq 10^9\) )
其實等價於對每一項找到左邊離他最近且比他小的值,然後再把序列反轉過來做一次
複雜度?\(O(N^2)\)
但有沒有可能再做得更好一點?
來畫張圖自己玩玩看
來畫張圖自己玩玩看
假設我們還不知道答案
但答案絕對不會是它
Why?
考慮每一項在什麼時間點以後注定不可能成為答案
「如果右邊有東西不比我大,那我就不可能是答案」
「如果右邊有東西不比我大,那我就不可能是答案」
\(\Rightarrow\) 我們要用 stack 維護這樣的「單調性」
「如果右邊有東西不比我大,那我就不可能是答案」
\(\Rightarrow\) 我們要用 stack 維護這樣的「單調性」
\(\Rightarrow\) stack 裡頭的每一項一定比前一項大
「如果右邊有東西不比我大,那我就不可能是答案」
\(\Rightarrow\) 我們要用 stack 維護這樣的「單調性」
\(\Rightarrow\) stack 裡頭的每一項一定比前一項大
while (size() > 0 && top() >= value[idx]) { // top比我還大
pop(); // 把top丟掉
}
if (size() > 0) ans[idx] = top();
// 目前的top會是比我小且離我最近的那個
push(value[idx]); // 記得把值丟進stack
要找最大矩形,可以「枚舉每個值作為最小值」向外延伸
將向左、向右拆開成兩個問題
題目轉化為「找到左邊第一個比我小的值」
一個值不可能成為最小值的條件(右邊出現比它小的值)
利用 stack 維護這樣的「單調遞增」
有一群各自屬於不同小圈圈的人要排隊,當有人想加入隊伍時,若隊伍前方有同個小圈圈的人,則他會直接插進去,否則就要從最後面開始排隊。
操作包含:
編號 \(id\) 、屬於小圈圈 \(c_i\) 的人想加入隊伍
詢問隊伍最前方人的 \(id\),並讓他離開隊伍
#13
範例輸入:
"add" 3 1
"add" 2 2
"add" 1 1
"pop"
"pop"
範例輸出:
3
1
開一個紀錄小圈圈順序的 queue
對每個小圈圈各別再開一個 queue
加入跟刪除的時候要怎麼處理就交給各位了!
連結串列
對於每個資料紀錄前後資料的位置
可以 \(\mathcal{O}(1)\) 加入、刪除特定資料
不支援 random-access
不能 \(\mathcal{O}(1)\) 存取指定 index 的資料
維護一個可變動序列中,每個值所在位置的前後數字。
(值域 \(\leq 10^5\),數字不重複)
操作包含:
將一個數字 \(A\) 加入序列中的某個位置(ex. 數字 \(B\) 和 \(C\) 之間)
從序列中刪除數值 \(A\),其餘順序不變
詢問數字 \(A\) 所在位置的前後數字
對於每個數字紀錄前面是誰、後面是誰
刪除數字 \(A\) 時,假設 \(A\) 的前一個為 \(B\)、後一個為 \(C\)
把 \(B\) 的後一個改為 \(C\)
把 \(C\) 的前一個改為 \(B\)
詢問時即可直接得到答案
C++ 標準模板庫
提供了各種容器的實作(包含 stack, queue, deque)
其實就是有人幫你把一些感覺比較複雜但又會常常用到的東西寫好,讓你寫程式可以輕鬆一點(x
如果有人沒用過可以參考(?)
非必要,不過用了會變得比較輕鬆
使用mac或其他電腦裡沒有內建這個東東的人:連結
向量,不過可能要再更廣義一點
可變長度的陣列
vector <int> vec;
vector<vector<int>> vec2;
vec.push_back(123);
cout << vec.size() << endl;
vector <int> vec(50); // 方法一
vec.resize(50); // 方法二
sort(vec.begin(), vec.end());
reverse(vec.begin(), vec.end());
// 方法一
for (int i = 0; i < vec.size(); i++)
cout << vec[i] << endl;
// 方法二
for (int val : vec)
cout << val << endl;
裡頭有很多小工具,不過今天只討論 pair
相當於只有兩個參數的 struct
pair<int,int> p;
p = make_pair(5, 3);
cout << p.first << ' ' << p.second << endl;
適合很懶惰的人(ex. 講師本人)
非必要,僅供參考
#include <bits/stdc++.h>
#define pb push_back
#define pii pair<int,int>
#define ff first
#define ss second
#define 2qbx soft // 這個是亂打的,你想定義什麼都可以
using namespace std;
其實就跟昨天的內容一模一樣
stack <int> stk;
for (int i = 0; i < 10; i++) stk.push(i);
while (stk.size()) {
cout << stk.size() << " : ";
cout << stk.top() << '\n';
stk.pop();
}
queue <int> que;
for (int i = 0; i < 10; i++) que.push(i);
while (que.size()) {
cout << que.size() << " : ";
cout << que.front() << '\n';
que.pop();
}
deque <int> deq;
for (int i = 0; i < 10; i++) {
if (i % 2) deq.push_back(i);
else deq.push_front(i);
}
cout << deq.size() << '\n';
cout << deq.front() << ' ' << deq.back() << '\n';
堆積
一個能維護極值的資料結構
功能
存取、刪除 heap 最頂端的資料
(最頂端的資料為按照定義的比較函數所得到的極值)
新增資料到 heap 中
各項操作複雜度皆為 \(\mathcal{O}(\log N)\)(\(N\) 為 heap 大小)
STL 提供的 heap 叫做 priority_queue
注意預設的極值是最大值
#29
priority_queue <int> pq;
pq.push(103);
cout << pq.top() << endl;
pq.pop();
priority_queue <int, vector<int>, greater<int>> pq;
struct cmp {
bool operator()(int a, int b) {
return a%100 < b%100;
}
};
priority_queue <int, vector<int>, cmp> pq;
請維護以下操作:
將一個值加入目前的集合中
輸出最大值,並把他從集合中刪除
輸出最小值,並把他從集合中刪除
(值域 \(\leq 10^5\),數字可重複)
#30
直接開兩個 priority_queue 會出現什麼問題?
直接開兩個 priority_queue 會出現什麼問題?
\(\Rightarrow\) 沒有辦法在最小值的 priority_queue 中刪除最大值
沒有辦法在最小值的 priority_queue 中刪除最大值
\(\Rightarrow\) 多開一個陣列維護每個值的出現次數
沒有辦法在最小值的 priority_queue 中刪除最大值
\(\Rightarrow\) 多開一個陣列維護每個值的出現次數
\(\Rightarrow\) 每次一直刪,直到刪掉一個目前存在於集合中的數值
(也就是還沒被刪過的數值)
將 \(N\) 個點用 \(N-1\) 條邊相連
節點?子節點?
有根 or 無根?
有根樹
每個節點有至多兩個子節點
每一層在填滿節點之前不會進入下一層
\(N\) 個節點的樹至多只有 \(\log_2 N\) 層
用 Complete Binary Tree 來維護 Heap
設法讓每個節點的值大於其子節點
\(\mathcal{O}(1)\) 直接拿最頂端那個就好了
加入一個點時,先把他排在下一個空位置
不管怎樣直接把他換到最上面(要把原本在最上面的往下擠
while (任意左右子節點比他小) 就往下換
交換次數大約是樹的深度!
把最上面的點刪掉
不管怎樣直接把最下面的某個點換到最上面
while (任意左右子節點比他小) 就往下換
交換次數大約也是樹的深度!
我們發現在這樣的結構裡頭加入、刪除的複雜度是由樹的深度決定的
樹的深度是 \(\log_2 N\),因此這些操作的 複雜度也是 \(\log_2 N\)
集合(元素不重複)
插入資料
刪除資料
尋找資料
各項操作複雜度皆為 \(\mathcal{O}(\log N)\)(常數有點大)
和數學上的集合不一樣的地方 - set 是有序的!
#33
set <int> S;
S.insert(51);
S.erase(51); // 會順便回傳下一個元素的iterator
S.find(51) // 回傳該元素的iterator
S.count(51) // 回傳該元素是否存在
迭代器
一種資料形態,其意義為記憶空間位址,使用方法如指標
在 set 中可以利用 prev(), next() 來得到前後的 iterator
// 方法一
for (auto it = S.begin(); it != S.end(); it++)
cout << *it << endl;
// 方法二
for (int val : S)
cout << val << endl;
auto it = S.lower_bound(40);
auto it = S.upper_bound(40);
auto it = prev(S.upper_bound(40));
auto it = prev(S.lower_bound(40));
映射(key不重複)
用 key 去索引 value
插入、修改、刪除、尋找
各項操作複雜度皆為 \(\mathcal{O}(\log N)\)(常數有點大)
map 也是有序的(按照 key 排序)
#36
map <string,int> mp; // string打到int
mp["confirmed_case"] = 13240;
mp.erase("abc");
mp.find("abc");
mp.count("abc");
// 方法一
for (auto it = mp.begin(); it != mp.end(); it++)
cout << it->first << ' ' << it->second << endl;
// 方法二
for (auto [a, b] : mp)
cout << a << ' ' << b << endl;
給定一個長度為 \(N\) 的黑白格子帶,每次操作會將一個目前是白色的位置塗為黑色,請在每次操作後輸出當前最長及最短連續黑色格子的長度。
(\(N \leq 10^5\)、操作數量 \(\leq 10^5\))
#26
範例:
Ans:3, 1
範例:
Ans:3, 1
Ans:3, 2
範例:
Ans:3, 1
Ans:3, 2
Ans:6, 2
首先,我們要用一個map來記錄每個連續黑色長度的數量
對於每塊連續黑色的格子,我們在最左端及最右端紀錄這段連續黑色的長度。
每次將一個格子塗黑時,我們檢查它的左右是否已經是連續黑色的格子
每次將一個格子塗黑時,我們檢查它的左右是否已經是連續黑色的格子,若是,則將它們合併:
維持讓新連續黑色塊的最左端及最右端能夠紀錄這段連續黑色的長度
在 map 上更新這些資訊
每次將一個格子塗黑時,我們檢查它的左右是否已經是連續黑色的格子,若是,則將它們合併:
維持讓新連續黑色塊的最左端及最右端能夠紀錄這段連續黑色的長度
在 map 上更新這些資訊
每次操作完後,因為 map 是有序的,我們可以從中得到最大及最小的 key,也就是最長及最短的連續黑色格子。
當連續黑色格子長度改變在 map 上更新時,要記得把原本的長度扣回去,如果扣完後發現已經不存在該種長度的連續黑色格子,就要將它從 map 中刪掉
註:額外補充,之後圖論課才會用到
併查集
一種用來處理不交集(disjoint set)的資料結構
功能
詢問兩個元素是否在同一個集合
合併兩個元素所在的集合
在有優化的情況下各項操作複雜度皆為 \(O(\alpha(N))\) (\(\alpha \)為反阿克曼函數)
我們將每個集合以一棵樹來表示
對於每個節點只記錄他父節點的參照
我們之後將使用每個集合的根節點來代表那個集合
開一個陣列,每個 index 代表一個節點
每個節點紀錄其父節點,初始值設為自己的 index
想尋找一個節點所在的集合,我們就將這個節點一直往上指,直到碰到代表這個集合的根節點
因為 \(a\)、\(b\) 最終都指向 \(r\),因此他們在同一個集合!
想尋找一個節點所在的集合,我們就將這個節點一直往上指,直到碰到代表這個集合的根節點
int fnd(int now) {
if (dsu[now] != now)
return fnd(dsu[now]);
else // 代表遇到根了!
return now;
}
如果這棵樹長這樣
如果這棵樹長這樣
每次查詢最差是 \(O(N)\),TLE
在往上尋找根節點時,同時改變樹的結構,直接讓過程中經過的節點都指向根
在往上尋找根節點時,同時改變樹的結構,直接讓過程中經過的節點都指向根
想像原本這種最差的 \(O(N)\) 情形會怎麼樣?
程式碼剛好多十個字元而已xd
int fnd(int now) {
if (dsu[now] != now)
return dsu[now] = fnd(dsu[now]);
else
return now;
}
想合併兩個節點所在的集合,我們只要找到各自集合的根節點,再將其中一個指向對方就好!
想合併兩個節點所在的集合,我們只要找到各自集合的根節點,再將其中一個指向對方就好!
void Union(int a, int b) {
dsu[fnd(a)] = fnd(b);
// 將「a的根節點的父節點」設為「b的根節點」
}
如果多維護一下每個集合的 size,合併時讓小的集合往大的合併複雜度會比較好
void Union(int a, int b) {
a = fnd(a), b = fnd(b);
if (a == b) return;
if (sze[a] > sze[b]) {
sze[a] += sze[b];
dsu[b] = a;
} else {
sze[b] += sze[a];
dsu[a] = b;
}
}