# 圖論 (3) ## 6/14 社課 ---- ## Outline - 歐拉路徑 - 樹重心 - 樹直徑 --- ## 歐拉路徑 一筆畫問題 ---- - 歐拉路徑:圖上每條邊洽走過一次的路徑 - 歐拉迴路:起終點向同的歐拉路徑 ---- ### 歐拉路徑存在性 無向圖: 恰存在 $0$ 或 $2$ 個度數是奇數的點 若為 $0$ 個則存在歐拉迴路 有向圖: 除了起終點之外 每個點的入度 $=$ 出度 而起點的出度會多 $1$,終點入度會多 $1$ 若所有節點入度 $=$ 出度,存在歐拉迴路 ---- ### 構造歐拉路徑 (存在歐拉路徑的條件下) 對**邊** DFS 也就是對於每個點,看他所連接的每一條邊 沒有走過就走下去 當所有可以走的邊都走完了(也就是離開一條邊) 這個點就可以丟到答案裡 最後得到的答案因為是倒過來的 將他反轉就可以得到一條歐拉路徑 ---- 實際的實作中 可以存每個點的編號 然後再開一個陣列紀錄每條邊是否走過 在有向圖中 可以開直接拿 `vector` 中的最後一個 然後直接 pop 掉 ---- ### [一筆畫問題](https://tioj.ck.tp.edu.tw/problems/1084) 求字典序最小的歐拉路徑 ---- 只要 DFS 編號最小的點就好 每進到一個點就把他的邊排序 ---- **範例 Code** ```cpp= #include<bits/stdc++.h> using namespace std; using pii=pair<int, int>; int m; vector<vector<pii>> e; vector<bool> vis; vector<int> ans; void dfs(int now){ sort(e[now].begin(), e[now].end()); // 要字典序最小 for(pii i:e[now]){ int to=i.first, id=i.second; if(vis[id]) continue; vis[id]=1; dfs(to); } // 這個點已經找沒有連著未走過的邊,加入歐拉路徑尾端 ans.push_back(now); } int main(){ while(cin >> m, m){ e.assign(501, vector<pii>()); vis.assign(m, 0); ans.clear(); for(int i=0, u, v; i<m; i++){ cin >> u >> v; // 存下每個邊的編號 e[u].push_back({v, i}); e[v].push_back({u, i}); } bool ok=0; // 是否找到歐拉路徑 for(int i=1; i<=500; i++){ if(e[i].size()%2){ // 存在奇點,設為起點 dfs(i); ok=1; break; } } if(!ok){ // 不存在奇點,找編號最小的點當起點 for(int i=1; i<=500; i++){ if(!e[i].empty()){ dfs(i); break; } } } reverse(ans.begin(), ans.end()); // 將序列反轉得到答案 for(int i:ans) cout << i << '\n'; cout << '\n'; } } ``` --- ## 樹重心 一棵樹的重心(? ---- 某個點 把它拿掉之後 剩下的連通塊點數都不超過 $\frac{N}{2}$ ---- 白話一點 一棵樹上拿掉一個點 可能會讓圖分成很多塊 這幾塊互相不連接 但每一塊的節點數量都不會大於原圖數量的一半 ---- 一棵樹上一定有重心 一棵樹最多兩個重心 ---- #### 一些酷酷的性質 - 在一棵樹上多加一個點,重心最多移動一條邊 - 將兩棵樹用一條邊連接,新的重心會在原兩棵樹重心間的路徑上 ---- ### [樹重心裸題](https://cses.fi/problemset/task/2079) 求任意樹重心 怎麼求樹重心? ---- 先隨便定根 計算每個子樹的大小 在 DFS 過程中到的每個點 若以他為根,則他的所有子樹就會是 一開始 DFS 順序中得到的他的所有子樹 以及一棵除了他的子樹外剩下的點所構成的樹 ---- 也就是說 假設以他為根的子樹大小為 $sz(v)$ 那麼假設將他定為整棵樹的樹根 他就會多出一個子樹 大小為 $N-sz(v)$ 這個動作被稱為 "**換根**" ---- 利用這個想法 我們可以在 DFS 的過程中 得到以每個點為根的樹 他的子樹大小為何 等價於將他拔掉之後 剩下的連通塊節點數量為何 若剩下的大小都不超過 $\frac{N}{2}$,他就是重心 ---- **範例 Code** ```cpp= #include<bits/stdc++.h> #define fastio ios::sync_with_stdio(0); cin.tie(0); #define F first #define S second using namespace std; using pii=pair<int, int>; int n, sz[1000005]; vector<int> e[1000005], ans; void dfs(int now, int pre){ sz[now]=1; int mx=0; // 以他為根的最大子節點子樹 for(int i:e[now]){ if(i==pre) continue; dfs(i, now); sz[now]+=sz[i]; mx=max(mx, sz[i]); } mx=max(mx, n-sz[now]); if(mx<=n/2) ans.push_back(now); // 若最大子樹大小不超過一半,這個點就是重心 } int main(){ fastio cin >> n; for(int i=0, u, v; i<n-1; i++){ cin >> u >> v; e[u].push_back(v); e[v].push_back(u); } dfs(1, 1); // 隨便定根 cout << ans[0] << '\n'; } ``` --- ## 樹直徑 最長的一條路 ---- ### 用 DP 找樹直徑 對於一個點 樹直徑一定是 - 通過他 - 在他的其中一個子樹內 其中一個 ---- 所以我們可以**分治**! 對於以每個點 $u$ 為根的子樹 有他的直徑 $dis(u)$、最大深度 $h_u$ 則 $dis(u)=\max\{dis(v), h_w+h_x+1\}$ 其中 $v,w,x$ 都是 $u$ 的子節點 ---- [裸題](https://zerojudge.tw/ShowProblem?problemid=b967) ```cpp= #include<bits/stdc++.h> #define fastio ios::sync_with_stdio(0); cin.tie(0); using namespace std; int n, dis=-1; vector<int> e[100005]; int dfs(int now, int pre){ int mx=0, se=0; for(int i:e[now]){ if(i==pre) continue; int dep=dfs(i, now); if(mx<dep){ se=mx; mx=dep; } else if(se<dep) se=dep; } dis=max(dis, mx+se); // 更新最大值 return mx+1; // 回傳子樹深度 } int main(){ fastio while(cin >> n){ dis=-1; for(int i=0; i<n; i++) e[i].clear(); for(int i=0, u, v; i<n-1; i++){ cin >> u >> v; e[u].push_back(v); e[v].push_back(u); } dfs(0, 0); cout << dis << '\n'; } } ``` ---- 樹直徑還有 $2$ 次 DFS 的做法 寫起來比較簡單 常數比 DP 作法大一點 有興趣的可以查查看
{"description":"type: slide","title":"06/14 C++社課","contributors":"[{\"id\":\"1a0296c8-ce58-4742-acda-22c02ae81a74\",\"add\":4359,\"del\":84}]"}
    72 views