Chương 7·Bài 76

Advanced Trees

Có những bài mà cách giải nhanh nhất là đừng giải trên dữ liệu gốc, mà dựng ra một cái cây khác từ nó. Ví dụ: hỏi “giá trị nhỏ nhất của đoạn [l,r][l, r]” trên mảng, nếu có cây phù hợp, trở thành hỏi tổ tiên chung của hai đỉnh.

Bốn cây phụ hay gặp: cây Cartesian biến cực tiểu đoạn thành tổ tiên chung; cây Kruskal dựng lại biến “thời điểm hai đỉnh nối nhau” thành giá trị ở LCA; cây ảo nén cây lớn xuống chỉ các đỉnh được hỏi và LCA của chúng; cây cầu co mỗi thành phần không có cầu thành một đỉnh. Dựng xong, bài còn lại là DP hay LCA trên cây.

Cây Cartesian

Với dãy giá trị đôi một khác nhau, cây Cartesian (theo cực tiểu) có gốc là vị trí nhỏ nhất, cây con trái dựng từ đoạn bên trái, cây con phải từ đoạn bên phải. Duyệt trung thứ tự ra đúng thứ tự vị trí, và giá trị cha nhỏ hơn giá trị con. Cực tiểu của đoạn [l,r][l, r] là LCA của ll và rr.

Dựng trong O(n)O(n) bằng ngăn xếp đơn điệu chứa đường biên phải của cây: thêm ii, bỏ khỏi ngăn xếp mọi phần tử lớn hơn aia_i; phần tử cuối cùng bị bỏ trở thành con trái của ii, và ii thành con phải của đỉnh ngăn xếp còn lại. Các phần tử bị bỏ khác giữ nguyên cha cũ.

Mỗi đỉnh nằm đúng cột vị trí của nó. Xanh dương là ngăn xếp, cũng là biên phải của cây; xanh lá là các phần tử vừa bị bỏ.

Ví dụCây Phân Hoạch Theo Giá Trị Nhỏ Nhất

Đề. Dãy n≤106n \le 10^6 giá trị đôi một khác nhau. In cha của mỗi vị trí trong cây Cartesian nhỏ nhất (gốc có cha là chính nó).

Lời giải. Dựng bằng ngăn xếp biên phải như ở trên. st[0..top-1] là biên phải của cây đã dựng cho các phần tử 0..i−10..i-1, giá trị tăng dần từ đáy lên đỉnh. Thêm ii: bỏ khỏi ngăn xếp mọi phần tử lớn hơn aia_i; phần tử bị bỏ cuối cùng thành con trái của ii, còn ii thành con phải của phần tử đang ở đỉnh ngăn xếp.

last là phần tử cuối bị bỏ: nó và cả cây con của nó lớn hơn aia_i và nằm bên trái ii, nên trở thành cây con trái của ii; các phần tử bị bỏ trước đó vẫn là con của nhau như cũ. Ngăn xếp rỗng thì ii tạm là gốc; nếu sau này có phần tử nhỏ hơn đến, par[i] được sửa lại khi ii là last của lần bỏ đó.

C++
for (int i = 0; i < n; i++) {
    int last = -1;
    while (top && a[st[top - 1]] > a[i]) last = st[--top];
    if (last != -1) par[last] = i;
    par[i] = top ? st[top - 1] : i;
    st[top++] = i;
}
par[st[0]] = st[0];

Đáy ngăn xếp cuối cùng là phần tử nhỏ nhất, tức gốc, có cha là chính nó theo yêu cầu đề.

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

const int N = 1e6 + 5;
int n, a[N], par[N], st[N], top;

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

    cin >> n;
    for (int i = 0; i < n; i++) cin >> a[i];
    for (int i = 0; i < n; i++) {
        int last = -1;
        while (top && a[st[top - 1]] > a[i]) last = st[--top];
        if (last != -1) par[last] = i;
        par[i] = top ? st[top - 1] : i;
        st[top++] = i;
    }
    par[st[0]] = st[0];
    for (int i = 0; i < n; i++) cout << par[i] << ' ';
    cout << '\n';
}
5
8 3 6 1 5

Độ phức tạp: mỗi chỉ số vào và ra ngăn xếp một lần: O(n)O(n).

