Chương 6·Bài 67

Centroid Decomposition

Cho một cây, đếm xem có bao nhiêu đường đi dài đúng kk cạnh. Duyệt mọi cặp đỉnh thì mất O(n2)O(n^2).

Phân rã trọng tâm chọn một đỉnh cân bằng cc và nhận xét: mọi đường đi hoặc đi qua cc, hoặc nằm gọn trong một thành phần sau khi bỏ cc. Đường qua cc đếm được nhờ khoảng cách từ cc tới mọi đỉnh của từng nhánh; phần còn lại làm tiếp trên mỗi thành phần. Trọng tâm làm kích thước giảm ít nhất một nửa nên mỗi đỉnh chỉ thuộc O(log⁡n)O(\log n) tầng. Các trọng tâm lồng nhau tạo thành cây trọng tâm, dùng cho truy vấn động như “đỉnh đánh dấu gần nhất”.

Trọng tâm

Trọng tâm là đỉnh mà khi bỏ đi, mọi thành phần còn lại có không quá ⌊n/2⌋\lfloor n/2 \rfloor đỉnh. Luôn tồn tại: bắt đầu từ một đỉnh bất kỳ, nếu có nhánh lớn hơn một nửa thì bước sang nhánh đó; nhánh phía sau lưng chỉ còn chưa tới một nửa nên không bao giờ quay lại, và quá trình dừng.

Cài đặt: BFS lấy thứ tự và cha, duyệt ngược để tính sz và mxc (cây con lớn nhất). Phần lớn nhất khi bỏ uu là max⁡(mxcu,n−szu)\max(mxc_u, n - sz_u); đỉnh đầu tiên có giá trị này không quá n/2n/2 là trọng tâm. Dùng BFS thay DFS để cây dạng đường dài không làm tràn ngăn xếp.

Trong hình, mỗi bước chọn trọng tâm của một thành phần (cam), bỏ nó đi rồi làm tiếp trên các phần còn lại. Nhãn là tầng.

Phân rã trọng tâm từng tầng.

Hỏi nhanh

Cây là một đường thẳng n=217n = 2^{17} đỉnh. Phân rã trọng tâm có bao nhiêu tầng?

Ví dụĐiểm Cân Bằng

Đề. Cây n≤2⋅105n \le 2 \cdot 10^5 đỉnh. In một trọng tâm.

Lời giải. Tính kích thước mọi cây con khi treo cây ở một đỉnh bất kỳ. Bỏ đỉnh uu đi thì các phần còn lại là các cây con của con uu, cùng phần “phía trên” có n−szun - sz_u đỉnh. uu là trọng tâm khi phần lớn nhất trong số đó không quá n/2n / 2. Hàm centroid dưới đây được dùng lại nguyên vẹn ở mọi ví dụ sau, nên viết sẵn cho một thành phần bất kỳ chưa bị xóa.

Bước 1: kích thước không đệ quy. BFS từ s trên các đỉnh chưa bị xóa (del), ghi cha fa và thứ tự comp. Duyệt comp từ cuối lên thì mỗi đỉnh được cộng vào cha sau khi kích thước của nó đã đủ; mxc[p] là cây con lớn nhất của các con của p.

C++
comp.assign(1, s);
fa[s] = 0;
for (int i = 0; i < (int)comp.size(); i++) {
    int u = comp[i];
    sz[u] = 1, mxc[u] = 0;
    for (int v : adj[u])
        if (!del[v] && v != fa[u]) fa[v] = u, comp.push_back(v);
}
for (int i = (int)comp.size() - 1; i > 0; i--) {
    int u = comp[i], p = fa[u];
    sz[p] += sz[u];
    mxc[p] = max(mxc[p], sz[u]);
}

Bước 2: kiểm điều kiện. max(mxc[u], tot - sz[u]) là phần lớn nhất còn lại khi bỏ uu. Viết 2 * ... <= tot để khỏi chia.

C++
int tot = comp.size();
for (int u : comp)
    if (2 * max(mxc[u], tot - sz[u]) <= tot) return u;

Trọng tâm luôn tồn tại: bắt đầu ở gốc và đi sang con có cây con lớn hơn tot/2tot / 2 chừng nào còn con như vậy; mỗi bước phần phía trên vẫn nhỏ hơn tot/2tot / 2, và quá trình dừng ở một đỉnh thỏa điều kiện. Dòng return s cuối chỉ để hàm có giá trị trả về.

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

