Chương 3·Bài 31

Euler Tour

Câu hỏi trên cây con (tổng, đếm, cập nhật cả nhánh) trông khó vì cây không có thứ tự tuyến tính. Một lần DFS cho mỗi đỉnh một thời điểm vào tintin: đánh số các đỉnh theo thứ tự lần đầu gặp. Khi đó mọi cây con là một đoạn liền trong thứ tự này. Câu hỏi trên cây con trở thành câu hỏi trên đoạn của một mảng, và mọi cấu trúc dữ liệu đoạn đã học (tổng tiền tố, Fenwick, cây phân đoạn, Mo) dùng được ngay.

Thời điểm vào ra và kích thước cây con

Nhắc lại vài chữ về cây. Cây là đồ thị liên thông không có chu trình; cây nn đỉnh có đúng n−1n - 1 cạnh. Chọn một đỉnh làm gốc thì mỗi đỉnh khác có đúng một cha, là đỉnh kề nó trên đường đi về gốc. Các đỉnh nằm phía dưới uu, kể cả uu, tạo thành cây con của uu; trừ chính uu ra thì đó là các hậu duệ của uu. Độ sâu của một đỉnh là số cạnh từ gốc tới nó.

DFS từ gốc, gán tin[u] = ++timer khi vừa vào uu, và tout[u] = timer khi đã duyệt xong mọi con của uu. Các đỉnh được đánh số trong lúc ở trong cây con của uu chính là các đỉnh của cây con đó, và chúng nhận các số liên tiếp từ tinutin_u tới toututout_u. Vậy:

  • Cây con của uu là đoạn [tinu,toutu][tin_u, tout_u] trong mảng ord, với ord[tin[u]] = u.
  • Kích thước cây con là szu=toutu−tinu+1sz_u = tout_u - tin_u + 1.
  • uu là tổ tiên của vv (hoặc u=vu = v) khi và chỉ khi tinu≤tinv≤toututin_u \le tin_v \le tout_u.
Số trên đỉnh là tin/tout. Bấm một đỉnh: cây con của nó là một đoạn liền trong mảng ord.
C++
void dfs(ll u, ll p) {
    tin[u] = ++timer;
    ord[timer] = u;
    for (ll v : g[u])
        if (v != p) dfs(v, u);
    tout[u] = timer;
}

Đệ quy sâu tới nn tầng khi cây là một đường; với n=2⋅105n = 2 \cdot 10^5 thường vẫn ổn trên máy chấm Linux, nhưng nếu lo tràn ngăn xếp thì tự giữ ngăn xếp.

Ví dụSơ Đồ Cấp Dưới

Đề. Cây n≤2⋅105n \le 2 \cdot 10^5 đỉnh gốc 1, cho cha của mỗi đỉnh 2..n2..n (cha không nhất thiết có chỉ số nhỏ hơn). In số hậu duệ của từng đỉnh.

Lời giải. DFS từ 1 gán tin[u] lúc vào và tout[u] lúc ra (số thứ tự lớn nhất trong cây con). Cây con của uu chiếm đúng các số tinu..toututin_u..tout_u, nên có toutu−tinu+1tout_u - tin_u + 1 đỉnh, và trừ chính uu còn toutu−tinutout_u - tin_u hậu duệ. Cha có thể có chỉ số lớn hơn con nên không tính được bằng một vòng lặp ngược chỉ số; DFS lo việc đó.

Bước 1: danh sách con. Đề cho cha của từng đỉnh, nên g[p] chỉ chứa con của pp và dfs không cần tham số cha.

C++
for (ll v = 2; v <= n; v++) {
    ll p;
    cin >> p;
    g[p].push_back(v);
}

Bước 2: DFS gán thời điểm.

C++
void dfs(ll u) {
    tin[u] = ++timer;
    for (ll v : g[u]) dfs(v);
    tout[u] = timer;
}

Đệ quy sâu tới 2⋅1052 \cdot 10^5 khi cây là một đường; mức đó thường vẫn chạy được, nhưng cây sâu hơn thì phải chuyển sang ngăn xếp tường minh.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 2e5 + 5;
ll n, timer, tin[N], tout[N];
vector<ll> g[N];