Ví dụTổng Các Giá Trị Nhỏ Nhất

Đề. Hoán vị độ dài n≤2⋅105n \le 2 \cdot 10^5. Tính tổng cực tiểu của mọi đoạn con.

Lời giải. Đếm theo đóng góp: aia_i là cực tiểu của đúng các đoạn chứa ii và nằm trong cây con của ii trên cây Cartesian. Cây con đó là khoảng (Li,Ri)(L_i, R_i), với LiL_i, RiR_i là vị trí gần nhất bên trái, bên phải có giá trị nhỏ hơn. Đầu trái chọn trong (Li,i](L_i, i], đầu phải trong [i,Ri)[i, R_i), nên đóng góp là ai(i−Li)(Ri−i)a_i (i - L_i)(R_i - i). Không cần dựng cây: LL, RR tìm bằng ngăn xếp đơn điệu.

Bước 1: nhỏ hơn gần nhất bên trái. Sau khi bỏ mọi phần tử lớn hơn aia_i, đỉnh ngăn xếp là phần tử nhỏ hơn gần nhất bên trái; rỗng thì Li=0L_i = 0. Lượt phải sang trái làm đối xứng cho RiR_i, rỗng thì Ri=n+1R_i = n + 1.

C++
for (int i = 1; i <= n; i++) {
    while (!st.empty() && a[st.back()] > a[i]) st.pop_back();
    L[i] = st.empty() ? 0 : st.back();
    st.push_back(i);
}

Bước 2: cộng đóng góp. Đóng góp tới 2⋅105⋅105⋅1052 \cdot 10^5 \cdot 10^5 \cdot 10^5, nên ép sang long long trước khi nhân.

C++
for (int i = 1; i <= n; i++) ans += (ll)a[i] * (i - L[i]) * (R[i] - i);

Hoán vị nên không có hai giá trị bằng nhau; với dãy có giá trị trùng thì một bên phải dùng >, bên kia >= để mỗi đoạn chỉ được tính cho đúng một vị trí.

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

const int N = 2e5 + 5;
int n, a[N], L[N], R[N];

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

    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    vector<int> st;
    for (int i = 1; i <= n; i++) {
        while (!st.empty() && a[st.back()] > a[i]) st.pop_back();
        L[i] = st.empty() ? 0 : st.back();
        st.push_back(i);
    }
    st.clear();
    for (int i = n; i >= 1; i--) {
        while (!st.empty() && a[st.back()] > a[i]) st.pop_back();
        R[i] = st.empty() ? n + 1 : st.back();
        st.push_back(i);
    }
    ll ans = 0;
    for (int i = 1; i <= n; i++) ans += (ll)a[i] * (i - L[i]) * (R[i] - i);
    cout << ans << '\n';
}
5
3 1 5 2 4

Độ phức tạp: O(n)O(n).

Cây Kruskal

Chạy Kruskal (hay thêm cạnh theo thời gian) với DSU, nhưng mỗi lần hợp hai thành phần gốc rar_a, rbr_b thì tạo đỉnh mới tt mang giá trị là trọng số (thời điểm) của cạnh, có hai con rar_a, rbr_b. Được một cây với nn lá là các đỉnh gốc và n−1n - 1 đỉnh trong. Giá trị tăng dần khi đi lên, nên:

  • Thời điểm sớm nhất uu, vv liên thông (hay ngưỡng nhỏ nhất để nối) là giá trị tại lca⁡(u,v)\operatorname{lca}(u, v).
  • Tập đỉnh nối được với uu khi chỉ dùng cạnh không quá WW là các lá trong cây con của tổ tiên cao nhất của uu có giá trị không quá WW (tìm bằng nhảy nhị phân).
Ô vuông là đỉnh mới mang trọng số cạnh vừa hợp; mọi lá dưới một ô nối được với nhau bằng các cạnh không quá giá trị ô đó.

Đỉnh mới có số hiệu lớn hơn hai con, nên duyệt số hiệu giảm dần là đi từ trên xuống, khỏi DFS.

Hỏi nhanh