const int N = 2e5 + 5;
int n, fa[N], sz[N], mxc[N];
bool del[N];
vector<int> adj[N], comp;

int centroid(int s) {
    comp.assign(1, s);
    fa[s] = 0;
    for (int i = 0; i < (int)comp.size(); i++) {
        int u = comp[i];
        sz[u] = 1, mxc[u] = 0;
        for (int v : adj[u])
            if (!del[v] && v != fa[u]) fa[v] = u, comp.push_back(v);
    }
    for (int i = (int)comp.size() - 1; i > 0; i--) {
        int u = comp[i], p = fa[u];
        sz[p] += sz[u];
        mxc[p] = max(mxc[p], sz[u]);
    }
    int tot = comp.size();
    for (int u : comp)
        if (2 * max(mxc[u], tot - sz[u]) <= tot) return u;
    return s;
}

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

    cin >> n;
    for (int i = 1; i < n; i++) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    cout << centroid(1) << '\n';
}
7
1 2
2 3
3 4
4 5
4 6
6 7

Mỗi lần gọi O(tot)O(tot).

Đường qua trọng tâm

Ở trọng tâm cc của thành phần hiện tại, mỗi đường đi qua cc ghép từ hai nửa thuộc hai nhánh khác nhau (hoặc một nửa rỗng, tức cc là một đầu). Duyệt các nhánh lần lượt: với nhánh hiện tại, tính độ sâu mọi đỉnh tới cc, ghép với mảng đếm cnt[d] của các nhánh trước (khởi đầu cnt[0] = 1 cho chính cc), rồi mới thêm nhánh này vào cnt. Ghép trước, thêm sau thì hai nửa không bao giờ cùng một nhánh, nên không đếm đường không đơn.

Xong cc thì đánh dấu cc đã xóa, xóa mảng đếm (chỉ tới độ sâu đã dùng), rồi xử lý từng thành phần con. Mỗi tầng tốn tổng O(n)O(n), có O(log⁡n)O(\log n) tầng. Dùng ngăn xếp các đỉnh đại diện thành phần thay cho đệ quy.

Hỏi nhanh

Ở trọng tâm cc, nhánh AA có độ sâu {1,2}\{1, 2\}, nhánh BB có {1}\{1\}, cần đường dài đúng 2. Xử lý AA rồi BB theo cách “ghép trước, thêm sau” đếm được bao nhiêu đường?

Ví dụĐường Đi Đúng K Cạnh

Đề. Cây n≤2⋅105n \le 2 \cdot 10^5 đỉnh, k≤nk \le n. Đếm số đường đi đơn có đúng kk cạnh.

Lời giải. Phân rã trọng tâm: tìm trọng tâm cc của thành phần, đếm mọi đường đi qua cc, xóa cc, rồi làm tiếp trên từng thành phần con. Đường qua cc có độ dài kk gồm một nửa độ sâu dd trong một nhánh và một nửa độ sâu k−dk - d ở nhánh khác, hoặc chính cc (độ sâu 0). centroid như ví dụ trước.

Bước 1: ngăn xếp thay đệ quy. st chứa các đỉnh đại diện cho thành phần cần xử lý. Lấy ra một đỉnh, tìm trọng tâm c của thành phần chứa nó và đánh dấu del[c]. cnt[0] = 1 là chính c: đường có một đầu là c ghép với nửa rỗng này.

C++
int c = centroid(st.back());
st.pop_back();
del[c] = 1;
int maxd = 0;
cnt[0] = 1;

Bước 2: từng nhánh. BFS gom các đỉnh của nhánh vào q kèm độ sâu; đỉnh sâu hơn kk không ghép được với ai, nên không mở rộng đỉnh có dep == k. Vòng thứ nhất ghép với các nhánh trước; vòng thứ hai mới thêm nhánh này vào cnt. Tách hai vòng như vậy thì hai đầu của một đường không bao giờ cùng một nhánh.

C++
for (int u : q) ans += cnt[k - dep[u]];
for (int u : q) cnt[dep[u]]++, maxd = max(maxd, dep[u]);