void dfs(ll u) {
    tin[u] = ++timer;
    for (ll v : g[u]) dfs(v);
    tout[u] = timer;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll v = 2; v <= n; v++) {
        ll p;
        cin >> p;
        g[p].push_back(v);
    }
    dfs(1);
    for (ll u = 1; u <= n; u++) cout << tout[u] - tin[u] << ' ';
    cout << '\n';
}
7
1 1 3 3 5 1

O(n)O(n).

Ví dụQuan Hệ Trên Cây

Đề. Cây n≤105n \le 10^5 đỉnh gốc 1, q≤105q \le 10^5 câu hỏi (u,v)(u, v) với u≠vu \ne v: in MA nếu uu là tổ tiên của vv, RI nếu vv là tổ tiên của uu, SA nếu không.

Lời giải. Một lần DFS tính tintin, touttout; mỗi câu hỏi so hai đoạn trong O(1)O(1). Đoạn cây con của hai đỉnh hoặc lồng nhau (một đỉnh là tổ tiên của đỉnh kia), hoặc rời nhau (hai nhánh khác nhau), không bao giờ chồng một phần.

Bước 1: DFS trên cây cho bằng cạnh. Cạnh thêm vào hai đầu, nên dfs mang theo đỉnh cha để không đi ngược lên.

C++
void dfs(ll u, ll p) {
    tin[u] = ++timer;
    for (ll v : g[u])
        if (v != p) dfs(v, u);
    tout[u] = timer;
}

Bước 2: kiểm tổ tiên. uu là tổ tiên của vv (hoặc chính vv) khi tin[v] nằm trong đoạn của uu. Đề cho u≠vu \ne v nên không lẫn với trường hợp bằng nhau. Thử hai chiều; cả hai sai là hai nhánh khác nhau.

