--- ###### tags: `2020 師大附中校隊培訓` --- # 樹 ## 2020 師大附中校隊培訓 #### joylintp #### 10.28.2020 --- ## 樹 ## Tree ---- ![](https://i.imgur.com/eRwjQLs.png) 樹(tree): 任兩點之間都相通且沒有環的圖 * 一棵有$n$個節點的樹恰有$n-1$條邊 ---- ![](https://i.imgur.com/dxdaw43.png) ### 根節點 ---- ![](https://i.imgur.com/J7jbTEC.png) ### 葉節點 ---- ![](https://i.imgur.com/vBL8X6X.png) ### 父節點 ---- ![](https://i.imgur.com/PJIJZXY.png) ### 子節點 ---- ![](https://i.imgur.com/WNdUNM0.png) ### 祖先 ---- ![](https://i.imgur.com/CW4ZDtD.png) ### 子代 ---- ![](https://i.imgur.com/maaSx8H.png) ### 子樹 ---- ![](https://i.imgur.com/mjRHKW2.png) ### 層 --- ## 樹直徑 一棵樹當中最遠的兩點間的簡單路徑 ---- ![](https://i.imgur.com/U9aMrQ9.png) ---- #### 作法 1. 選取任意點 $x$ 進行DFS 2. 找到距離 $x$ 最遠的點 $u$ 3. 再從 $u$ 點進行 DFS 4. 找到距離 $u$ 最遠的點 $v$ 5. $u$、$v$ 間的簡單路徑即為樹直徑 ---- #### 實作 ```cpp= #include<bits/stdc++.h> using namespace std; bool vis[100000]; int dis[100000]; vector<int> edge[100000]; void dfs(int rt) { vis[rt] = true; for (int i : edge[rt]) if (!vis[i]) dis[i] = dis[rt] + 1, dfs(i); } signed main() { ios_base::sync_with_stdio(false); cin.tie(0); int n; while (cin >> n) { for (int i = 0; i < n; i++) edge[i].clear(); int a, b; for (int i = 0; i < n - 1; i++) cin >> a >> b, edge[a].push_back(b), edge[b].push_back(a); for (int i = 0; i < n; i++) vis[i] = 0, dis[i] = 1e9; dis[0] = 0, dfs(0); int mx = 0, mxn = 0; for (int i = 0; i < n; i++) mx = max(mx, dis[i]); for (int i = 0; i < n; i++) if (dis[i] == mx) mxn = i; for (int i = 0; i < n; i++) vis[i] = 0, dis[i] = 1e9; dis[mxn] = 0, dfs(mxn); mx = 0; for (int i = 0; i < n; i++) mx = max(mx, dis[i]); cout << mx << '\n'; } return 0; } ``` ---- * [b967: 第 4 題 血緣關係(樹直徑)](https://zerojudge.tw/ShowProblem?problemid=b967) * [1211 . 圖論 之 最小生成樹(帶權樹直徑)](https://tioj.ck.tp.edu.tw/problems/1211) --- ## 樹重心 拔除該節點後剩餘子樹的節點數不超過原樹的 $\frac{1}{2}$,該點即為原樹的重心 ---- ![](https://i.imgur.com/iryo1RZ.png) ---- #### 作法 1. DFS求出以每個節點為根的子樹大小 2. 判斷每個點是否符合條件 ---- #### 實作 ```cpp= #include<bits/stdc++.h> using namespace std; bool vis[100000]; int n, sz[100000], par[100000]; vector<int> edge[100000]; void dfs(int u) { vis[u] = true; sz[u] = 1; for (int i : edge[u]) if (!vis[i]) par[i] = u, dfs(i), sz[u] += sz[i]; } signed main() { ios_base::sync_with_stdio(false); cin.tie(0); int T; cin >> T; while (T--) { cin >> n; for (int i = 0; i < n; i++) edge[i].clear(), sz[i] = par[i] = 0, vis[i] = false; int a, b; for (int i = 1; i < n; i++) cin >> a >> b, edge[a].push_back(b), edge[b].push_back(a); dfs(0); for (int i = 0; i < n; i++) { int mx = n - sz[i]; for (int j : edge[i]) if (j != par[i]) mx = max(mx, sz[j]); if (mx <= n / 2) { cout << i << '\n'; break; } } } return 0; } ``` ---- * [樹重心](https://neoj.sprout.tw/problem/293/) --- ## 並查集 一種資料結構,它可以支援以下操作: * 詢問某元素隸屬的集合 * 合併兩個集合 ---- 點集合在圖論上可被當成「連通塊」 → 並查集擁有查詢任兩點是否連通的功能 ---- ```cpp= int group[100001]; void init(int n) { for (int i = 1; i <= n; i++) group[i] = i; } int fnd(int a) { if (a == group[a]) return a; return group[a] = fnd(group[a]); } void uni(int a, int b) { int x = fnd(a), y = fnd(b); if (x == y) return; else group[x] = y, sz[y] += sz[x]; } ``` --- ## 最小生成樹 ### Minimum Spanning Tree 無向圖中,一個樹的子圖其包含原圖的所有節點稱為一棵生成樹 其中邊權總和最小的生成樹即為最小生成樹 ---- ### Krsukal’s algorithm 將原圖中連接一部分點的生成樹稱為最小生成子樹: * 一個單獨的點,可以視作一棵最小生成子樹 * 兩棵最小生成子樹連結成一棵最小生成子樹,以兩棵子樹之間權重最小的邊連接,顯然是最好的 * 三棵最小生成子樹連接成一棵最小生成子樹,先連結其中兩棵權重最小的子樹,再連接第三棵,總是比較好 ---- ### 貪心法,從最小權重的邊開始選起 * 若目前權重最小的邊其兩個端點隸屬於不同的集合,則選取該條邊 * 若兩個點隸屬同一集合,也就是兩點間已連通,則不選擇該條邊 複雜度 $O(E(log\ E+\alpha (V))$ ---- ### Prim's algorithm 觀察另一性質:從當前最小生成樹往外連邊權最小的邊必定是好的 * 取任意一點為起點,設為一開始的最小生成樹 * 找樹外與樹最短的邊並與目前的最小生成樹相連 複雜度 $O((V+E)log\ E)$ --- ## 最低共同祖先 ### Lowest Common Ancestor 有根樹上兩節點的最低共同祖先為其祖先的交集中深度最深者 ---- ### 時間戳記,判斷兩節點是否有祖先關係 遍歷有根樹時, 設進入節點 $i$ 的時間為 $in_i$,離開的時間為 $out_i$,可觀察到: * $u$ 是 $v$ 的祖先若且唯若 $in_u \le in_v$ 且 $out_v \le out_u$ ```cpp= void dfs(int rt) { vis[rt] = true, in[rt] = T++; for (int i : edge[rt]) if (!vis[i]) dfs(i); out[rt] = T++; } bool ancestor(int u, int v) { return (in[u] <= in[v] && out[v] <= out[u]); } ``` ---- ### 倍增法,預處理每個節點往上 $2^k$ 層祖先 ```cpp= int ac[k][n]; // 表示節點n第2^k層的祖先 for (int i = 1; i <= k; i++) for (int j = 1; j <= n; j++) ac[i][j] = ac[i - 1][ac[i - 1][j]]; // 第2^i層的祖先是第2^(i-1)層的祖先的第2^(i-1)層的祖先 ``` 建表複雜度為 $O(kn)$ → 層數不會超過$n$層,複雜度為 $O(n\ log\ n)$ ---- ### 查詢 $u$、$v$ 的 LCA 特殊情況: * 若 $u$ 為 $v$ 的祖先,則 $u$、$v$ 的 LCA 為 $u$ * 若 $v$ 為 $u$ 的祖先,則 $u$、$v$ 的 LCA 為 $v$ ---- ### 查詢 $u$、$v$ 的 LCA ### 二分搜 若 $u$ 點的第 $i$ 層祖先是 $v$ 的祖先,則 $u$ 點的第 $i+1$ 層祖先也是 $v$ 的祖先 ```cpp= int LCA(int a, int b) { if (ancestor(a, b)) return a; if (ancestor(b, a)) return b; for (int i = k; i >= 0; i--) if (!ancestor(ac[i][a], b)) a = ac[i][a]; // 提高下界,把a設為第2^i層的祖先 return ac[0][a]; } ``` 時間複雜度 $O(log\ n)$
{"metaMigratedAt":"2023-06-15T14:51:02.308Z","metaMigratedFrom":"Content","title":"樹","breaks":true,"contributors":"[{\"id\":\"6b95215f-91a7-4eaf-bcfe-d43740108f96\",\"add\":6920,\"del\":1437}]"}
    287 views