Bước 3: dọn và đẩy thành phần con. Xóa cnt chỉ tới maxd; xóa cả mảng mỗi lần sẽ tốn O(n)O(n) cho mỗi trọng tâm, tức O(n2)O(n^2). Mỗi hàng xóm chưa bị xóa của c đại diện cho một thành phần con.

C++
for (int d = 0; d <= maxd; d++) cnt[d] = 0;
for (int v : adj[c])
    if (!del[v]) st.push_back(v);

Vì sao O(nlog⁡n)O(n \log n): mỗi thành phần con có không quá một nửa số đỉnh của thành phần cha, nên mỗi đỉnh nằm trong tối đa log⁡2n\log_2 n thành phần, và ở mỗi thành phần công việc tuyến tính theo kích thước.

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

const int N = 2e5 + 5;
int n, k, fa[N], sz[N], mxc[N], dep[N], cnt[N];
bool del[N];
vector<int> adj[N], comp;

int centroid(int s) {
    comp.assign(1, s);
    fa[s] = 0;
    for (int i = 0; i < (int)comp.size(); i++) {
        int u = comp[i];
        sz[u] = 1, mxc[u] = 0;
        for (int v : adj[u])
            if (!del[v] && v != fa[u]) fa[v] = u, comp.push_back(v);
    }
    for (int i = (int)comp.size() - 1; i > 0; i--) {
        int u = comp[i], p = fa[u];
        sz[p] += sz[u];
        mxc[p] = max(mxc[p], sz[u]);
    }
    int tot = comp.size();
    for (int u : comp)
        if (2 * max(mxc[u], tot - sz[u]) <= tot) return u;
    return s;
}

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

    cin >> n >> k;
    for (int i = 1; i < n; i++) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    ll ans = 0;
    vector<int> st = {1};
    while (!st.empty()) {
        int c = centroid(st.back());
        st.pop_back();
        del[c] = 1;
        int maxd = 0;
        cnt[0] = 1;
        for (int v : adj[c]) {
            if (del[v]) continue;
            vector<int> q = {v};
            fa[v] = c, dep[v] = 1;
            for (int i = 0; i < (int)q.size(); i++) {
                int u = q[i];
                if (dep[u] == k) continue;
                for (int w : adj[u])
                    if (!del[w] && w != fa[u]) fa[w] = u, dep[w] = dep[u] + 1, q.push_back(w);
            }
            for (int u : q) ans += cnt[k - dep[u]];
            for (int u : q) cnt[dep[u]]++, maxd = max(maxd, dep[u]);
        }
        for (int d = 0; d <= maxd; d++) cnt[d] = 0;
        for (int v : adj[c])
            if (!del[v]) st.push_back(v);
    }
    cout << ans << '\n';
}
6 3
1 2
2 3
2 4
4 5
4 6

O(nlog⁡n)O(n \log n).

Độ dài trong khoảng

Cần đếm các cặp có tổng độ sâu trong [k1,k2][k_1, k_2]: với nửa có độ sâu dd, số nửa còn lại phù hợp là tổng cnt trên đoạn [k1−d,k2−d][k_1 - d, k_2 - d]. Thay mảng đếm bằng Fenwick theo độ sâu, mỗi lần ghép một truy vấn đoạn O(log⁡n)O(\log n), tổng O(nlog⁡2n)O(n \log^2 n). Xóa Fenwick bằng cách trừ lại đúng các độ sâu đã thêm.

Cùng khung này đổi “độ sâu” thành tổng trọng số, tổng ±1\pm 1, mặt nạ bit, giá trị theo mô-đun; cấu trúc ghép đổi thành mảng, Fenwick, map hay hai con trỏ trên danh sách đã sắp.

Ví dụĐường Đi Trong Khoảng Độ Dài

Đề. Cây n≤2⋅105n \le 2 \cdot 10^5 đỉnh. Đếm số đường đi có số cạnh trong [k1,k2][k_1, k_2].

Lời giải. Khung y như ví dụ trước. Khác ở chỗ nửa có độ sâu dd ghép được với mọi nửa có độ sâu trong [k1−d,k2−d][k_1 - d, k_2 - d], nên mảng đếm thay bằng Fenwick theo độ sâu để hỏi tổng một đoạn. Đỉnh sâu hơn k2k_2 không cần duyệt.