C++
bool anc(ll u, ll v) {
    return tin[u] <= tin[v] && tin[v] <= tout[u];
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 1e5 + 5;
ll n, q, timer, tin[N], tout[N];
vector<ll> g[N];

void dfs(ll u, ll p) {
    tin[u] = ++timer;
    for (ll v : g[u])
        if (v != p) dfs(v, u);
    tout[u] = timer;
}

bool anc(ll u, ll v) {
    return tin[u] <= tin[v] && tin[v] <= tout[u];
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> q;
    for (ll i = 1; i < n; i++) {
        ll a, b;
        cin >> a >> b;
        g[a].push_back(b);
        g[b].push_back(a);
    }
    dfs(1, 0);
    while (q--) {
        ll u, v;
        cin >> u >> v;
        if (anc(u, v)) cout << "MA\n";
        else if (anc(v, u)) cout << "RI\n";
        else cout << "SA\n";
    }
}
8 6
1 2
1 3
2 4
2 5
3 6
6 7
6 8
2 5
5 2
4 5
1 8
7 3
5 8

DFS O(n)O(n), mỗi câu hỏi O(1)O(1).

Cây con là một đoạn

Đặt giá trị của đỉnh uu vào ô tinutin_u của một mảng. Tổng trên cây con của uu là tổng đoạn [tinu,toutu][tin_u, tout_u]; đổi giá trị một đỉnh là sửa một ô. Fenwick (bài 16) trả lời cả hai trong O(log⁡n)O(\log n). Thứ tự các con khi DFS quyết định thứ tự trong mảng: muốn thứ tự duyệt cố định (chẳng hạn con nhỏ trước), sắp danh sách con trước khi DFS.

Hỏi nhanh

Cây có tinu=4tin_u = 4 và toutu=9tout_u = 9. Điều nào sai?

Ví dụVị Trí Trong Cây Con

Đề. Cây n≤2⋅105n \le 2 \cdot 10^5 đỉnh gốc 1, cha pv<vp_v < v. Preorder là thứ tự thăm đỉnh trước rồi mới lần lượt tới các cây con của nó; ở đây duyệt các con theo chỉ số tăng dần. Mỗi câu hỏi (u,k)(u, k): in đỉnh thứ kk trong preorder của cây con gốc uu, hoặc −1-1.

Lời giải. Preorder của cả cây là thứ tự các đỉnh theo tin. Preorder của cây con uu là đoạn con liền của nó, từ vị trí tinutin_u tới toututout_u. Vậy ghi ord[t] là đỉnh có tin = t, và đáp án là ord[tin[u] + k - 1] nếu kk không vượt kích thước cây con.

Bước 1: danh sách con đã sắp. Đọc vv từ 2 tới nn theo thứ tự tăng và đẩy vào g[p], nên danh sách con tự sắp tăng, đúng thứ tự preorder đề đòi.

C++
for (ll v = 2; v <= n; v++) {
    ll p;
    cin >> p;
    g[p].push_back(v);
}

Bước 2: DFS ghi thứ tự.

C++
void dfs(ll u) {
    tin[u] = ++timer;
    ord[timer] = u;
    for (ll v : g[u]) dfs(v);
    tout[u] = timer;
}

Bước 3: trả lời. tout[u] - tin[u] + 1 là kích thước cây con; kk lớn hơn thì in −1-1.

C++
if (k > tout[u] - tin[u] + 1) cout << -1 << '\n';
else cout << ord[tin[u] + k - 1] << '\n';
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 2e5 + 5;
ll n, q, timer, tin[N], tout[N], ord[N];
vector<ll> g[N];

void dfs(ll u) {
    tin[u] = ++timer;
    ord[timer] = u;
    for (ll v : g[u]) dfs(v);
    tout[u] = timer;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> q;
    for (ll v = 2; v <= n; v++) {
        ll p;
        cin >> p;
        g[p].push_back(v);
    }
    dfs(1);
    while (q--) {
        ll u, k;
        cin >> u >> k;
        if (k > tout[u] - tin[u] + 1) cout << -1 << '\n';
        else cout << ord[tin[u] + k - 1] << '\n';
    }
}
9 7
1 1 2 2 3 3 6 6
1 1
1 6
2 3
3 4
6 3
6 4
7 1

DFS O(n)O(n), mỗi câu hỏi O(1)O(1).

Ví dụTổng Trong Cây Con

Đề. Cây n≤2⋅105n \le 2 \cdot 10^5 đỉnh có giá trị, q≤2⋅105q \le 2 \cdot 10^5 thao tác: gán giá trị một đỉnh, hoặc in tổng giá trị trên cây con của một đỉnh.

Lời giải. Trải cây thành mảng theo thứ tự tintin: cây con của ss thành đoạn [tins,touts][tin_s, tout_s]. Bài thành “gán một điểm, hỏi tổng một đoạn” trên mảng, giải bằng Fenwick. Tổng tới 2⋅10142 \cdot 10^{14}, cần ll.

Bước 1: DFS và đặt giá trị. Sau DFS, đặt a[i] vào ô tin[i] của Fenwick.

C++
dfs(1, 0);
for (ll i = 1; i <= n; i++) update(tin[i], a[i]);

Bước 2: gán. Fenwick chỉ cộng được, nên cộng phần chênh x - a[s] rồi nhớ giá trị mới vào a[s].

C++
update(tin[s], x - a[s]);
a[s] = x;

Bước 3: tổng cây con. Tổng đoạn bằng hai tổng tiền tố.

C++
cout << get(tout[s]) - get(tin[s] - 1) << '\n';
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 2e5 + 5;
ll n, q, timer, a[N], tin[N], tout[N], bit[N];
vector<ll> g[N];

void update(ll pos, ll val) {
    for (; pos <= n; pos += pos & -pos) bit[pos] += val;
}

ll get(ll pos) {
    ll res = 0;
    for (; pos > 0; pos -= pos & -pos) res += bit[pos];
    return res;
}

void dfs(ll u, ll p) {
    tin[u] = ++timer;
    for (ll v : g[u])
        if (v != p) dfs(v, u);
    tout[u] = timer;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> q;
    for (ll i = 1; i <= n; i++) cin >> a[i];
    for (ll i = 1; i < n; i++) {
        ll u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }
    dfs(1, 0);
    for (ll i = 1; i <= n; i++) update(tin[i], a[i]);
    while (q--) {
        ll t, s;
        cin >> t >> s;
        if (t == 1) {
            ll x;
            cin >> x;
            update(tin[s], x - a[s]);
            a[s] = x;
        } else cout << get(tout[s]) - get(tin[s] - 1) << '\n';
    }
}
6 6
5 1 4 2 7 3
1 2
1 3
2 4
2 5
3 6
2 2
1 5 10
2 1
2 3
1 1 8
2 1

Mỗi thao tác O(log⁡n)O(\log n).

Đường từ gốc: cập nhật cây con, hỏi điểm

Tổng trên đường từ gốc tới ss là tổng các tổ tiên của ss cùng chính nó. Đổi góc nhìn: giá trị của đỉnh uu đóng góp vào đáp án của mọi đỉnh trong cây con của uu. Vậy cộng aua_u lên cả đoạn [tinu,toutu][tin_u, tout_u], và đáp án của ss là giá trị tại một điểm tinstin_s. Fenwick trên mảng hiệu làm được cả hai: cộng đoạn là hai lần cộng điểm, hỏi điểm là một tổng tiền tố. Hai cách nhìn “cập nhật điểm, hỏi cây con” và “cập nhật cây con, hỏi điểm” là đối ngẫu của nhau.

Hỏi nhanh

Để tính tổng trên đường từ gốc tới ss, khi đổi giá trị của đỉnh uu thêm xx ta cập nhật gì?

Ví dụTổng Trên Đường Từ Gốc

Đề. Cây n≤2⋅105n \le 2 \cdot 10^5 đỉnh có giá trị, q≤2⋅105q \le 2 \cdot 10^5 thao tác: gán giá trị một đỉnh, hoặc in tổng giá trị trên đường từ gốc 1 tới một đỉnh.

Lời giải. Đảo vai: thay vì hỏi tổng của các tổ tiên của ss, cho mỗi đỉnh uu “phát” giá trị aua_u xuống cả cây con của nó. Khi đó giá trị nhận được tại ss là tổng aa của mọi đỉnh có cây con chứa ss, tức mọi tổ tiên của ss và chính ss. “Cộng cả cây con” là cộng một đoạn [tinu,toutu][tin_u, tout_u], “đọc giá trị tại ss” là đọc một điểm: Fenwick trên mảng hiệu làm cả hai trong O(log⁡n)O(\log n).

Bước 1: cộng một cây con. Mảng hiệu: +val+val ở đầu đoạn, −val-val ngay sau cuối đoạn; giá trị tại một điểm là tổng tiền tố tới điểm đó.

C++
void add(ll u, ll val) {
    update(tin[u], val);
    update(tout[u] + 1, -val);
}

Bước 2: khởi tạo và gán. Ban đầu mỗi đỉnh góp a[u] cho cả cây con của mình. Gán giá trị mới thì cộng phần chênh lên đúng cây con của đỉnh đó.

C++
for (ll i = 1; i <= n; i++) add(i, a[i]);
C++
add(s, x - a[s]);
a[s] = x;

Bước 3: câu hỏi. Tổng trên đường từ gốc tới ss là get(tin[s]).

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 2e5 + 5;
ll n, q, timer, a[N], tin[N], tout[N], bit[N];
vector<ll> g[N];

void update(ll pos, ll val) {
    for (; pos <= n; pos += pos & -pos) bit[pos] += val;
}

ll get(ll pos) {
    ll res = 0;
    for (; pos > 0; pos -= pos & -pos) res += bit[pos];
    return res;
}

void add(ll u, ll val) {
    update(tin[u], val);
    update(tout[u] + 1, -val);
}

void dfs(ll u, ll p) {
    tin[u] = ++timer;
    for (ll v : g[u])
        if (v != p) dfs(v, u);
    tout[u] = timer;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> q;
    for (ll i = 1; i <= n; i++) cin >> a[i];
    for (ll i = 1; i < n; i++) {
        ll u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }
    dfs(1, 0);
    for (ll i = 1; i <= n; i++) add(i, a[i]);
    while (q--) {
        ll t, s;
        cin >> t >> s;
        if (t == 1) {
            ll x;
            cin >> x;
            add(s, x - a[s]);
            a[s] = x;
        } else cout << get(tin[s]) << '\n';
    }
}
6 6
5 1 4 2 7 3
1 2
1 3
2 4
2 5
3 6
2 5
1 2 6
2 4
2 6
1 1 1
2 6

Mỗi thao tác O(log⁡n)O(\log n).

Cập nhật cả cây con

Khi thao tác sửa mọi đỉnh của một cây con và câu hỏi cũng trên cây con, cả hai đều là đoạn: cây phân đoạn lười (bài 17) trên mảng theo thứ tự tintin. Đảo trạng thái cả nhánh là đảo một đoạn; gán màu cả nhánh là gán một đoạn. Cây con nhỏ hay lớn đều tốn O(log⁡n)O(\log n).

Ví dụĐảo Trạng Thái Trong Nhánh

Đề. Cây n≤2⋅105n \le 2 \cdot 10^5 đỉnh, mỗi đỉnh bật hoặc tắt. q≤2⋅105q \le 2 \cdot 10^5 lệnh: pow v đảo mọi đỉnh trong cây con của vv, get v đếm số đỉnh bật trong cây con của vv.

Lời giải. Trải cây theo tintin: pow v thành “đảo mọi phần tử của đoạn [tinv,toutv][tin_v, tout_v]”, get v thành “đếm số 1 trên đoạn đó”. Đây đúng là bài “Dãy Công Tắc” của bài 17, chạy trên mảng trải phẳng.

Bước 1: trải trạng thái theo DFS. val[tin[u]] = t[u], rồi build dựng cây phân đoạn trên mảng đó.

C++
void dfs(ll u) {
    tin[u] = ++timer;
    val[timer] = t[u];
    for (ll v : g[u]) dfs(v);
    tout[u] = timer;
}

Bước 2: thẻ đảo. Đảo cả đoạn của một nút: số đỉnh bật thành độ dài trừ số bật, và lật cờ lz để nhớ hai con chưa bị đảo. Hai lần đảo triệt tiêu nhau nên cờ là XOR.

C++
void apply(ll id, ll l, ll r) {
    st[id] = (r - l + 1) - st[id];
    lz[id] ^= 1;
}

void down(ll id, ll l, ll r) {
    if (!lz[id]) return;
    ll mid = (l + r) >> 1;
    apply(id * 2, l, mid);
    apply(id * 2 + 1, mid + 1, r);
    lz[id] = 0;
}

Bước 3: hai lệnh. update và get là cây lười chuẩn trên đoạn [tinv,toutv][tin_v, tout_v].

C++
if (op == "pow") update(1, 1, n, tin[v], tout[v]);
else cout << get(1, 1, n, tin[v], tout[v]) << '\n';
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 2e5 + 5;
ll n, q, timer, t[N], tin[N], tout[N], val[N], st[4 * N], lz[4 * N];
vector<ll> g[N];

void dfs(ll u) {
    tin[u] = ++timer;
    val[timer] = t[u];
    for (ll v : g[u]) dfs(v);
    tout[u] = timer;
}

void build(ll id, ll l, ll r) {
    if (l == r) {
        st[id] = val[l];
        return;
    }
    ll mid = (l + r) >> 1;
    build(id * 2, l, mid);
    build(id * 2 + 1, mid + 1, r);
    st[id] = st[id * 2] + st[id * 2 + 1];
}

void apply(ll id, ll l, ll r) {
    st[id] = (r - l + 1) - st[id];
    lz[id] ^= 1;
}

void down(ll id, ll l, ll r) {
    if (!lz[id]) return;
    ll mid = (l + r) >> 1;
    apply(id * 2, l, mid);
    apply(id * 2 + 1, mid + 1, r);
    lz[id] = 0;
}

void update(ll id, ll l, ll r, ll u, ll v) {
    if (v < l || r < u) return;
    if (u <= l && r <= v) {
        apply(id, l, r);
        return;
    }
    down(id, l, r);
    ll mid = (l + r) >> 1;
    update(id * 2, l, mid, u, v);
    update(id * 2 + 1, mid + 1, r, u, v);
    st[id] = st[id * 2] + st[id * 2 + 1];
}

ll get(ll id, ll l, ll r, ll u, ll v) {
    if (v < l || r < u) return 0;
    if (u <= l && r <= v) return st[id];
    down(id, l, r);
    ll mid = (l + r) >> 1;
    return get(id * 2, l, mid, u, v) + get(id * 2 + 1, mid + 1, r, u, v);
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll v = 2; v <= n; v++) {
        ll p;
        cin >> p;
        g[p].push_back(v);
    }
    for (ll i = 1; i <= n; i++) cin >> t[i];
    dfs(1);
    build(1, 1, n);
    cin >> q;
    while (q--) {
        string op;
        ll v;
        cin >> op >> v;
        if (op == "pow") update(1, 1, n, tin[v], tout[v]);
        else cout << get(1, 1, n, tin[v], tout[v]) << '\n';
    }
}
7
1 1 2 2 3 6
1 0 1 1 0 0 1
9
get 1
get 2
pow 2
get 2
pow 1
get 1
get 3
pow 6
get 6

