# 樹壓平 Euler Tour Technique
在[前面的章節](https://hackmd.io/@ShanC/Lowest-Common-Ancestor#%E8%A4%87%E7%BF%92%E6%99%82%E9%96%93%E6%88%B3%E8%A8%98)講過時間戳記,這邊就把它單獨拿來玩玩。要記得,時間戳記是 DFS 原有的性質,因此很多地方都用的到它
## 複習時間戳記
時間戳記是 DFS 本身就會有的特性之一,你應該要知道才對,因為在最基礎的[這篇](https://hackmd.io/@ShanC/bfs_dfs#%E6%99%82%E9%96%93%E6%88%B3%E8%A8%98)講過了
### 程式碼實作
我們可以在上述遞迴版本的程式碼中加入:
- $d[i]$: 發現節點 $i$ 的時間 (discovery time)
- $f[i]$ : 節點 $i$ 完成所有鄰居拜訪的時間 (finishing time)
```cpp
void dfs_time(int u, int pre) {
d[u] = ++timestamp; // 紀錄發現時間
for (int &v : g[u]) {
if (v != pre) dfs_time(v, u);
}
f[u] = ++timestamp; // 紀錄結束時間
}
```
以下圖為例,我們可以把時間戳記當作是一個計時器
<center>
<img src="https://hackmd.io/_uploads/ByPVHVv_xg.png" style="margin: 0 auto; display: block; width: 600px">
</center>
### 括號定理
把所有節點 $v$ 的發現與結束時間畫在數線上,會形成好幾個區間
<center>
<img src="https://hackmd.io/_uploads/Bkq8VG__gx.png" style="margin: 0 auto; display: block; width: 600px">
</center>
藉由上圖會發現,對於任兩節點 $u$ 與 $v$ 在樹中 :
1. 如果區間 $[d[u], f[u]]$ 包含 $[d[v], f[v]]$,則 $u$ 為 $v$ 的祖先
2. 如果區間 $[d[v], f[v]]$ 包含 $[d[u], f[u]]$,則 $u$ 為 $v$ 的後代
3. 如果區間 $[d[u], f[u]]$ 與 $[d[v], f[v]]$ 沒有交集,則 $u$ 與 $v$ 沒有血緣關係
可以得到推論 : 在一棵樹中,$d[u]<d[v]<f[v]<f[u]$ 若且唯若,$v$ 是 $u$ 的後代
## 樹壓平 Euler Tour Technique
從上圖可以觀察出來,其實用 DFS 走訪樹,就相當於在圖上走歐拉迴路 (邊不重複的迴路),Euler Tour Technique 因此得名。中文圈大家都說「樹壓平」也挺有意思,因為它真的是把樹壓平成直線的手段。樹壓平最常用來做 RMQ (區間詢問),因此最麻煩的是結合其他區間查詢的資料結構一起玩。因為我蠻討厭維護資料結構的,所以這篇不討論那堆討人厭的東西
### 直接 DFS 的問題
設樹上每個節點都有權重,很多題目都很愛 :
- 給 $q$ 次詢問
- 每次詢問中會有兩種操作 :
- 修改權種值
- 詢問某棵子樹的權重和 (or 最大/最小)
這種題目每次修改值都會影響到整棵子樹。在之前的章節其實有說過,直接 DFS 樹上前綴和、後綴和與差分可以解決這個問題,只是他們比較偏離線的作法,$O(n)$ 跑起來會很慢。如果要做線上即時處理,以競程的習慣來說應該要有個 $O(\log n)$ 的複雜度
### 原理說明
注意到,樹壓平後的每個**區間**都代表一棵**子樹**。因此
$$
\text{對子樹修改值}=\text{對區間修改值}
$$
既然可以修改區間,那就可以引入 Fenwick tree、segment tree 與根號算法來求解區間問題
## 例題說明 : [Subtree Queries](https://cses.fi/problemset/task/1137)
### 題目
給一棵有編號的樹 ($1$~$n$),$1$ 為根,每個節點都有權重。給 $q$ 比詢問,共有兩種問題 :
1. 修改節點 $s$ 的值變成 $x$
2. 求出 $s$ 為根的子樹權重和
### 限制
- $1 \le n, q \le 2 \cdot 10^5$
- $1 \le a,b, s \le n$
- $1 \le v_i, x \le 10^9$
### 題解
原理跟[剛才](#原理說明)說的一樣,首先一樣是先建立時間戳記的陣列 `d[]` 與 `f[]`
<center>
<img src="https://hackmd.io/_uploads/ryCerG1tel.png" style="margin: 0 auto; display: block; width: 600px">
</center>
接下來我們會得到一個區間,若給每個節點權重,我們可以在抵達該點的時間加上值,舉例如下圖 : 給定節點的權重分別為 $4,2,5,2,1$
<center>
<img src="https://hackmd.io/_uploads/SyavHf1txg.png" style="margin: 0 auto; display: block; width: 600px">
</center>
假設我們要查詢 $3$ 這個子樹的總和,相當於求區間 $[4, 9]$ 的總和,也就是 $5+2+1=8$
不難發現,由於離開的格子都用不到,因此可以省去離開的時間戳記
```cpp
void dfs_time(int u, int pre) {
d[u] = ++timestamp; // 紀錄發現時間
for (int &v : g[u]) {
if (v != pre) dfs_time(v, u);
}
f[u] = timestamp; // 紀錄結束時間
}
```
實際上就會長這樣
<center>
<img src="https://hackmd.io/_uploads/rk97OMktll.png" style="margin: 0 auto; display: block; width: 600px">
</center>
### 實作程式碼
`val[]` 是每個節點的權重、`MXN` 是最大節點數,其餘變數名稱都跟上面長的一樣。至於 Fenwick tree 的部分,請用你自己的模板
```cpp
/* 略 */
void dfs_time(int u, int pre) {
d[u] = ++timestamp; // 紀錄發現時間
for (int &v : g[u]) {
if (v != pre) dfs_time(v, u);
}
f[u] = timestamp; // 紀錄結束時間
}
int main() {
/* 輸入的部分略 */
dfs_time(1, 0);
BIT bit(MXN); // 這邊使用 Fenwick tree 來維護前綴和
for (int i = 1; i <= n; i++)
bit.update(d[i], val[i]);
while (q--) {
int mod, s, x;
cin >> mod;
if (mod == 1) {
cin >> s >> x;
bit.update(d[s], x - val[s]);
val[s] = x;
}
else {
cin >> s;
cout << bit.query(d[s], f[s]) << '\n';
}
}
return 0;
}
```
## 可能會採的雷
要注意樹壓平只跟「子樹」有關,如果權重的維護是在整條路徑上,那可能需要其他方法像是 (輕重) 樹鏈剖分
## 題目練習
[CSES Subtree Queries](https://cses.fi/problemset/task/1137)
[CSES Path Queries](https://cses.fi/problemset/task/1138)
[Codeforces 383C. Propagating tree](https://codeforces.com/problemset/problem/383/C)
[Codeforces 877E. Danil and a Part-time Job](https://codeforces.com/problemset/problem/877/E)
[AtCoder Beginner Contest 294G - Distance Queries on a Tree](https://atcoder.jp/contests/abc294/tasks/abc294_g)
----
## 參考資料
[海大競程 - Tree Algorithm 2](https://hackmd.io/@LeeShoWhaodian/2025I2CP_tree2#/)
----
> [ShanC 程式競賽筆記](https://hackmd.io/@ShanC/B1ouGxqcC)
> 作者: ShanC
> 更新: 2025/8/24