不是數論
Lecturer : Lemon
應該蠻明顯ㄉ
樹論是圖論的一部分
其實只要是這種類似植物的名稱
通常就是圖論的一種(?
e.g. 仙人掌(可能完全不會學到w)、森林
但線段樹可能不算(?
如果下面ㄉ名詞看不懂 建議去翻之前ㄉslides
1
3
4
5
6
2
1
3
4
5
6
2
如果題目的輸入告訴你
節點數\(n\)、邊的數量\(n-1\) 且圖連通
不要懷疑,他就4一棵樹
我們用存圖的方法(adjacency list之類ㄉ)來儲存邊
接著隨便選一個點做深度優先搜尋(dfs)
並且記錄每個點的parent和child
就okㄌ
#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來將其轉變為類似樹的形式
但這是下下一次要講ㄉ ㄏㄏ
樹有著非常好的分治結構
所以當我們利用樹做分治、DP
透過在每個節點上紀錄其子樹的答案
父節點便能夠透過子節點得到資訊
我們就可以維護正確的答案ㄌ
其實就4線段樹的運作邏輯
這也就是為甚麼它叫做樹w
做法只是在每個點多存一個深度ㄉ資訊
高度也4
留給讀者自行寫扣(?
我猜應該沒有很難w
從隨便一個點(通常會選樹根)DFS找最遠的點\(f\)
從\(f\)做DFS找最遠的點\(g\)
\(dis(f, g)\)即為所求
複雜度\(O(V)\)
樹直徑 :
最遠的兩點間距離
如果你想知道為什麼ㄉ話w
不嚴謹ㄉ證明我用講ㄉ(?
樹圓心 :
和最遠的點距離最近的節點
(做樹根時高度最矮的點)
如果你還是想知道為什麼ㄉ話
證明我還是用講ㄉ
樹圓心一定在直徑上(不是因為它叫圓心)
所以在第二次DFS可以順便找出圓心
複雜度 : \(O(V)\)
lowest common ancestor
最低共同祖先
基本上就字面上ㄉ意思w
為什麼我們想知道這ㄍ咚咚(?
ans : 由於兩個點的簡單路徑唯一
所以我們只要找到LCA
就等於找到路徑ㄌouob
一個很明顯的做法
對於其中一個點做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)\)
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\)
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演算法
因為非常好演算法
複雜度非常好
差不多一樣冰淇淋
再見