Bước 1: Fenwick trên độ sâu. Độ sâu bắt đầu từ 0 nên chỉ số dời lên 1. get(i) là số nửa có độ sâu không quá ii; i < 0 trả 0, i > n bị kẹp về nn, nên gọi với cận bất kỳ cũng an toàn.

C++
int get(int i) {
    if (i < 0) return 0;
    int s = 0;
    for (i = min(i, n) + 1; i; i -= i & -i) s += bit[i];
    return s;
}

Bước 2: đếm và thêm. Số nửa ở các nhánh trước có độ sâu trong [k1−d,k2−d][k_1 - d, k_2 - d]; used ghi lại mọi độ sâu đã thêm (kể cả độ sâu 0 của chính c).

C++
for (int u : q) ans += get(k2 - dep[u]) - get(k1 - dep[u] - 1);
for (int u : q) add(dep[u], 1), used.push_back(dep[u]);

Bước 3: hoàn tác. Cuối mỗi trọng tâm trừ lại đúng những ô đã thêm; Fenwick trở về toàn 0 mà không tốn O(n)O(n).

C++
for (int d : used) add(d, -1);
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N = 2e5 + 5;
int n, k1, k2, fa[N], sz[N], mxc[N], dep[N], bit[N];
bool del[N];
vector<int> adj[N], comp;

void add(int i, int v) {
    for (i++; i <= n + 1; i += i & -i) bit[i] += v;
}

int get(int i) {
    if (i < 0) return 0;
    int s = 0;
    for (i = min(i, n) + 1; i; i -= i & -i) s += bit[i];
    return s;
}

int centroid(int s) {
    comp.assign(1, s);
    fa[s] = 0;
    for (int i = 0; i < (int)comp.size(); i++) {
        int u = comp[i];
        sz[u] = 1, mxc[u] = 0;
        for (int v : adj[u])
            if (!del[v] && v != fa[u]) fa[v] = u, comp.push_back(v);
    }
    for (int i = (int)comp.size() - 1; i > 0; i--) {
        int u = comp[i], p = fa[u];
        sz[p] += sz[u];
        mxc[p] = max(mxc[p], sz[u]);
    }
    int tot = comp.size();
    for (int u : comp)
        if (2 * max(mxc[u], tot - sz[u]) <= tot) return u;
    return s;
}

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

    cin >> n >> k1 >> k2;
    for (int i = 1; i < n; i++) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    ll ans = 0;
    vector<int> st = {1};
    while (!st.empty()) {
        int c = centroid(st.back());
        st.pop_back();
        del[c] = 1;
        vector<int> used = {0};
        add(0, 1);
        for (int v : adj[c]) {
            if (del[v]) continue;
            vector<int> q = {v};
            fa[v] = c, dep[v] = 1;
            for (int i = 0; i < (int)q.size(); i++) {
                int u = q[i];
                if (dep[u] == k2) continue;
                for (int w : adj[u])
                    if (!del[w] && w != fa[u]) fa[w] = u, dep[w] = dep[u] + 1, q.push_back(w);
            }
            for (int u : q) ans += get(k2 - dep[u]) - get(k1 - dep[u] - 1);
            for (int u : q) add(dep[u], 1), used.push_back(dep[u]);
        }
        for (int d : used) add(d, -1);
        for (int v : adj[c])
            if (!del[v]) st.push_back(v);
    }
    cout << ans << '\n';
}
7 2 3
1 2
2 3
2 4
4 5
4 6
6 7

Mỗi đỉnh của mỗi thành phần tốn thêm một truy vấn và một cập nhật Fenwick: O(nlog⁡2n)O(n \log^2 n).

Cây trọng tâm

Ghi lại trọng tâm cha cpar[c] (trọng tâm của thành phần vừa chứa cc) được cây trọng tâm độ sâu O(log⁡n)O(\log n). Với mọi u,vu, v, đường u→vu \to v đi qua trọng tâm tổ tiên chung thấp nhất cc của chúng trong cây trọng tâm, nên

dist⁡(u,v)=min⁡c laˋ tổ tieˆn chung(dist⁡(u,c)+dist⁡(c,v)).\operatorname{dist}(u, v) = \min_{c \text{ là tổ tiên chung}} \big(\operatorname{dist}(u, c) + \operatorname{dist}(c, v)\big).