Mỗi lệnh O(log⁡n)O(\log n).

Thêm chiều độ sâu

Câu hỏi “trong cây con của uu, ở độ sâu dd” là giao của hai điều kiện: tintin nằm trong [tinu,toutu][tin_u, tout_u], và độ sâu bằng dd. Gom các đỉnh theo độ sâu, mỗi nhóm giữ danh sách tintin tăng dần (DFS đẩy vào theo thứ tự tintin nên tự sắp). Số đỉnh của nhóm dd thuộc cây con uu là số phần tử của danh sách nằm trong đoạn, tìm bằng hai lần tìm kiếm nhị phân. Các đỉnh cùng độ sâu trong một cây con cũng nằm liền nhau trong danh sách của tầng, nên có thể giữ thêm tổng tiền tố hay mặt nạ XOR theo từng tầng.

Bấm Theo độ sâu: mỗi tầng là danh sách tin tăng dần, các đỉnh của một cây con trên tầng ấy nằm liền nhau.

Ví dụĐếm Hậu Duệ Theo Độ Sâu

Đề. Cây n≤2⋅105n \le 2 \cdot 10^5 đỉnh gốc 1, pv<vp_v < v; q≤2⋅105q \le 2 \cdot 10^5 câu hỏi (u,d)(u, d): đếm số đỉnh trong cây con của uu có độ sâu (tính từ gốc) bằng dd.