Cạnh theo thứ tự ngày: ngày 1 nối 11 đến 22, ngày 2 nối 33 đến 44, ngày 3 nối 22 đến 33. LCA của đỉnh 1 và 4 trên cây Kruskal mang giá trị nào?

Ví dụNgày Kết Nối Đầu Tiên

Đề. n,m,q≤2⋅105n, m, q \le 2 \cdot 10^5. Ngày ii xây đường thứ ii. Mỗi truy vấn (u,v)(u, v) hỏi ngày sớm nhất hai thành phố liên thông, 0 nếu u=vu = v, −1-1 nếu không bao giờ.

Lời giải. Dựng cây Kruskal theo thứ tự ngày: mỗi lần đường mới nối hai thành phần, tạo đỉnh mới mang số ngày, hai con là gốc của hai thành phần. Đỉnh càng cao mang ngày càng lớn, và uu, vv liên thông lần đầu vào ngày ghi ở LCA của chúng. Hai đỉnh ở hai cây khác nhau thì không bao giờ liên thông.

Bước 1: dựng cây Kruskal. dsu[x] trỏ tới đỉnh cây Kruskal đang là gốc thành phần chứa xx. Hợp aa, bb: tạo tot, gán dsu[a] = dsu[b] = tot và đặt tot làm cha của cả hai; val[tot] là ngày.

C++
a = find(a), b = find(b);
if (a == b) continue;
++tot;
val[tot] = i;
dsu[a] = dsu[b] = tot;
up[0][a] = up[0][b] = tot;

Bước 2: độ sâu. Đỉnh mới luôn có số hiệu lớn hơn hai con, nên duyệt x từ tot xuống 1 là đi từ trên xuống: cha đã có dep trước con. Đỉnh không có cha là gốc của một cây, đặt up[0][x] = x. lca bằng nhảy nhị phân như bài 36.

C++
for (int x = tot; x >= 1; x--) {
    if (!up[0][x]) up[0][x] = x, dep[x] = 0;
    else dep[x] = dep[up[0][x]] + 1;
}

Bước 3: trả lời. u=vu = v in 0; khác thành phần in −1-1; còn lại in val của LCA.

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

const int N = 4e5 + 5, LG = 19;
int n, m, q, tot, dsu[N], val[N], up[LG][N], dep[N];

int find(int u) {
    while (dsu[u] != u) u = dsu[u] = dsu[dsu[u]];
    return u;
}

int lca(int u, int v) {
    if (dep[u] < dep[v]) swap(u, v);
    for (int j = LG - 1; j >= 0; j--)
        if (dep[u] - (1 << j) >= dep[v]) u = up[j][u];
    if (u == v) return u;
    for (int j = LG - 1; j >= 0; j--)
        if (up[j][u] != up[j][v]) u = up[j][u], v = up[j][v];
    return up[0][u];
}

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

    cin >> n >> m >> q;
    for (int i = 1; i < 2 * n; i++) dsu[i] = i;
    tot = n;
    for (int i = 1; i <= m; i++) {
        int a, b;
        cin >> a >> b;
        a = find(a), b = find(b);
        if (a == b) continue;
        ++tot;
        val[tot] = i;
        dsu[a] = dsu[b] = tot;
        up[0][a] = up[0][b] = tot;
    }
    for (int x = tot; x >= 1; x--) {
        if (!up[0][x]) up[0][x] = x, dep[x] = 0;
        else dep[x] = dep[up[0][x]] + 1;
    }
    for (int j = 1; j < LG; j++)
        for (int x = 1; x <= tot; x++) up[j][x] = up[j - 1][up[j - 1][x]];
    while (q--) {
        int u, v;
        cin >> u >> v;
        if (u == v) cout << 0 << '\n';
        else if (find(u) != find(v)) cout << -1 << '\n';
        else cout << val[lca(u, v)] << '\n';
    }
}
5 3 4
1 2
3 4
2 3
1 1
1 2
1 4
1 5

Dựng O(m α(n)+nlog⁡n)O(m\,\alpha(n) + n \log n), mỗi truy vấn O(log⁡n)O(\log n). LCA bằng nhảy nhị phân đọc bảng up ở nhiều tầng liên tiếp, nên cách xếp bảng ảnh hưởng tới tốc độ:

Hai triệu truy vấn LCA bằng nhảy nhị phân, cây 200 nghìn đỉnh, trung vị 5 lần chạy, i7-10850H, g++ -O2.up[bước][đỉnh]896 ms · gấp 1,5 lầnup[đỉnh][bước]594 ms
Hai triệu truy vấn LCA bằng nhảy nhị phân trên cây 200 nghìn đỉnh, hai cách xếp bảng, đo trên i7-10850H, g++ -O2.

Ngược với bài 35 (ở đó việc dựng bảng chiếm phần lớn thời gian): ở đây truy vấn đọc nhiều bước của cùng một đỉnh, nên xếp theo đỉnh (up[v][j]) nhanh hơn xếp theo bước (up[j][v]) khoảng 1,5 lần. Cách xếp nào nhanh tùy việc nào chiếm nhiều thời gian hơn, nên đo khi cần.

Cây ảo

Nhiều truy vấn, mỗi truy vấn chỉ quan tâm kk đỉnh, tổng kk nhỏ: DP trên cả cây mỗi truy vấn là O(nq)O(nq). Cây ảo gồm các đỉnh được hỏi cùng LCA của mọi cặp, và nối mỗi đỉnh với tổ tiên gần nhất trong tập đó. Nó có không quá 2k−12k - 1 đỉnh và giữ nguyên quan hệ tổ tiên, nên DP trên nó thay cho DP trên cây gốc.

Dựng: sắp các đỉnh theo thứ tự DFS, thêm gốc; duyệt với ngăn xếp chứa một đường từ gốc xuống. Với đỉnh mới vv, lấy l=lca⁡(v,đỉnh nga˘n xeˆˊp)l = \operatorname{lca}(v, \text{đỉnh ngăn xếp}); bỏ khỏi ngăn xếp (và nối cạnh ảo) những đỉnh sâu hơn ll, chèn ll nếu chưa có, rồi đẩy vv. Tổng O(klog⁡n)O(k \log n) cho việc sắp và LCA. Thông tin trên cạnh ảo (như trọng số nhỏ nhất trên đường) phải tính từ cây gốc, thường là tiền xử lý dọc đường từ gốc.

Bấm đỉnh để chọn hoặc bỏ. Cam: đỉnh được hỏi; xanh dương: LCA và gốc cần thêm; cạnh cam đậm là các đoạn đường mà cạnh ảo thay thế.

Ví dụCắt Đường Bảo Vệ Gốc

Đề. Cây n≤2.5⋅105n \le 2.5 \cdot 10^5 đỉnh có trọng số cạnh, gốc 1. Tới 5⋅1055 \cdot 10^5 truy vấn, tổng số đỉnh đặc biệt tới 5⋅1055 \cdot 10^5. Mỗi truy vấn: phá cạnh với tổng chi phí nhỏ nhất để gốc không tới được đỉnh đặc biệt nào.

Lời giải. mnvmn_v là cạnh rẻ nhất trên đường từ gốc tới vv, tính một lần cho cả cây. Gọi f(v)f(v) là chi phí nhỏ nhất để cắt mọi đỉnh đặc biệt trong cây con của vv khỏi gốc. Nếu vv đặc biệt thì phải cắt trên đường từ gốc tới vv, rẻ nhất là mnvmn_v. Ngược lại có hai lựa chọn: cắt một lần ở trên vv (mnvmn_v), hoặc lo riêng từng con, ∑cf(c)\sum_c f(c). Quy hoạch động này chỉ cần các đỉnh đặc biệt và LCA của chúng, nên mỗi truy vấn dựng cây ảo O(k)O(k) đỉnh và chạy trên đó.

Bước 1: duyệt cây một lần. Lấy uu ra khỏi ngăn xếp thì gán dfn[u] rồi đẩy các con; thứ tự lấy ra là một thứ tự DFS hợp lệ. mn[v] lấy min dọc đường đi xuống; gốc có mn = INF vì không cắt được phía trên gốc.

C++
up[0][v] = u, dep[v] = dep[u] + 1, mn[v] = min(mn[u], (ll)e.second);