Khi dựng, BFS từ mỗi trọng tâm cc trong thành phần của nó và lưu dist[lev[c]][u]; mỗi đỉnh có tối đa log⁡n\log n giá trị như vậy. Đánh dấu vv: với mọi tổ tiên cc của vv (kể cả vv), cập nhật best[c] là khoảng cách nhỏ nhất từ cc tới một đỉnh đánh dấu. Hỏi vv: lấy nhỏ nhất của best[c] + dist(v, c) trên các tổ tiên. Mỗi thao tác O(log⁡n)O(\log n).

Ví dụTín Hiệu Đỏ Gần Nhất

Đề. Cây n≤105n \le 10^5 đỉnh, ban đầu chỉ đỉnh 1 đỏ. m≤105m \le 10^5 thao tác: tô đỏ một đỉnh; hỏi khoảng cách từ một đỉnh tới đỉnh đỏ gần nhất.

Lời giải. Dựng cây trọng tâm một lần. Mỗi trọng tâm cc ở tầng LL BFS trong thành phần của nó và ghi dist[L][u], khoảng cách từ uu tới cc. Mỗi đỉnh có tối đa log⁡2n+1\log_2 n + 1 trọng tâm tổ tiên, nên bảng có O(nlog⁡n)O(n \log n) ô. best[c] là khoảng cách nhỏ nhất từ cc tới một đỉnh đỏ nằm trong thành phần của cc. Tô đỏ và hỏi đều chỉ đi trên các trọng tâm tổ tiên của đỉnh.

Bước 1: dựng cây trọng tâm. Ngăn xếp giữ bộ ba (đỉnh đại diện, trọng tâm cha, tầng). Lấy ra thì tìm trọng tâm c, ghi cpar[c], lev[c], đánh dấu del[c], rồi BFS từ c trên phần chưa bị xóa để điền dist[L]. Các hàng xóm còn sống đi vào ngăn xếp với tầng L+1L + 1. Một đỉnh uu được BFS ở mọi tầng mà nó còn nằm trong thành phần, nên dist[lev[c]][u] có nghĩa cho đúng các trọng tâm tổ tiên của uu.

C++
int c = centroid(cur[0]), L = cur[2];
cpar[c] = cur[1], lev[c] = L, del[c] = 1;
vector<int> q = {c};
fa[c] = 0, dist[L][c] = 0;
for (int i = 0; i < (int)q.size(); i++) {
    int u = q[i];
    for (int w : adj[u])
        if (!del[w] && w != fa[u]) fa[w] = u, dist[L][w] = dist[L][u] + 1, q.push_back(w);
}
for (int v : adj[c])
    if (!del[v]) st.push_back({v, c, L + 1});

Bước 2: tô đỏ và hỏi. Đi từ vv lên theo cpar, kể cả chính vv (vv cũng là trọng tâm của một thành phần nào đó).

C++
void paint(int v) {
    for (int c = v; c; c = cpar[c]) best[c] = min(best[c], dist[lev[c]][v]);
}

int ask(int v) {
    int res = INF;
    for (int c = v; c; c = cpar[c]) res = min(res, best[c] + dist[lev[c]][v]);
    return res;
}

Vì sao ask đúng: đường từ vv tới đỉnh đỏ gần nhất ww đi qua trọng tâm tổ tiên chung thấp nhất cc của vv và ww; cc nằm trên đường lên của vv, và best[c] đã tính tới ww. Các trọng tâm khác chỉ cho giá trị không nhỏ hơn khoảng cách thật. INF = 1e9 cộng khoảng cách tới 10510^5 vẫn vừa int; đỉnh 1 đỏ sẵn nên gọi paint(1) trước.

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

const int N = 1e5 + 5, LG = 18, INF = 1e9;
int n, m, fa[N], sz[N], mxc[N], cpar[N], lev[N], best[N], dist[LG][N];
bool del[N];
vector<int> adj[N], comp;