Lời giải. Đỉnh độ sâu dd trong cây con của uu là các đỉnh độ sâu dd có tin nằm trong [tinu,toutu][tin_u, tout_u]. Gom tin của các đỉnh theo độ sâu thành từng danh sách đã sắp; mỗi câu hỏi đếm số phần tử của một danh sách nằm trong một đoạn bằng hai lần chặt nhị phân.

Bước 1: DFS gom theo độ sâu. lv[d] chứa các tin của đỉnh độ sâu dd. DFS gán tin tăng dần và đẩy ngay, nên mỗi danh sách tự sắp tăng.

C++
void dfs(ll u) {
    tin[u] = ++timer;
    lv[dep[u]].push_back(tin[u]);
    for (ll v : g[u]) {
        dep[v] = dep[u] + 1;
        dfs(v);
    }
    tout[u] = timer;
}

Bước 2: đếm trong đoạn. upper_bound(tout) trừ lower_bound(tin) là số phần tử trong [tinu,toutu][tin_u, tout_u]. dd nhỏ hơn độ sâu của uu hay lớn hơn độ sâu lớn nhất đều tự cho 0, vì danh sách rỗng hoặc không phần tử nào trong đoạn.

C++
cout << upper_bound(lv[d].begin(), lv[d].end(), tout[u]) - lower_bound(lv[d].begin(), lv[d].end(), tin[u]) << '\n';
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 2e5 + 5;
ll n, q, timer, tin[N], tout[N], dep[N];
vector<ll> g[N], lv[N];

