樹論
不是數論
Lecturer : Lemon
圖論的樹枝 - 樹論
應該蠻明顯ㄉ
樹論是圖論的一部分
其實只要是這種類似植物的名稱
通常就是圖論的一種(?
e.g. 仙人掌(可能完全不會學到w)、森林
但線段樹可能不算(?
樹
樹(圖論)
樹的性質
如果下面ㄉ名詞看不懂 建議去翻之前ㄉslides
- 圖是一棵樹 \(\Longrightarrow\) 圖無環
- 圖是一棵樹\(\iff\)圖連通且邊數 \( = \) 節點數 \( - 1\)
- 圖是一棵樹\(\iff\)任意兩節點存在唯一的簡單路徑(不回頭)
- 圖是一棵樹\(\iff\)去掉任意一條邊則不連通
- 圖是一棵樹\(\iff\)連任意一條邊則有環
畫一棵樹
1
3
4
5
6
2
名詞定義
1
3
4
5
6
2
- 樹根 : 選定一個點做「根」
- 樹葉 : 度數為一的點(不是根的那些)
- 子樹 : 移除一個點後剩下ㄉ樹們
- 父(parent)子(child):離根較近者稱為父、 離根較遠者稱為子
- 祖先(ancestor)、後代(descendant) : 父的父的父...、子的子的子...
- 深度 : 一個點到根的距離
- 高度 : 最大ㄉ深度
建一棵樹
如果題目的輸入告訴你
節點數\(n\)、邊的數量\(n-1\) 且圖連通
不要懷疑,他就4一棵樹
我們用存圖的方法(adjacency list之類ㄉ)來儲存邊
接著隨便選一個點做深度優先搜尋(dfs)
並且記錄每個點的parent和child
就okㄌ
CODE
#include <iostream>
#include <vector>
using namespace std;
const int MAXN = 1e6 + 5;
vector<int> graph[MAXN];
vector<int> chd[MAXN];
int parent[MAXN];
void dfs(int u, int pa = -1) {
parent[u] = pa; //save parent
for(auto v : graph[u]) {
if(pa != v) { //child_node != parent_node
chd[u].push_back(v); //save children
dfs(v, u);
}
}
return;
}
int main() {
int n;
cin >> n;
for(int i = 0; i < n - 1; ++i) {
int u, v;
cin >> u >> v;
graph[u].push_back(v);
graph[v].push_back(u);
}
dfs(1); //root
for(int i = 1; i <= n; ++i) {
cout << "Node " << i << ": \n";
cout << "Parent is " << parent[i] << '\n';
cout << "Child(ren) is(are) ";
for(auto c : chd[i]) {
cout << c << ' ';
}
cout << '\n';
}
}
關於DFS
由於樹沒有環,
所以不用另外紀錄節點有沒有被走過
只要確定不要往回走即可
事實上
對於任意一張簡單圖(無自環、多重邊)
我們都可以透過DFS來將其轉變為類似樹的形式
但這是下下一次要講ㄉ ㄏㄏ
樹DP
樹有著非常好的分治結構
所以當我們利用樹做分治、DP
透過在每個節點上紀錄其子樹的答案
父節點便能夠透過子節點得到資訊
我們就可以維護正確的答案ㄌ
其實就4線段樹的運作邏輯
這也就是為甚麼它叫做樹w
深度 & 高度
做法只是在每個點多存一個深度ㄉ資訊
高度也4
留給讀者自行寫扣(?
我猜應該沒有很難w
例題
樹直徑ㄉ其中一種做法
從隨便一個點(通常會選樹根)DFS找最遠的點\(f\)
從\(f\)做DFS找最遠的點\(g\)
\(dis(f, g)\)即為所求
複雜度\(O(V)\)
樹直徑 :
最遠的兩點間距離
如果你想知道為什麼ㄉ話w
不嚴謹ㄉ證明我用講ㄉ(?
樹圓心ㄉ其中一種做法
樹圓心 :
和最遠的點距離最近的節點
(做樹根時高度最矮的點)
如果你還是想知道為什麼ㄉ話
證明我還是用講ㄉ
樹圓心一定在直徑上(不是因為它叫圓心)
所以在第二次DFS可以順便找出圓心
複雜度 : \(O(V)\)
例題
LCA
lowest common ancestor
最低共同祖先
基本上就字面上ㄉ意思w
為什麼我們想知道這ㄍ咚咚(?
ans : 由於兩個點的簡單路徑唯一
所以我們只要找到LCA
就等於找到路徑ㄌouob
Naive
一個很明顯的做法
對於其中一個點做DFS
直到碰到另外一個點
路徑上深度最小的點
就是LCAㄌ
預處理ㄉ時間複雜度 : \(O(V)\)
查詢ㄉ時間複雜度 : \(O(V)\)
好一點ㄉ做法
另一個蠻明顯ㄉ做法
我們會用上之前存ㄉparent
對於深度較大ㄉ點\(e\)、另一點\(f\)
我們發現 \(LCA (e, f) = LCA (parent(e), f)\)
if(depth[e] < depth[f]) swap(e, f); //讓e當深度較大ㄉ點
while(depth[e] != depth[f]) { //爬到一樣高
e = parent[e];
}
while(e != f) { //一起往上爬
e = parent[e];
f = parent[f];
}
就做完ㄌ
預處理ㄉ時間複雜度 : \(O(V)\)
查詢ㄉ時間複雜度 : \(O(d) \in O(V)\)
二分搜
延續上一個做法
我們發現兩點LCA的祖先也是相同ㄉ
所以深度呈現單調性
考慮對深度二分搜ouo
我們同樣先把兩點爬到一樣高
假設現在的深度\(d\)
我們只要搜\(O(log d)\)次就能找到LCAㄌ
但很快你會發現
你往上爬的時間是\(O(d)\)
預處理ㄉ時間複雜度 : \(O(V)\)
查詢ㄉ時間複雜度是很棒ㄉ\(O(d log d) \in O(V log V)\)
倍增法csw algorithm
binary lifting 或 binary jumping
但我只推薦叫binary lifting
想法很簡單 :
對於每個節點,我們儲存它的\(2^n\)輩祖先
ㄛ我要用畫ㄉ 窩不會用slides
栗子
倍增法
只要把祖先們都保存好(?
就可以在\(O(1)\)的時間搜尋
由於總共要搜\(O(log d)\)次
查詢ㄉ總複雜度 : \(O(log d) \in O(log V)\)
啊我們要怎麼存好祖先R
慢慢爬上去的時間複雜度是 \(O(V^2)\) 欸
果然還是用DFS好
倍增法
如果我們令 \(p[i][j]\)
代表第 \(i\) 個節點的第 \(2^j\) 輩祖先
很酷的事情4
\(p[i][j+1] = p[\ p[i][j]\ ][j]\)
我們就可以在\(O(1)\)的時間找到祖先
由於總共有\(O(log d)\)個祖先
又有\(O(V)\)個點
預處理ㄉ時間複雜度 : \(O(V log d) \in O(V log V)\)
倍增法
倍增法其實可以用在很多問題上
像是下禮拜ㄉ Sparse Table 也是類似的概念
當我們存取2的冪次作為區間的資料
我們在二分搜的時候
都能將時間複雜度壓在完美ㄉ\(O(log n)\)
例題
樹壓平
它很酷所以我才教ㄉ
對於一棵樹(一張圖)
我們在DFS的時候
可以記錄它被visit的時刻
和函式結束的時刻
又4我la
樹壓平
令被visit的時刻為 \(l\)
函式結束的時刻為 \(r\)
對於祖先\(u\)、後代\(v\)
顯然有 \(l_u \leq l_v \leq r_v \leq r_u\)
CODE
int l[MAXN], r[MAXN];
int timer = 0; //紀錄時間點
void dfs(int u, int pa = -1) {
l[u] = (++timer); //讓時間點+1並記錄
parent[u] = pa;
for(auto v : graph[u]) {
if(pa != v) {
chd[u].push_back(v);
dfs(v, u);
}
}
r[u] = timer; //記錄離開的時間
return;
}
用途
透過樹壓平
我們就把一個圖論問題
轉換成ㄌ相對應的區間序列問題
配合資料結構
可以達成子樹的加值、查詢等操作
例題
好多題都要樹鍊剖分QAQ
其實
上面講的很多問題都有不只一種的解法
兩個禮拜以後
Tarjan演算法
不要忘記
不要錯過
記得來小社上Tarjan演算法
因為非常好演算法
複雜度非常好
差不多一樣冰淇淋
再見
喔 下禮拜是線段樹
樹論
By lemonilemon
樹論
- 296