Tree
基礎樹論
INDEX
Basics
Sparse Table
Binary Indexed Tree
Segment Tree
簡報自己讀不懂很正常他偏抽象
Basics
基本概念
樹
定義
=每個節點中間洽有一簡單路徑\\
有\ N\ 個節點和\ N - 1\ 條邊
=圖為聯通且沒有環
人工造林
電腦科學裡面的樹
根
葉子
子節點
父節點
度
祖先
子樹
深度
1
2
3
Traversal
存圖&遍歷
int n;
vector<vector<int>> adj(n + 1);
for (int i = 0; i < n - 1; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u); // 無向邊要建雙向
}
存圖
void dfs(int u, int p) {
// process node u
for (int v : adj[u]) {
if (v == p) continue; // 防止往回走
dfs(v, u);
}
}
// main
dfs(root, -1); // 通常根節點是 1 可以直接 dfs(1, -1);
遍歷
\text{DFS}
\text{lambda}
auto dfs = [&](auto self, int u, int p) -> void {
// process node u
for (int v : adj[u]) {
if (v == p) continue;
self(self, v, u);
}
}; // 記得加分號
// main
dfs(dfs, root, -1);
vector<int> dist(n + 1, -1);
queue<int> q;
dist[start] = 0;
q.push(start);
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : adj[u]) {
if (dist[v] == -1) {
dist[v] = dist[u] + 1;
q.push(v);
}
}
}
遍歷
\text{BFS}
Subtree
子樹大小
題目
給定每個人的上司,計算出每個人的下屬有幾個
\text{boss}
n
-
1
1
2
1
3
2
4
3
5
5
2
2
1
1
1
2
3
4
5
void dfs(int u, int p) {
cnt[u] = 1;
for (int v : adj[u]) {
if (v == p) continue;
dfs(v, u); // 要先處理完子節點再更新狀態
cnt[u] += cnt[v];
}
}
記錄\ cnt[u]\ 為節點\ u\ 的子樹大小
Diameter
樹直徑
題目
找出樹直徑
\text{edges}
1-2
1-3
3-4
3-5
1
2
3
4
5
兩次\ \text{DFS}
1
2
3
4
5
1
2
3
4
5
1
2
3
5
1
3
2
5
1
5
4
int s = 1, dia = 0;
vector<int> depth(n + 1);
auto dfs = [&](auto self, int u, int p) -> void {
if (depth[u] > dia) {
dia = depth[u];
s = u;
}
for (int v : adj[u]) if (v != p) {
depth[v] = depth[u] + 1;
self(self, v, u);
}
};
dfs(dfs, 1, -1);
depth[s] = 0;
dfs(dfs, s, -1);
s\ 是當前最遠點,depth\ 為深度
樹\ \text{DP}
3
7
1
2
4
6
5
auto dfs = [&](auto self, int u, int p) -> int {
int mx = 0, mx2 = 0;
for (int v : adj[u]) if (v != p) {
int h = self(self, v, u) + 1;
if (h > mx) {
mx2 = mx;
mx = h;
} else if (h > mx2) {
mx2 = h;
}
}
dia = max(dia, mx + mx2);
return mx;
};
dfs(dfs, 1, -1);
mx\ 是最大路徑,mx2\ 為次大路徑
All longest paths
全源最長路
題目
找出對於每個節點最遠點的距離
\text{edges}
1-2
1-3
3-4
3-5
1
2
3
4
5
\text{dist}
n
2
1
3
2
2
3
3
4
3
5
分為往上走或往下種兩種情況
1
2
3
4
5
auto dfs = [&](auto self, int u, int p) -> void {
for (int v : adj[u]) if (v != p) {
self(self, v, u);
if (maxDist[v] + 1 > maxDist[u]) {
secMax[u] = maxDist[u];
maxDist[u] = maxDist[v] + 1;
path[u] = v;
} else if (maxDist[v] + 1 > secMax[u]) {
secMax[u] = maxDist[v] + 1;
}
}
};
計算往子節點走的最大路徑與次大路徑
maxDist[u]\ 為節點\ u\ 的最長路徑,此時是往子節點\ path[u]
auto dfs2 = [&](auto self, int u, int p) -> void {
for (int v : adj[u]) if (v != p) {
if (path[u] == v) { // maxDist[u] is the path toward v
if (secMax[u] + 1 > maxDist[v]) {
secMax[v] = maxDist[v];
maxDist[v] = secMax[u] + 1;
path[v] = u;
} else if (secMax[u] + 1 > secMax[v]) {
secMax[v] = secMax[u] + 1;
}
} else { // maxDist[u] + 1 >= maxDist[v] in this case
// cause if not, then maxDist[u] should calculated as maxDist[v] + 1 in first dfs
secMax[v] = maxDist[v];
maxDist[v] = maxDist[u] + 1;
path[v] = u;
}
self(self, v, u);
}
};
更新往父節點走的情況
對於每個子節點都嘗試更新往上走的最大路徑
Rerooting DP
換根 DP
題目
對於每一個節點,輸出他到其他所有點的距離和
\text{edges}
1-2
1-3
3-4
3-5
1
2
3
4
5
n
\text{dist sum}
1
6=1+1+2+2
2
9=1 + 2 + 3 + 3
3
5= 1+1+1+2
4
8=1+2+2+3
5
8=1+2+2+3
+1
-1
1
2
3
4
5
1
1
2
2
6
\text{dist}
n
0
1
1
2
1
3
2
4
2
5
1
2
3
4
5
1
5
1
1
2
\text{dist}
n
1
1
2
2
0
3
1
4
1
5
\Rightarrow dp[v] = dp[u] + (n - sz[v]) - sz[v]
1
2
3
4
5
1
5
1
1
2
\text{dist}
n
1
1
2
2
0
3
1
4
1
5
+1
-1
令\ dp[u]\ 為節點\ u\ 到所有節點的距離和\\
sz[u]\ 為節點\ u\ 的子樹大小
往上的節點數(非子樹)
auto dfs = [&](auto self, int u, int p, int dist) -> void {
ans[1] += dist;
sz[u] = 1; // 計算子樹大小
for (int v : adj[u]) if (v != p) {
self(self, v, u, dist + 1);
sz[u] += sz[v];
}
};
計算子樹大小和根節點為\ 1\ 時的答案
換根\ \text{DP}\rightarrow dp[v] = dp[u] + (n - sz[v]) - sz[v]
auto dfs2 = [&](auto self, int u, int p) -> void {
for (int v : adj[u]) if (v != p) {
ans[v] = ans[u] - sz[v] + (n - sz[v]);
self(self, v, u);
}
};
Binary Lifting
倍增法
題目
定義\ succ(x, k)\ 為節點\ x\ 往上\ k\ 的節點
succ(x, k) =
\begin{cases}
succ(x, 1)& k=1\\
succ(succ(x, k / 2), k / 2)& k>1\\
\end{cases}
給定每個人的從屬關係和\ Q\ 個詢問:x\ 上面\ k\ 層的的上司是誰?
1
4
2
1
5
往上的路徑一定會形成一條鏈
7
6
9
1
4
2
1
5
7
6
9
succ(9, 5) = succ(succ(9, 4), 0)=succ(4, 1)=2
x
9
7
6
5
4
2
1
succ(x, 1)
7
6
5
4
2
1
-
succ(x, 2)
6
5
4
2
1
-
-
succ(x, 4)
4
2
1
-
-
-
-
定義\ succ[k][u]\ 為節點\ u\ 往上走\ 2^k\ 的點
\Rightarrow succ[k][u] = succ[k - 1][succ[k - 1][u]]
vector<vector<int>> succ(__lg(n) + 1, vector<int>(n + 1));
vector<int> depth(n + 1);
auto dfs = [&](auto self, int u) -> void {
for (int v : adj[u]) {
succ[0][v] = u;
depth[v] = depth[u] + 1;
for (int i = 1; i <= __lg(depth[v]); i++) {
succ[i][v] = succ[i - 1][succ[i - 1][v]];
}
self(self, v);
}
};
O(\log n)\ 預處理
for (int i = lg(k); i >= 0; i--) {
if (k & (1 << i)) {
x = succ[i][x];
}
}
O(\log n)\ 查詢
LCA
最低共通祖先
題目
給定每個人的從屬關係和\ Q\ 個詢問:距離\ a\ 和\ b\ 最近的共通祖先為何
3
7
1
2
4
6
5
8
倍增法
3
7
1
2
4
6
5
8
倍增法
3
7
1
2
4
6
5
8
auto lca = [&](int x, int y) -> int {
if (depth[x] < depth[y]) swap(x, y);
while (depth[x] > depth[y]) {
x = succ[lg(depth[x] - depth[y])][x];
}
if (x == y) return x; // 如果定位到同深度時已經找到 要先返回
for (int i = lg(depth[x]); i >= 0; i--) {
if (succ[i][x] != succ[i][y]) {
x = succ[i][x], y = succ[i][y];
}
}
return succ[0][x];
};
depth\ 為深度,succ\ 為倍增表
歐拉迴路
3
7
1
2
4
6
5
8
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
node\ id
depth
1
1
2
2
5
3
2
2
6
3
8
4
6
3
2
2
1
1
3
2
1
1
4
2
7
3
4
2
1
1
歐拉迴路
3
7
1
2
4
6
5
8
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
node\ id
depth
1
1
2
2
5
3
2
2
6
3
8
4
6
3
2
2
1
1
3
2
1
1
4
2
7
3
4
2
1
1
vector<int> timer(2 * n + 1), id(n + 1), depth(n + 1);
auto dfs = [&](auto self, int u) -> void {
id[u] = cnt;
timer[cnt++] = u;
for (int v : adj[u]) {
depth[v] = depth[u] + 1;
self(self, v);
timer[cnt++] = u;
}
};
歐拉迴路
vector<vector<int>> st(lg(cnt) + 1, vector<int>(cnt + 1));
for (int i = 1; i <= cnt; i++) st[0][i] = i;
for (int i = 1; i <= lg(cnt); i++) {
int len = 1 << (i - 1);
for (int j = 1; j + len <= cnt; j++) {
int a = st[i - 1][j], b = st[i - 1][j + len];
if (depth[timer[a]] <= depth[timer[b]]) st[i][j] = a;
else st[i][j] = b;
}
}
O(n \log n)\ 建最小值稀疏表
int l = id[a], r = id[b];
if (l > r) swap(l, r);
int k = lg(r - l + 1);
a = st[k][l], b = st[k][r - (1 << k) + 1];
if (depth[timer[a]] <= depth[timer[b]]) {
cout << timer[a] << '\n';
} else cout << timer[b] << '\n';
O(1)\ 查詢
Flatten
樹壓平
題目
給定一顆樹,有兩種操作:將節點\ s\ 的值修改成 \ x\ /\ 查詢節點\ s\ 的子樹值之和
1
2
3
4
5
start
end
node\ id
0
5
1
2
2
5
3
4
4
5
1
2
3
4
5
auto dfs = [&](auto self, int u, int p) -> void {
st[u] = ++cnt;
for (int v : adj[u]) if (v != p) {
self(self, v, u);
}
en[u] = cnt;
};
st[u]\ 為節點\ u\ 起始時間,en[u]\ 為結束時間
BIT tree(n);
for (int i = 1; i <= n; i++) {
tree.modify(st[i], v[i]);
}
套資結&初始化
if (o == 1) { // modify
int x;
cin >> x;
tree.modify(st[s], x);
} else {
cout << tree.query(en[s]) - tree.query(st[s] - 1) << '\n';
}
改值&查詢
掰掰
基礎樹論
By keaucucal
基礎樹論
樹
- 33