int centroid(int s) {
    comp.assign(1, s);
    fa[s] = 0;
    for (int i = 0; i < (int)comp.size(); i++) {
        int u = comp[i];
        sz[u] = 1, mxc[u] = 0;
        for (int v : adj[u])
            if (!del[v] && v != fa[u]) fa[v] = u, comp.push_back(v);
    }
    for (int i = (int)comp.size() - 1; i > 0; i--) {
        int u = comp[i], p = fa[u];
        sz[p] += sz[u];
        mxc[p] = max(mxc[p], sz[u]);
    }
    int tot = comp.size();
    for (int u : comp)
        if (2 * max(mxc[u], tot - sz[u]) <= tot) return u;
    return s;
}

void paint(int v) {
    for (int c = v; c; c = cpar[c]) best[c] = min(best[c], dist[lev[c]][v]);
}

int ask(int v) {
    int res = INF;
    for (int c = v; c; c = cpar[c]) res = min(res, best[c] + dist[lev[c]][v]);
    return res;
}

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

    cin >> n >> m;
    for (int i = 1; i < n; i++) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    vector<array<int, 3>> st = {{1, 0, 0}};
    while (!st.empty()) {
        array<int, 3> cur = st.back();
        st.pop_back();
        int c = centroid(cur[0]), L = cur[2];
        cpar[c] = cur[1], lev[c] = L, del[c] = 1;
        vector<int> q = {c};
        fa[c] = 0, dist[L][c] = 0;
        for (int i = 0; i < (int)q.size(); i++) {
            int u = q[i];
            for (int w : adj[u])
                if (!del[w] && w != fa[u]) fa[w] = u, dist[L][w] = dist[L][u] + 1, q.push_back(w);
        }
        for (int v : adj[c])
            if (!del[v]) st.push_back({v, c, L + 1});
    }
    for (int v = 1; v <= n; v++) best[v] = INF;
    paint(1);
    while (m--) {
        int t, v;
        cin >> t >> v;
        if (t == 1) paint(v);
        else cout << ask(v) << '\n';
    }
}
6 7
1 2
2 3
2 4
4 5
4 6
2 5
1 4
2 5
2 3
1 3
2 6
2 3

Dựng O(nlog⁡n)O(n \log n), mỗi thao tác O(log⁡n)O(\log n). Mỗi thao tác đọc dist của đúng một đỉnh qua khoảng 18 tầng, nên cách xếp bảng ảnh hưởng tới tốc độ:

Hai triệu lần đọc khoảng cách tới 18 trọng tâm tổ tiên, 100 nghìn đỉnh, trung vị 5 lần chạy, i7-10850H, g++ -O2.dist[tầng][đỉnh]83 ms · gấp 1,7 lầndist[đỉnh][tầng]50 ms
Hai triệu lần đọc khoảng cách tới 18 trọng tâm tổ tiên của 100 nghìn đỉnh, hai cách xếp bảng, đo trên i7-10850H, g++ -O2.

Xếp theo đỉnh (dist[u][L], 18 số của một đỉnh nằm liền nhau) nhanh hơn xếp theo tầng (dist[L][u]) khoảng 1,7 lần, vì một lần đọc bộ nhớ đệm đủ cho cả đường lên. Quy tắc: dữ liệu một truy vấn đọc cùng lúc thì để sát nhau.

Khi đi thi

  • Đếm hay tối ưu trên mọi đường đi của cây, điều kiện cộng được theo hai nửa: phân rã trọng tâm, ghép với các nhánh trước rồi mới thêm nhánh này, chỉ xóa phần cấu trúc đã dùng.
  • Cài không đệ quy: BFS tính kích thước, ngăn xếp các thành phần cần xử lý; mảng đếm cỡ thành phần chứ không phải nn mỗi lần.
  • Truy vấn động về khoảng cách tới tập đỉnh: cây trọng tâm, mỗi đỉnh lưu khoảng cách tới O(log⁡n)O(\log n) trọng tâm tổ tiên.

Tóm tắt

  • Trọng tâm: bỏ đi thì mọi phần không quá n/2n/2; độ sâu của phân rã là O(log⁡n)O(\log n).
  • Mọi đường đi qua đúng một trọng tâm đầu tiên tách hai đầu của nó.
  • Ghép nửa đường theo nhánh, trước khi thêm nhánh, để không đếm cặp cùng nhánh.
  • Cây trọng tâm: khoảng cách giữa hai đỉnh là cực tiểu qua các trọng tâm tổ tiên chung.

Bài tập

Đọc thêm