void dfs(ll u) {
    tin[u] = ++timer;
    lv[dep[u]].push_back(tin[u]);
    for (ll v : g[u]) {
        dep[v] = dep[u] + 1;
        dfs(v);
    }
    tout[u] = timer;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll v = 2; v <= n; v++) {
        ll p;
        cin >> p;
        g[p].push_back(v);
    }
    dfs(1);
    cin >> q;
    while (q--) {
        ll u, d;
        cin >> u >> d;
        cout << upper_bound(lv[d].begin(), lv[d].end(), tout[u]) - lower_bound(lv[d].begin(), lv[d].end(), tin[u]) << '\n';
    }
}
9
1 1 2 2 3 3 6 6
6
1 2
2 2
3 3
6 2
6 1
8 3

DFS O(n)O(n), mỗi câu hỏi O(log⁡n)O(\log n).

Câu hỏi đoạn ngoại tuyến trên mảng Euler

Khi mọi câu hỏi trên cây con biết trước, chúng là các câu hỏi đoạn trên một dãy tĩnh, và mọi kỹ thuật ngoại tuyến cho dãy đều dùng được: Mo (bài 25), quét đầu phải với Fenwick, gộp nhỏ vào lớn. Đếm số giá trị khác nhau trên đoạn có cách quét quen thuộc: duyệt đầu phải rr tăng dần, chỉ giữ dấu 1 ở lần xuất hiện cuối của mỗi giá trị trong [1,r][1, r]; số giá trị khác nhau trên [l,r][l, r] là số dấu 1 trong [l,r][l, r].

Ví dụSố Màu Trong Mỗi Cây Con

