不是數論
Lecture : AaW
演算法小社 [14]
很多圖論的題目都和樹有關
看出一張圖為樹、套用樹的性質來解題是種常用的技巧
之後會學到的一堆 Tarjan 演算法就會教你把圖和樹變來變去,但那是下個月ㄉ事了
樹的定義:
定義:沒有環的連通圖
根
根
樹葉
度數為一的點(不是根的那些)
選定一個點做「根」
0
3
4
3
2
2
2
2
1
1
1
對於這一點來說
父節點
子樹
對於這一點來說
沒有父節點
三棵子樹
對於這一點來說
沒有父節點
三棵子樹
對於這一點來說
沒有父節點
三棵子樹
對於這一點來說
祖先(們)
改用這點當根
如果有一張連通圖,
點樹為 n ,邊數為 n-1
一定是一棵樹!
proof is left as an exercise to the reader
複習:
vector<int> adj[maxn]; // 鄰接串列 bool visited[maxn]; // 是否到訪過 int dfs(int i) { visited[i] = true; // 在這裏做一些按照題目邊DFS邊要做的事情 for (auto v : adj[i]) { if (!visited[v]) { dfs(v); } } }
建議把模板背好喔
#include <bits/stdc++.h> using namespace std; vector<int> adj[maxn]; // 鄰接串列 vector<int> child[maxn]; // 記錄每個點的子節點為哪些 int father[maxn]; // 記錄每個點的父節點為何 void dfs(int i, int fa) { father[i] = fa; for (auto v : adj[i]) { if (v == fa) continue; child[i].push_back(v); dfs(v, i); } } int main() { // input // 假設共有n個點,編號 1~n,1為根 int n; cin >> n; for (int i = 0; i < n-1; ++i) { // n-1 條邊 int a, b; cin >> a >> b; adj[a].push_back(b); // 建立鄰接串列 adj[b].push_back(a); } int root = 1; dfs(root, 0); // 根沒有父親,用0代表 }
由於樹沒有環,
所以不用另外紀錄節點有沒有被走過
只要確定不要往回走即可
事實上
對於任意一張簡單圖(無自環、多重邊)
我們都可以透過DFS來將其轉變為類似樹的形式
但這是很久以後要講ㄉ ㄏㄏ
利用剛剛學的DFS技巧摟w
樹上距離最遠的兩點
步驟:
從任意一個點當根,找到離根最遠的點 i
從 i 當根,找到離 i 最遠的點 j
複雜度 O(v)
0
藍字為距離,假設我以一號點當 root
1
1
1
2
2
2
2
3
3
3
4
藍字為距離,改用11號點當root
4
4
5
5
5
5
6
6
0
1
2
3
0
藍字為距離,改用11號點當root
1
2
3
4
4
5
5
5
5
6
6
直徑
#include <bits/stdc++.h> using namespace std; #define ll long long #define endl '\n' const int maxn = 2e5+5; vector<int> adj[maxn]; void dfs(int i, vector<int> & dep, vector<bool> & visited) { visited[i] = 1; for (auto &v : adj[i]) { if (!visited[v]) { dep[v] = dep[i]+1; dfs(v, dep, visited); } } } signed main(){ ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); int n; cin >> n; for (int i = 0; i < n-1; ++i) { int a, b; cin >> a >> b; adj[a].push_back(b); adj[b].push_back(a); } vector<int> dep(n+1, 0); vector<bool> visited(n+1, 0); dfs(1, dep, visited); int maxDeepNode = 1; for (int i = 1; i <= n; ++i) { if (dep[i] > dep[maxDeepNode]) maxDeepNode = i; } fill(dep.begin(), dep.end(), 0); fill(visited.begin(), visited.end(), 0); dfs(maxDeepNode, dep, visited); int diameter = 0; for (auto &i : dep) diameter = max(i, diameter); cout << diameter << endl; return 0; }
TIOJ 1213 (有邊權)
也叫做樹分治
樹有著非常好的分治結構
所以當我們利用樹做分治、DP
透過在每個節點上紀錄其子樹的答案
父節點便能夠透過DFS時,子節點回傳的資訊推出該點的資訊
我們就可以維護正確的答案ㄌ
下週要學的線段樹的原理就是利用樹 DP 喔
樹深度 & 樹高度
int dep[maxn]; void dfs(int n, int fa, int d) { dep[n] = d; for (int v:adj[n]) { if (v != fa) dfs(v, n, d+1); } }
How to 解?
當節點為葉節點時,答案為1
否則,節點答案為其子節點大小總和+1
注意此題最後輸出答案時要-1,因為要去掉自己
直接看程式碼
#include <bits/stdc++.h> using namespace std; #define ll long long #define pb push_back #define endl '\n' #define _ ios::sync_with_stdio(0);cin.tie(0);cout.tie(0); const int maxn = 2e5+5; vector<int> adj[maxn]; int sz[maxn]; int dfs(int i, int fa) { sz[i] = 1; for (auto &v : adj[i]) { if (v == fa) continue; sz[i] += dfs(v, i); } return sz[i]; } signed main(){_ int n; cin >> n; for (int i = 2; i <= n; ++i) { int boss; cin >> boss; adj[i].pb(boss); adj[boss].pb(i); } dfs(1, 0); for (int i = 1; i <= n; ++i) cout << sz[i]-1 << " "; cout << endl; return 0; }
More 題目
More 簡報
最低共同祖先
若點x是點y的祖先,那麼
x=y
or
x是y的父親的祖先
兩個點u,v的最低共同祖先(LCA)就是深度最大的點x,使得x同時是u和v的祖先
如何求 LCA ?
1. 暴力DFS法
先預處理每一點的深度
然後每次詢問(u,v)時
直接從u開始DFS,紀錄到v路徑上面深度最小的那個點
複雜度 O(v)
又慢又難寫
如何求 LCA ?
2. 比較好的暴力法
先預處理每一點的深度&父親
每次拿兩個點中較深的點走到他的父親,當兩個點第一次重合的時候就是LCA。
走的步數是兩點之間的距離,最差是 O(v)
int lca (int u, int v) { if(depth[u] < depth[v]) { swap(u, v); //讓u當深度較大ㄉ點 } while(depth[u] != depth[v]) { //爬到一樣高 u = parent[v]; } while(u != v) { //一起往上爬 u = parent[u]; v = parent[v]; } return u; }
要怎麼更快地往上走 ?
假設 代表共同祖先
LCA
對於每個點,紀錄他的1,2,4,⋯,2k倍祖先。
倍增法:跳著走!
找到非共同祖先裡面深度最淺的點e
e的父親就是lca
How? 用sparse table
e
先紀錄每個點的深度以及 2k 倍祖先
要詢問a跟b的LCA的時候:
why?
LCA
e
假設 e 和 u 的距離
用二進位表示為
0001012
u
我們會依序檢查
25 :1000002
24 : 0100002
23 : 0010002
22 : 0001002
x
x
x
v
這時候我們走到22祖先,你會發覺e,u距離縮短了,且剩下的距離一定小於22
也就是說,透過這個方法,我們一定有辦法讓e,u 距離為0
u
8
1
2
4
3
5
7
6
1
2
4
3
先紀錄每個點的深度以及 2k 倍祖先
要詢問a跟b的LCA的時候:
why?
時間複雜度: O(nlogn) 預處理, O(logn)詢問
所以到底要如何預處理?
如果我們令 p[i][j]
代表第 j 個節點的第 2i 輩祖先
很酷的事情是
p[i][j]=p[ i−1 ][ p[i−1][j] ]
我們就可以在O(1)的時間找到祖先
由於總共有O(logd)個祖先
又有O(V)個點
預處理ㄉ時間複雜度 : O(Vlogd)∈O(VlogV)
➜ 第 j 個節點的第 2i 輩祖先為
第 j 個節點的第 2i−1 輩祖先的第 2i−1倍祖先
int dep[maxn]; int anc[18][maxn]; // anc[i][j] 代表 j 的 2^i 倍祖先 // 紀錄到每一點的 2^17 倍祖先 (大於1e5) void dfs(int n, int fa, int d) { anc[0][n] = fa; // 一倍祖先就是父親 dep[n] = d; for (int v : adj[n]) { if (v != fa) dfs(v, n, d+1); } } void setupLCA() { dep[0] = -1; dfs(1, 0, 0); for (int i = 1; i < 18; i++) { for (int j = 1; j <= n; j++) { anc[i][j] = anc[i-1][anc[i-1][j]]; } } } int lca(int a, int b){ if (dep[a] < dep[b]) swap(a, b); for (int i = 17;i >= 0;i--) { if (dep[anc[i][a]] >= dep[b]) { a = anc[i][a]; } } if (a == b) return a; for (int i = 17;i >= 0;i--) { if (anc[i][a] != anc[i][b]) { a = anc[i][a]; b = anc[i][b]; } } return anc[0][a]; } int main() { // 省略輸入圖 setupLCA(); while (q--) { int u, v; cin >> u >> v; cout << lca(u, v) << endl; } }
int dep[maxn]; int anc[18][maxn]; // anc[i][j] 代表 j 的 2^i 倍祖先 // 紀錄到每一點的 2^17 倍祖先 (大於1e5) void dfs(int n, int fa, int d) { anc[0][n] = fa; // 一倍祖先就是父親 dep[n] = d; for (int v : adj[n]) { if (v != fa) dfs(v, n, d+1); } } void setupLCA() { dep[0] = -1; dfs(1, 0, 0); for (int i = 1; i < 18; i++) { for (int j = 1; j <= n; j++) { anc[i][j] = anc[i-1][anc[i-1][j]]; } } } int lca(int a, int b){ if (dep[a] < dep[b]) swap(a, b); for (int i = 17;i >= 0;i--) { if (dep[anc[i][a]] >= dep[b]) { a = anc[i][a]; } } if (a == b) return a; for (int i = 17;i >= 0;i--) { if (anc[i][a] != anc[i][b]) { a = anc[i][a]; b = anc[i][b]; } } return anc[0][a]; } int main() { // 省略輸入圖 setupLCA(); while (q--) { int u, v; cin >> u >> v; cout << lca(u, v) << endl; } }
int dep[maxn]; int anc[18][maxn]; // anc[i][j] 代表 j 的 2^i 倍祖先 // 紀錄到每一點的 2^17 倍祖先 (大於1e5) void dfs(int n, int fa, int d) { anc[0][n] = fa; // 一倍祖先就是父親 dep[n] = d; for (int v : adj[n]) { if (v != fa) dfs(v, n, d+1); } } void setupLCA() { dep[0] = -1; dfs(1, 0, 0); for (int i = 1; i < 18; i++) { for (int j = 1; j <= n; j++) { anc[i][j] = anc[i-1][anc[i-1][j]]; } } } int lca(int a, int b){ if (dep[a] < dep[b]) swap(a, b); for (int i = 17;i >= 0;i--) { if (dep[anc[i][a]] >= dep[b]) { a = anc[i][a]; } } if (a == b) return a; for (int i = 17;i >= 0;i--) { if (anc[i][a] != anc[i][b]) { a = anc[i][a]; b = anc[i][b]; } } return anc[0][a]; } int main() { // 省略輸入圖 setupLCA(); while (q--) { int u, v; cin >> u >> v; cout << lca(u, v) << endl; } }
int dep[maxn]; int anc[18][maxn]; // anc[i][j] 代表 j 的 2^i 倍祖先 // 紀錄到每一點的 2^17 倍祖先 (大於1e5) void dfs(int n, int fa, int d) { anc[0][n] = fa; // 一倍祖先就是父親 dep[n] = d; for (int v : adj[n]) { if (v != fa) dfs(v, n, d+1); } } void setupLCA() { dep[0] = -1; dfs(1, 0, 0); for (int i = 1; i < 18; i++) { for (int j = 1; j <= n; j++) { anc[i][j] = anc[i-1][anc[i-1][j]]; } } } int lca(int a, int b){ if (dep[a] < dep[b]) swap(a, b); for (int i = 17;i >= 0;i--) { if (dep[anc[i][a]] >= dep[b]) { a = anc[i][a]; } } if (a == b) return a; for (int i = 17;i >= 0;i--) { if (anc[i][a] != anc[i][b]) { a = anc[i][a]; b = anc[i][b]; } } return anc[0][a]; } int main() { // 省略輸入圖 setupLCA(); while (q--) { int u, v; cin >> u >> v; cout << lca(u, v) << endl; } }
int dep[maxn]; int anc[18][maxn]; // anc[i][j] 代表 j 的 2^i 倍祖先 // 紀錄到每一點的 2^17 倍祖先 (大於1e5) void dfs(int n, int fa, int d) { anc[0][n] = fa; // 一倍祖先就是父親 dep[n] = d; for (int v : adj[n]) { if (v != fa) dfs(v, n, d+1); } } void setupLCA() { dep[0] = -1; dfs(1, 0, 0); for (int i = 1; i < 18; i++) { for (int j = 1; j <= n; j++) { anc[i][j] = anc[i-1][anc[i-1][j]]; } } } int lca(int a, int b){ if (dep[a] < dep[b]) swap(a, b); for (int i = 17;i >= 0;i--) { if (dep[anc[i][a]] >= dep[b]) { a = anc[i][a]; } } if (a == b) return a; for (int i = 17;i >= 0;i--) { if (anc[i][a] != anc[i][b]) { a = anc[i][a]; b = anc[i][b]; } } return anc[0][a]; } int main() { // 省略輸入圖 setupLCA(); while (q--) { int u, v; cin >> u >> v; cout << lca(u, v) << endl; } }
int dep[maxn]; int anc[18][maxn]; // anc[i][j] 代表 j 的 2^i 倍祖先 // 紀錄到每一點的 2^17 倍祖先 (大於1e5) void dfs(int n, int fa, int d) { anc[0][n] = fa; // 一倍祖先就是父親 dep[n] = d; for (int v : adj[n]) { if (v != fa) dfs(v, n, d+1); } } void setupLCA() { dep[0] = -1; dfs(1, 0, 0); for (int i = 1; i < 18; i++) { for (int j = 1; j <= n; j++) { anc[i][j] = anc[i-1][anc[i-1][j]]; } } } int lca(int a, int b){ if (dep[a] < dep[b]) swap(a, b); for (int i = 17;i >= 0;i--) { if (dep[anc[i][a]] >= dep[b]) { a = anc[i][a]; } } if (a == b) return a; for (int i = 17;i >= 0;i--) { if (anc[i][a] != anc[i][b]) { a = anc[i][a]; b = anc[i][b]; } } return anc[0][a]; } int main() { // 省略輸入圖 setupLCA(); while (q--) { int u, v; cin >> u >> v; cout << lca(u, v) << endl; } }
int dep[maxn]; int anc[18][maxn]; // anc[i][j] 代表 j 的 2^i 倍祖先 // 紀錄到每一點的 2^17 倍祖先 (大於1e5) void dfs(int n, int fa, int d) { anc[0][n] = fa; // 一倍祖先就是父親 dep[n] = d; for (int v : adj[n]) { if (v != fa) dfs(v, n, d+1); } } void setupLCA() { dep[0] = -1; dfs(1, 0, 0); for (int i = 1; i < 18; i++) { for (int j = 1; j <= n; j++) { anc[i][j] = anc[i-1][anc[i-1][j]]; } } } int lca(int a, int b){ if (dep[a] < dep[b]) swap(a, b); for (int i = 17;i >= 0;i--) { if (dep[anc[i][a]] >= dep[b]) { a = anc[i][a]; } } if (a == b) return a; for (int i = 17;i >= 0;i--) { if (anc[i][a] != anc[i][b]) { a = anc[i][a]; b = anc[i][b]; } } return anc[0][a]; } int main() { // 省略輸入圖 setupLCA(); while (q--) { int u, v; cin >> u >> v; cout << lca(u, v) << endl; } }
int dep[maxn]; int anc[18][maxn]; // anc[i][j] 代表 j 的 2^i 倍祖先 // 紀錄到每一點的 2^17 倍祖先 (大於1e5) void dfs(int n, int fa, int d) { anc[0][n] = fa; // 一倍祖先就是父親 dep[n] = d; for (int v : adj[n]) { if (v != fa) dfs(v, n, d+1); } } void setupLCA() { dep[0] = -1; dfs(1, 0, 0); for (int i = 1; i < 18; i++) { for (int j = 1; j <= n; j++) { anc[i][j] = anc[i-1][anc[i-1][j]]; } } } int lca(int a, int b){ if (dep[a] < dep[b]) swap(a, b); for (int i = 17;i >= 0;i--) { if (dep[anc[i][a]] >= dep[b]) { a = anc[i][a]; } } if (a == b) return a; for (int i = 17;i >= 0;i--) { if (anc[i][a] != anc[i][b]) { a = anc[i][a]; b = anc[i][b]; } } return anc[0][a]; } int main() { // 省略輸入圖 setupLCA(); while (q--) { int u, v; cin >> u >> v; cout << lca(u, v) << endl; } }
我備課好累我決定繼續偷
學弟妹記得準備學術考啦!