不是數論
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,⋯,2^k\)倍祖先。
倍增法:跳著走!
找到非共同祖先裡面深度最淺的點e
e的父親就是lca
How? 用sparse table
e
先紀錄每個點的深度以及 \(2^k\) 倍祖先
要詢問\(a\)跟\(b\)的LCA的時候:
why?
LCA
e
假設 e 和 u 的距離
用二進位表示為
\(000101_2\)
u
我們會依序檢查
\(2^5\) :\(100000_2\)
\(2^4\) : \(010000_2\)
\(2^3\) : \(001000_2\)
\(2^2\) : \(000100_2\)
x
x
x
v
這時候我們走到\(2^2\)祖先,你會發覺\(e, u\)距離縮短了,且剩下的距離一定小於\(2^2\)
也就是說,透過這個方法,我們一定有辦法讓\(e, u\) 距離為0
u
8
1
2
4
3
5
7
6
1
2
4
3
先紀錄每個點的深度以及 \(2^k\) 倍祖先
要詢問\(a\)跟\(b\)的LCA的時候:
why?
時間複雜度: \(O(n\log n)\) 預處理, \(O(\log n)\)詢問
所以到底要如何預處理?
如果我們令 \(p[i][j]\)
代表第 \(j\) 個節點的第 \(2^i\) 輩祖先
很酷的事情是
\(p[i][j] = p[\ i-1\ ][\ p[i-1][j]\ ]\)
我們就可以在\(O(1)\)的時間找到祖先
由於總共有\(O(\log d)\)個祖先
又有\(O(V)\)個點
預處理ㄉ時間複雜度 : \(O(V \log d) \in O(V \log V)\)
➜ 第 \(j\) 個節點的第 \(2^i\) 輩祖先為
第 \(j\) 個節點的第 \(2^{i-1}\) 輩祖先的第 \(2^{i-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;
}
}
我備課好累我決定繼續偷
學弟妹記得準備學術考啦!