Đề. Cây n≤2⋅105n \le 2 \cdot 10^5 đỉnh gốc 1, mỗi đỉnh một màu tới 10910^9. In số màu khác nhau trong cây con của từng đỉnh.

Lời giải. Trải cây theo tintin: câu hỏi của uu thành “đếm số màu khác nhau trên đoạn [tinu,toutu][tin_u, tout_u]”. Bài đếm giá trị khác nhau trên đoạn có cách giải ngoại tuyến bằng Fenwick: quét đầu phải rr tăng dần, và chỉ đặt dấu 1 ở lần xuất hiện cuối của mỗi màu trong [1,r][1, r]. Khi đó mỗi màu có mặt trong [l,r][l, r] được đếm đúng một lần, bởi dấu nằm trong đoạn.

Bước 1: nén màu và trải cây. Nén bằng sort và lower_bound trên bản sao v, để last dùng được mảng thường. col[tin[u]] là màu theo thứ tự DFS.

C++
sort(v + 1, v + n + 1);
for (ll i = 1; i <= n; i++) c[i] = lower_bound(v + 1, v + n + 1, c[i]) - v;

Bước 2: gom câu hỏi theo đầu phải. Câu hỏi của uu là đoạn [tinu,toutu][tin_u, tout_u], gom vào at[tout[u]].

C++
for (ll u = 1; u <= n; u++) at[tout[u]].push_back(u);

Bước 3: quét. Tới ô rr có màu cc: nếu màu cc từng xuất hiện cuối ở last[c] thì xóa dấu ở đó, đặt dấu ở rr, rồi trả lời mọi câu hỏi có đầu phải rr bằng tổng Fenwick trên [l,r][l, r].

C++
for (ll r = 1; r <= n; r++) {
    if (last[col[r]]) update(last[col[r]], -1);
    update(r, 1);
    last[col[r]] = r;
    for (ll u : at[r]) ans[u] = get(r) - get(tin[u] - 1);
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const ll N = 2e5 + 5;
ll n, timer, c[N], v[N], tin[N], tout[N], col[N], last[N], bit[N], ans[N];
vector<ll> g[N], at[N];

void update(ll pos, ll val) {
    for (; pos <= n; pos += pos & -pos) bit[pos] += val;
}

ll get(ll pos) {
    ll res = 0;
    for (; pos > 0; pos -= pos & -pos) res += bit[pos];
    return res;
}

void dfs(ll u, ll p) {
    tin[u] = ++timer;
    col[timer] = c[u];
    for (ll w : g[u])
        if (w != p) dfs(w, u);
    tout[u] = timer;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 1; i <= n; i++) {
        cin >> c[i];
        v[i] = c[i];
    }
    sort(v + 1, v + n + 1);
    for (ll i = 1; i <= n; i++) c[i] = lower_bound(v + 1, v + n + 1, c[i]) - v;
    for (ll i = 1; i < n; i++) {
        ll a, b;
        cin >> a >> b;
        g[a].push_back(b);
        g[b].push_back(a);
    }
    dfs(1, 0);
    for (ll u = 1; u <= n; u++) at[tout[u]].push_back(u);
    for (ll r = 1; r <= n; r++) {
        if (last[col[r]]) update(last[col[r]], -1);
        update(r, 1);
        last[col[r]] = r;
        for (ll u : at[r]) ans[u] = get(r) - get(tin[u] - 1);
    }
    for (ll u = 1; u <= n; u++) cout << ans[u] << ' ';
    cout << '\n';
}
8
1 2 1 3 2 3 3 4
1 2
1 3
2 4
2 5
3 6
6 7
6 8

Mỗi ô hai lần cập nhật, mỗi câu hỏi hai tổng tiền tố: O(nlog⁡n)O(n \log n).

Khi đi thi

  • Thấy “cây con của uu”: DFS lấy tintin, touttout rồi coi như đoạn [tinu,toutu][tin_u, tout_u]; mọi cấu trúc đoạn đều dùng được.
  • Nhớ đặt giá trị của đỉnh vào ô tinutin_u, không phải ô uu; hai chỉ số này dễ lẫn, nhất là khi in đáp án theo đỉnh.
  • Đường từ gốc là “cập nhật cây con, hỏi điểm”; đường giữa hai đỉnh bất kỳ cần thêm tổ tiên chung gần nhất (bài 36) hay phân rã nặng nhẹ (bài 58).