Bước 2: dựng cây ảo. Sắp đỉnh đặc biệt theo dfn, ngăn xếp s giữ một đường từ gốc xuống. Với vv mới, l = lca(v, s.back()). Mọi đỉnh trên ngăn xếp sâu hơn ll đã xong cây con, được nối với đỉnh dưới nó rồi bỏ đi. Nếu đỉnh còn lại trên cùng không phải ll thì ll là đỉnh rẽ nhánh mới: nối ll với đỉnh đó và thay nó bằng ll. Rồi đẩy vv.

C++
for (int v : h) {
    int l = lca(v, s.back());
    while (s.size() >= 2 && dep[s[s.size() - 2]] >= dep[l]) {
        vt[s[s.size() - 2]].push_back(s.back());
        s.pop_back();
    }
    if (s.back() != l) {
        vt[l].push_back(s.back());
        s.back() = l;
        nodes.push_back(l);
    }
    s.push_back(v);
    nodes.push_back(v);
}

Bước 3: quy hoạch động trên cây ảo. nodes sắp theo dfn và bỏ trùng; duyệt ngược thì con xong trước cha. Đỉnh đặc biệt lấy mn[v]; gốc chỉ có thể lo từng con; đỉnh khác lấy min hai lựa chọn.

C++
for (int i = (int)nodes.size() - 1; i >= 0; i--) {
    int v = nodes[i];
    ll sum = 0;
    for (int c : vt[v]) sum += f[c];
    if (key[v]) f[v] = mn[v];
    else if (v == 1) f[v] = sum;
    else f[v] = min(mn[v], sum);
}

Cuối mỗi truy vấn xóa vt và key của đúng các đỉnh đã dùng, không xóa cả mảng.

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

const int N = 2.5e5 + 5, LG = 18;
const ll INF = 1e18;
int n, tim, dfn[N], dep[N], up[LG][N];
ll mn[N], f[N];
bool key[N];
vector<pair<int, int>> adj[N];
vector<int> vt[N];

int lca(int u, int v) {
    if (dep[u] < dep[v]) swap(u, v);
    for (int j = LG - 1; j >= 0; j--)
        if (dep[u] - (1 << j) >= dep[v]) u = up[j][u];
    if (u == v) return u;
    for (int j = LG - 1; j >= 0; j--)
        if (up[j][u] != up[j][v]) u = up[j][u], v = up[j][v];
    return up[0][u];
}

bool byDfn(int a, int b) {
    return dfn[a] < dfn[b];
}

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

    cin >> n;
    for (int i = 1; i < n; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        adj[u].push_back({v, w});
        adj[v].push_back({u, w});
    }
    vector<int> st = {1};
    up[0][1] = 1, mn[1] = INF;
    while (!st.empty()) {
        int u = st.back();
        st.pop_back();
        dfn[u] = ++tim;
        for (pair<int, int> e : adj[u])
            if (e.first != up[0][u]) {
                int v = e.first;
                up[0][v] = u, dep[v] = dep[u] + 1, mn[v] = min(mn[u], (ll)e.second);
                st.push_back(v);
            }
    }
    for (int j = 1; j < LG; j++)
        for (int v = 1; v <= n; v++) up[j][v] = up[j - 1][up[j - 1][v]];
    int q;
    cin >> q;
    while (q--) {
        int k;
        cin >> k;
        vector<int> h(k);
        for (int i = 0; i < k; i++) cin >> h[i], key[h[i]] = 1;
        sort(h.begin(), h.end(), byDfn);
        vector<int> nodes = {1}, s = {1};
        for (int v : h) {
            int l = lca(v, s.back());
            while (s.size() >= 2 && dep[s[s.size() - 2]] >= dep[l]) {
                vt[s[s.size() - 2]].push_back(s.back());
                s.pop_back();
            }
            if (s.back() != l) {
                vt[l].push_back(s.back());
                s.back() = l;
                nodes.push_back(l);
            }
            s.push_back(v);
            nodes.push_back(v);
        }
        while (s.size() >= 2) {
            vt[s[s.size() - 2]].push_back(s.back());
            s.pop_back();
        }
        sort(nodes.begin(), nodes.end(), byDfn);
        nodes.erase(unique(nodes.begin(), nodes.end()), nodes.end());
        for (int i = (int)nodes.size() - 1; i >= 0; i--) {
            int v = nodes[i];
            ll sum = 0;
            for (int c : vt[v]) sum += f[c];
            if (key[v]) f[v] = mn[v];
            else if (v == 1) f[v] = sum;
            else f[v] = min(mn[v], sum);
        }
        cout << f[1] << '\n';
        for (int v : nodes) vt[v].clear(), key[v] = 0;
    }
}
6
1 2 5
2 3 4
2 4 7
1 5 3
5 6 8
3
2 3 4
2 3 6
3 2 3 4

