Tree Traversal & Flattening

🌳 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.

tin / toutThời điểm vào và kết thúc subtree
FlattenCây con thành một đoạn
Full EulerDãy dài 2n−1
LCA + RMQO(1) sau tiền xử lý

🎯 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

Bài toán mở đầu.
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 u có phải tổ tiên của v hay 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.

Cần biến subtree(u) thành một đoạn [L, R] trên mảng

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à:

flat = [1, 2, 4, 5, 3, 6, 7]
  • Cây con của đỉnh 2 gồm {2,4,5}, tương ứng đoạn [1,3].
  • Cây con của đỉnh 3 gồm {3,6,7}, tương ứng đoạn [4,6].
  • Cây con của đỉnh 1 là toàn bộ cây, tương ứng đoạn [0,6].
Kết luận đặt vấn đề: Euler Tour không trực tiếp trả lời truy vấn. Nó là bước chuyển đổi cây thành mảng để các cấu trúc dữ liệu trên đoạn có thể được áp dụng.

2. Ba cách ghi Euler Tour thường gặp

DạngCách ghiĐộ dàiDùng cho
Entry order / FlattenMỗi đỉnh được ghi một lần khi DFS đi vào.nSubtree query/update.
Full Euler TourGhi đỉnh khi đi vào và khi quay lại từ con.2n−1LCA bằng RMQ.
Entry/Exit eventsMỗi đỉnh có sự kiện vào và ra.2n sự kiệnOffline query, ancestor check.
Chú ý: đây không phải cùng một dãy. Chúng đều sinh từ DFS nhưng phục vụ mục đích khác nhau.

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.

subtree(u) ↔ flat[tin[u] ... tout[u]]
Hệ quả: đặt 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

1, 2, 4, 2, 5, 2, 1, 3, 6, 3, 7, 3, 1

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ầnThời gianBộ nhớGhi chú
DFS tạo tin/toutO(n)O(n)Mỗi cạnh được duyệt hai lần.
Flatten subtreeO(n)O(n)Mỗi đỉnh ghi một lần.
Full Euler TourO(n)O(n)Dãy dài 2n−1.
LCA + Sparse TableBuild O(n log n), query O(1)O(n log n)RMQ trên độ sâu.
Subtree sum + FenwickO(log n)/query-updateO(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, flat

7.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?

Cây lưu cạnh hai chiều. Nếu không bỏ qua cha, DFS sẽ quay ngược và lặp vô hạn.

Kiểm tra tổ tiên

u là tổ tiên của v ⇔ tin[u] ≤ tin[v] và tout[v] ≤ tout[u]

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.

Lỗi phổ biến: cho rằng mọi đường đi u-v đều là một đoạn liên tiếp trong flatten array. Chỉ subtree mới luôn là một đoạn.

10. Quiz và bài tập

Câu 1. Subtree u tương ứng đoạn nào?
Câu 2. Full Euler Tour của cây n đỉnh có độ dài?
Câu 3. Tổng subtree có update điểm nên kết hợp với?

Cơ bản

  1. In tin/tout.
  2. Kiểm tra tổ tiên.
  3. Liệt kê subtree.

Trung bình

  1. Subtree sum + Fenwick.
  2. Update điểm, query subtree.
  3. LCA + Sparse Table.

Nâng cao

  1. Subtree range add.
  2. Mo on Trees.
  3. Đếm màu phân biệt trong subtree.

HSG

  1. Virtual Tree.
  2. HLD + Euler Tour.
  3. Dynamic Euler Tour Tree.