Tóm tắt

  • DFS gán tintin khi vào, touttout khi ra; cây con của uu là đoạn [tinu,toutu][tin_u, tout_u], kích thước toutu−tinu+1tout_u - tin_u + 1.
  • uu là tổ tiên của vv khi tinu≤tinv≤toututin_u \le tin_v \le tout_u.
  • Cập nhật điểm, hỏi cây con: Fenwick trên vị trí tintin; cập nhật cây con, hỏi điểm: Fenwick trên mảng hiệu, cho tổng đường từ gốc.
  • Cập nhật và hỏi cả cây con: cây phân đoạn lười.
  • Gom theo độ sâu cho câu hỏi theo tầng; câu hỏi ngoại tuyến trên cây con là câu hỏi đoạn trên dãy tĩnh.

Bài tập

1
Hành Trình Trở Lạicanuc80k3100DFS thăm con theo chỉ số tăng dần, in đỉnh khi vào và in lại đỉnh cha mỗi khi quay về từ một con.
LQDOJ
2
Khoảng Đại Diện Cây Concanuc80k3103Đánh số các lá theo thứ tự DFS; khoảng của vv là lá nhỏ nhất và lớn nhất trong cây con của nó.
LQDOJ
3
Các Đỉnh Gần Một Đường Gốccanuc80k3105Thay mỗi đỉnh bằng cha của nó (trừ gốc), lấy đỉnh sâu nhất uu; câu trả lời là có khi mọi đỉnh đã thay đều là tổ tiên của uu.
LQDOJ
4
Kích Thước Theo Gốc Được Chọncanuc80k3110DFS từ gốc rr tính kích thước cây con một lần, mỗi câu hỏi tra mảng.
LQDOJ
5
Vườn Quả Trên Câycanuc80k3111Fenwick trên vị trí tintin: đổi trạng thái một quả là cộng ±1\pm 1, đếm quả trong cây con là tổng đoạn.
LQDOJ
6
Phân Công Mới Nhấtcanuc80k3112Cây phân đoạn gán lười trên vị trí tintin: giao việc cho cả cây con là gán đoạn, câu hỏi đọc một điểm.
LQDOJ
7
Điểm Khen Lan Xuốngcanuc80k3113Khen một người và mọi cấp dưới là cộng trên [tin,tout][tin, tout]; hỏi một người là đọc một điểm bằng Fenwick trên mảng hiệu.
LQDOJ
8
Cấp Dưới Năng Lực Cao Hơncanuc80k3117Xử lý các con bò theo năng lực giảm dần: trước khi thêm bò uu, đếm số bò đã thêm nằm trong [tinu,toutu][tin_u, tout_u].
LQDOJ
9
Xếp Chữ Theo Tầngcanuc80k3120Mỗi tầng giữ các đỉnh theo tintin và XOR tiền tố của mặt nạ chữ cái; đoạn của cây con tìm bằng tìm kiếm nhị phân, xếp được thành đối xứng khi mặt nạ có không quá một bit 1.
LQDOJ
10
Cộng Trong Phạm Vi Độ Sâucanuc80k3126DFS giữ một Fenwick theo độ sâu: vào vv thì áp các thao tác (v,d,x)(v, d, x) lên độ sâu [depv,depv+d][dep_v, dep_v + d], đọc điểm tại depvdep_v, ra khỏi vv thì hoàn tác.
LQDOJ
11
Màu Xuất Hiện Đủ Nhiềucanuc80k3129Câu hỏi cây con là đoạn [tinv,toutv][tin_v, tout_v]; chạy Mo với bảng đếm và số màu có tần suất ít nhất kk.
LQDOJ
12
Cộng Trên Một Phía Của Cạnhcanuc80k3132Cạnh (a,b)(a, b) với aa là con của bb: cộng vào cây con của aa; ngược lại cộng toàn cây rồi trừ cây con của bb. Gom bằng mảng hiệu theo tintin rồi cộng dồn.
LQDOJ
13
Họ Hàng Cùng Bậccanuc80k3141Tìm tổ tiên thứ pp của vv bằng nhảy nhị phân (bài 35), rồi đếm đỉnh cùng tầng với vv trong cây con đó, trừ chính vv.
LQDOJ
14
Giá Trị Lớn Thứ K Trong Cây Concanuc80k3146K≤20K \le 20: mỗi đỉnh giữ 20 giá trị lớn nhất của cây con, gộp từ các con rồi cắt bớt.
LQDOJ

Đọc thêm