Độ phức tạp: tổng O(∑klog⁡n)O(\sum k \log n).

Cây cầu

Cầu là cạnh mà bỏ đi thì đồ thị mất liên thông. Bỏ mọi cầu, mỗi thành phần còn lại là một thành phần liên thông hai cạnh; co mỗi thành phần thành một đỉnh, các cầu nối chúng thành một cây. Mọi đường s→ts \to t bắt buộc đi qua đúng các cầu trên đường đi giữa hai thành phần trong cây này, nên bài “số cạnh bắt buộc lớn nhất” là đường kính của cây cầu.

Tìm cầu bằng Tarjan: low[v] là tin nhỏ nhất tới được từ cây con của vv bằng một cạnh ngược; cạnh cây (p,v)(p, v) là cầu khi low[v] > tin[p]. Với 3⋅1053 \cdot 10^5 đỉnh, viết DFS bằng ngăn xếp tường minh lưu chỉ số cạnh kế tiếp, và bỏ qua đúng cạnh vừa đi lên (theo chỉ số) chứ không phải đỉnh cha.

Sửa danh sách cạnh để thêm hay phá chu trình; mỗi màu là một thành phần, cạnh đỏ là cầu.

Hỏi nhanh

Đồ thị là hai tam giác {1,2,3}\{1, 2, 3\} và {5,6,7}\{5, 6, 7\} nối bởi đường 3−4−53 - 4 - 5, thêm đỉnh 8 treo vào 7. Cây cầu có bao nhiêu đỉnh?

Ví dụTuyến Đường Bắt Buộc Dài Nhất

Đề. Đồ thị đơn liên thông n,m≤3⋅105n, m \le 3 \cdot 10^5. Chọn s≠ts \ne t để số cạnh mà mọi đường s→ts \to t đều phải qua là lớn nhất.

Lời giải. Cạnh mà mọi đường s→ts \to t đều phải đi qua là cầu nằm giữa thành phần của ss và thành phần của tt trên cây cầu. Vậy đáp án là đường kính của cây cầu. Tìm cầu bằng Tarjan không đệ quy, tô các thành phần liên thông hai cạnh bằng BFS không đi qua cầu, nối các thành phần bằng cầu, rồi tìm đường kính bằng hai lần BFS.

Bước 1: Tarjan bằng ngăn xếp. it[u] là chỉ số cạnh kế tiếp của uu, pe[u] là chỉ số cạnh đi từ cha xuống uu. Bỏ qua đúng cạnh pe[u], không phải mọi cạnh tới đỉnh cha, để cạnh song song vẫn được tính là cạnh ngược. Gặp đỉnh đã thăm thì cập nhật low[u] bằng tin[v]. Hết cạnh thì đưa low[u] lên cha p; low[u] > tin[p] nghĩa là cây con của uu không có cạnh nào vòng lên trên pp, nên cạnh pe[u] là cầu.

C++
int u = st.back();
if (it[u] < (int)adj[u].size()) {
    int v = adj[u][it[u]].first, id = adj[u][it[u]].second;
    it[u]++;
    if (id == pe[u]) continue;
    if (tin[v]) low[u] = min(low[u], tin[v]);
    else {
        tin[v] = low[v] = ++timer, pe[v] = id;
        st.push_back(v);
    }
} else {
    st.pop_back();
    if (!st.empty()) {
        int p = st.back();
        low[p] = min(low[p], low[u]);
        if (low[u] > tin[p]) isBridge[pe[u]] = 1;
    }
}

Bước 2: tô thành phần, dựng cây cầu. BFS từ mỗi đỉnh chưa tô, chỉ đi qua cạnh không phải cầu. Mỗi cầu nối hai thành phần ở hai đầu; đồ thị liên thông nên tr là một cây.

