🌳 Euler Tour trên cây
Biến cây thành dãy để giải truy vấn cây con, kiểm tra tổ tiên, tính tổng bằng Fenwick/Segment Tree và tìm LCA bằng RMQ.
🎯 Mục tiêu bài học
Cần hiểu
- Ý nghĩa
tin[u],tout[u],flat[]. - Vì sao subtree là một đoạn liên tiếp.
- Khác nhau giữa flatten Euler Tour và Full Euler Tour.
- Cách liên kết Euler Tour với Fenwick, Segment Tree, Sparse Table.
Khi nào dùng?
- Truy vấn/cập nhật toàn bộ cây con.
- Kiểm tra quan hệ tổ tiên.
- LCA bằng RMQ.
- Chuyển bài toán cây về bài toán mảng.
1. Đặt vấn đề: từ truy vấn trên cây đến truy vấn trên mảng
Cho một cây có
n đỉnh, gốc tại đỉnh 1. Mỗi đỉnh u có một giá trị
value[u]. Có nhiều truy vấn thuộc một trong các dạng:
- Tính tổng giá trị của tất cả đỉnh trong cây con của
u. - Cập nhật giá trị tại một đỉnh.
- Tăng giá trị của mọi đỉnh trong cây con của
u. - Kiểm tra
ucó phải tổ tiên củavhay không.
1.1. Cách làm trực tiếp
Với mỗi truy vấn cây con của u, ta có thể DFS từ u để duyệt toàn bộ hậu duệ.
Cách này đúng nhưng trong trường hợp xấu có thể phải đi qua gần như toàn bộ cây.
| Cách làm | Mỗi truy vấn | Tổng với q truy vấn | Đánh giá |
|---|---|---|---|
| DFS lại từ đỉnh u | O(size of subtree) | Có thể tới O(nq) | Dễ TLE khi n, q lớn |
| Euler Tour + Fenwick/Segment Tree | O(log n) | O((n+q)log n) | Phù hợp dữ liệu lớn |
1.2. Khó khăn cốt lõi
Fenwick Tree, Segment Tree và Prefix Sum đều hoạt động trên mảng, trong khi dữ liệu ban đầu lại là một cây. Vì vậy cần tìm cách đánh số các đỉnh sao cho toàn bộ cây con của một đỉnh trở thành một đoạn liên tiếp.
1.3. Ý tưởng Euler Tour
Ta chạy DFS từ gốc. Ngay khi đi vào một đỉnh u, gán cho nó vị trí tiếp theo trong một mảng.
Do DFS luôn duyệt xong toàn bộ cây con của u trước khi quay ra ngoài, các đỉnh trong cây con của
u sẽ nằm liên tiếp.
Bước 1
DFS từ gốc và ghi thứ tự các đỉnh khi đi vào.
Bước 2
Lưu tin[u] là vị trí đầu và tout[u] là vị trí cuối của subtree.
Bước 3
Chuyển truy vấn subtree thành truy vấn đoạn [tin[u], tout[u]].
1.4. Ví dụ nhanh
Với cây mẫu, thứ tự vào DFS là:
- Cây con của đỉnh
2gồm{2,4,5}, tương ứng đoạn[1,3]. - Cây con của đỉnh
3gồm{3,6,7}, tương ứng đoạn[4,6]. - Cây con của đỉnh
1là toàn bộ cây, tương ứng đoạn[0,6].
2. Ba cách ghi Euler Tour thường gặp
| Dạng | Cách ghi | Độ dài | Dùng cho |
|---|---|---|---|
| Entry order / Flatten | Mỗi đỉnh được ghi một lần khi DFS đi vào. | n | Subtree query/update. |
| Full Euler Tour | Ghi đỉnh khi đi vào và khi quay lại từ con. | 2n−1 | LCA bằng RMQ. |
| Entry/Exit events | Mỗi đỉnh có sự kiện vào và ra. | 2n sự kiện | Offline query, ancestor check. |
3. Mô phỏng DFS Euler Tour
Cây mẫu có các cạnh: 1-2, 1-3, 2-4, 2-5, 3-6, 3-7.
Stack DFS
Flatten
tin / tout
4. Cây con trở thành một đoạn
DFS phải duyệt xong toàn bộ hậu duệ của u trước khi quay ra ngoài, nên các đỉnh trong subtree của u nằm liên tiếp.
base[tin[u]] = value[u]. Tổng cây con của u chính là tổng đoạn [tin[u], tout[u]].5. Full Euler Tour và LCA bằng RMQ
Giữa lần xuất hiện đầu tiên của u và v, đỉnh có độ sâu nhỏ nhất chính là LCA.
6. Phân tích thuật toán
| Thành phần | Thời gian | Bộ nhớ | Ghi chú |
|---|---|---|---|
| DFS tạo tin/tout | O(n) | O(n) | Mỗi cạnh được duyệt hai lần. |
| Flatten subtree | O(n) | O(n) | Mỗi đỉnh ghi một lần. |
| Full Euler Tour | O(n) | O(n) | Dãy dài 2n−1. |
| LCA + Sparse Table | Build O(n log n), query O(1) | O(n log n) | RMQ trên độ sâu. |
| Subtree sum + Fenwick | O(log n)/query-update | O(n) | Truy vấn đoạn tin..tout. |
7. Code mẫu
#include <bits/stdc++.h>
using namespace std;
int n, timerDFS = 0;
vector<vector<int>> adj;
vector<int> tin, tout, flat;
void dfs(int u, int parent) {
tin[u] = timerDFS;
flat[timerDFS] = u;
timerDFS++;
for (int v : adj[u]) {
if (v == parent) continue;
dfs(v, u);
}
tout[u] = timerDFS - 1;
}
int main() {
cin >> n;
adj.assign(n + 1, {});
tin.assign(n + 1, 0);
tout.assign(n + 1, 0);
flat.assign(n, 0);
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
dfs(1, 0);
}struct Event {
int u, parent;
bool isExit;
};
void eulerIterative(int root) {
int timerDFS = 0;
vector<Event> st;
st.push_back({root, 0, false});
while (!st.empty()) {
Event cur = st.back();
st.pop_back();
if (cur.isExit) {
tout[cur.u] = timerDFS - 1;
continue;
}
tin[cur.u] = timerDFS;
flat[timerDFS++] = cur.u;
st.push_back({cur.u, cur.parent, true});
for (int i = (int)adj[cur.u].size() - 1; i >= 0; i--) {
int v = adj[cur.u][i];
if (v == cur.parent) continue;
st.push_back({v, cur.u, false});
}
}
}vector<int> firstPos, depth;
vector<int> euler, eulerDepth;
void dfsEuler(int u, int parent, int dep) {
depth[u] = dep;
if (firstPos[u] == -1)
firstPos[u] = (int)euler.size();
euler.push_back(u);
eulerDepth.push_back(dep);
for (int v : adj[u]) {
if (v == parent) continue;
dfsEuler(v, u, dep + 1);
euler.push_back(u);
eulerDepth.push_back(dep);
}
}def euler_flatten(adj, root=1):
n = len(adj) - 1
tin = [0] * (n + 1)
tout = [0] * (n + 1)
flat = [0] * n
timer = 0
def dfs(u, parent):
nonlocal timer
tin[u] = timer
flat[timer] = u
timer += 1
for v in adj[u]:
if v != parent:
dfs(v, u)
tout[u] = timer - 1
dfs(root, 0)
return tin, tout, flat7.1. Fenwick Tree + Euler Tour
// Lưu value[u] tại vị trí tin[u].
for (int u = 1; u <= n; u++)
bit.add(tin[u], value[u]);
// Cập nhật giá trị một đỉnh:
bit.add(tin[u], delta);
// Tổng cây con u:
long long subtreeSum = bit.rangeSum(tin[u], tout[u]);8. Giải thích code
Vì sao gán tin trước khi duyệt con?
tin[u] phải là vị trí đầu tiên của subtree u, nên được gán ngay khi DFS bước vào u.
Vì sao tout = timer − 1?
Sau khi duyệt xong subtree, timer đang chỉ vị trí trống tiếp theo. Vị trí cuối subtree là vị trí trước đó.
Vì sao bỏ qua parent?
Kiểm tra tổ tiên
Vì sao LCA trở thành RMQ?
Trong Full Euler Tour, đoạn giữa first[u] và first[v] mô tả quá trình DFS đi qua vùng nối hai nhánh. Đỉnh có depth nhỏ nhất trên đoạn là tổ tiên chung thấp nhất.
9. Ứng dụng và mở rộng
Subtree query
Fenwick/Segment Tree trên đoạn tin..tout.
Subtree update
Lazy Segment Tree để range add/range assign.
LCA
Euler Tour + Sparse Table hoặc Binary Lifting.
Path query
Euler Tour đơn thuần chưa đủ; thường dùng HLD.
Mo on Trees
Dùng Euler Tour hai lần xuất hiện cho truy vấn offline.
DSU on Tree
Kết hợp size subtree và thứ tự DFS để xử lý màu/tần suất.
10. Quiz và bài tập
Cơ bản
- In tin/tout.
- Kiểm tra tổ tiên.
- Liệt kê subtree.
Trung bình
- Subtree sum + Fenwick.
- Update điểm, query subtree.
- LCA + Sparse Table.
Nâng cao
- Subtree range add.
- Mo on Trees.
- Đếm màu phân biệt trong subtree.
HSG
- Virtual Tree.
- HLD + Euler Tour.
- Dynamic Euler Tour Tree.
💳 Quét mã ủng hộ tuỳ tâm nhé!