C++
for (int i = 0; i < m; i++)
    if (isBridge[i]) tr[comp[eu[i]]].push_back(comp[ev[i]]), tr[comp[ev[i]]].push_back(comp[eu[i]]);

Bước 3: đường kính. BFS từ thành phần bất kỳ tới đỉnh xa nhất a, rồi BFS từ a; khoảng cách xa nhất lần hai là đường kính.

C++
int a = bfs(1, k), b = bfs(a, k);
cout << dist[b] << '\n';
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N = 3e5 + 5;
int n, m, timer, tin[N], low[N], comp[N], dist[N], eu[N], ev[N], it[N], pe[N];
bool isBridge[N];
vector<pair<int, int>> adj[N];
vector<int> tr[N];

int bfs(int s, int k) {
    for (int i = 1; i <= k; i++) dist[i] = -1;
    queue<int> q;
    q.push(s);
    dist[s] = 0;
    int far = s;
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        if (dist[u] > dist[far]) far = u;
        for (int v : tr[u])
            if (dist[v] < 0) dist[v] = dist[u] + 1, q.push(v);
    }
    return far;
}

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

    cin >> n >> m;
    for (int i = 0; i < m; i++) {
        cin >> eu[i] >> ev[i];
        adj[eu[i]].push_back({ev[i], i});
        adj[ev[i]].push_back({eu[i], i});
    }
    vector<int> st = {1};
    tin[1] = low[1] = ++timer, pe[1] = -1;
    while (!st.empty()) {
        int u = st.back();
        if (it[u] < (int)adj[u].size()) {
            int v = adj[u][it[u]].first, id = adj[u][it[u]].second;
            it[u]++;
            if (id == pe[u]) continue;
            if (tin[v]) low[u] = min(low[u], tin[v]);
            else {
                tin[v] = low[v] = ++timer, pe[v] = id;
                st.push_back(v);
            }
        } else {
            st.pop_back();
            if (!st.empty()) {
                int p = st.back();
                low[p] = min(low[p], low[u]);
                if (low[u] > tin[p]) isBridge[pe[u]] = 1;
            }
        }
    }
    int k = 0;
    for (int s = 1; s <= n; s++) {
        if (comp[s]) continue;
        comp[s] = ++k;
        vector<int> q = {s};
        for (int i = 0; i < (int)q.size(); i++) {
            int u = q[i];
            for (pair<int, int> e : adj[u])
                if (!isBridge[e.second] && !comp[e.first]) comp[e.first] = k, q.push_back(e.first);
        }
    }
    for (int i = 0; i < m; i++)
        if (isBridge[i]) tr[comp[eu[i]]].push_back(comp[ev[i]]), tr[comp[ev[i]]].push_back(comp[eu[i]]);
    int a = bfs(1, k), b = bfs(a, k);
    cout << dist[b] << '\n';
}
8 9
1 2
2 3
3 1
3 4
4 5
5 6
6 7
7 5
7 8

Độ phức tạp: O(n+m)O(n + m).

Khi đi thi

  • Cực trị đoạn, hay “phần tử là cực trị của bao nhiêu đoạn”: cây Cartesian, dựng bằng ngăn xếp đơn điệu.
  • “Ngưỡng nhỏ nhất để nối”, “đỉnh tới được khi chỉ dùng cạnh không quá WW”: cây Kruskal dựng lại, LCA và nhảy nhị phân.
  • Nhiều truy vấn trên tập đỉnh nhỏ: cây ảo. Cạnh bắt buộc, thành phần không có cầu: cây cầu.

Tóm tắt

  • Cây Cartesian: duyệt giữa là thứ tự vị trí, cha nhỏ hơn con; cực tiểu đoạn là LCA.
  • Cây Kruskal: mỗi lần hợp tạo đỉnh mang trọng số cạnh; giá trị tại LCA là ngưỡng nối.
  • Cây ảo: các đỉnh hỏi và LCA của chúng, tối đa 2k−12k - 1 đỉnh, dựng bằng ngăn xếp theo thứ tự DFS.
  • Cây cầu: co thành phần hai cạnh; đường đi trên cây là các cầu bắt buộc.

Bài tập

Đọc thêm