Tuyển sinh 🎓 Mình nhận đào tạo lập trình giải thuật thi đấu: thi vào lớp 10 chuyên tin · thi học sinh giỏi tin học cấp tỉnh/thành · thi vào đội tuyển quốc gia · Olympic tin học sinh viên / ICPC · phỏng vấn thuật toán. Bấm vào đây để xem thông tin khóa học → Xem khóa học
Thi thử miễn phí
🏆 Thi thử HSG Tin học tuần này trên ClueOJ
Mở công khai cho mọi người · đề do thầy Nguyễn Việt Nam Sơn biên soạn
Lịch thi, đề cũ & lời giải →

Bài 34: LCA và nhảy nhị phân trong C++ và Python: tổ tiên chung gần nhất, tổ tiên thứ k, khoảng cách trên cây

Bài 34: LCA và nhảy nhị phân trong C++ và Python: tổ tiên chung gần nhất, tổ tiên thứ k, khoảng cách trên cây
Series: Thuật toán chuyên sâu Độ khó: Khá Bài 34 / Thuật toán chuyên sâu LCA Nhảy nhị phân Cây
// Thuật toán chuyên sâu · Bài 34
LCA và nhảy nhị phân: tổ tiên chung gần nhất trên cây
Thay vì leo từng cạnh, ta nhảy 1, 2, 4, 8… bước một lần. Chỉ với một bảng $n \log n$ ô, mỗi câu hỏi về tổ tiên chung, khoảng cách hay cạnh lớn nhất trên đường đi được trả lời trong $O(\log n)$.

Lời mở đầu

Ở bài 33 em đã quen với cây: chọn gốc, tính cha, độ sâu, kích thước cây con bằng DFS, rồi quy hoạch động trên cây. Những bài đó chỉ cần một lần duyệt là xong. Đề thi HSG lại rất thích kiểu khác: cho một cây cố định và hàng trăm nghìn câu hỏi, mỗi câu hỏi về đường đi giữa hai đỉnh: hai đỉnh cách nhau bao xa, cạnh nặng nhất trên đường đi là bao nhiêu, đỉnh thứ $k$ trên đường đi là đỉnh nào… Chạy lại BFS/DFS (bài 21) cho từng câu hỏi thì quá chậm.

Chìa khoá của mọi câu hỏi đó là tổ tiên chung gần nhất (LCA, viết tắt của Lowest Common Ancestor): đường đi giữa $u$ và $v$ luôn đi lên tới LCA rồi đi xuống. Hôm nay ta học kỹ thuật nhảy nhị phân (binary lifting) để tìm LCA trong $O(\log n)$. Kỹ thuật này là sự kết hợp của hai thứ em đã biết: viết một số thành tổng các lũy thừa của 2 (thao tác bit, bài 20) và tư duy “thử bước lớn trước” của tìm kiếm nhị phân (bài 16).

Trong bài này bạn sẽ học: tổ tiên chung gần nhất là gì và cách tìm ngây thơ $O(n)$ mỗi truy vấn; bảng nhảy nhị phân up[k][u] (tổ tiên thứ $2^k$) và công thức xây bảng $2^k = 2^{k-1} + 2^{k-1}$; thuật toán LCA bằng nhảy nhị phân $O(\log n)$ mỗi truy vấn và vì sao nó đúng; các ứng dụng: khoảng cách hai đỉnh, tổ tiên thứ k, cạnh lớn nhất trên đường đi; thứ tự DFS tin/tout để kiểm tra tổ tiên trong $O(1)$, và ý tưởng LCA bằng Euler tour + RMQ. Có khung chạy tay LCA, 3 mô phỏng từng bước và 8 bài tập kèm gợi ý.

Lộ trình bài viết, bấm vào từng phần để nhảy tới:

Phần 1Tổ tiên chung gần nhất là gì?

Cho một cây $n$ đỉnh, ta chọn đỉnh 1 làm gốc. Nhắc lại vài khái niệm từ bài 33:

  • Cha của $v$ là đỉnh kề với $v$ nằm trên đường từ $v$ về gốc. Độ sâu dep[v] là số cạnh từ gốc tới $v$ (gốc có độ sâu 0).
  • Tổ tiên của $v$ là mọi đỉnh nằm trên đường đi từ $v$ lên gốc, tính cả chính $v$. Ví dụ ở Hình 1, tổ tiên của 9 là 9, 5, 2, 1.
  • Tổ tiên chung của $u$ và $v$ là đỉnh vừa là tổ tiên của $u$, vừa là tổ tiên của $v$. Gốc luôn là một tổ tiên chung.
  • LCA(u, v) là tổ tiên chung sâu nhất, tức là gần $u$ và $v$ nhất.
độ sâu 0độ sâu 1độ sâu 2độ sâu 3độ sâu 4độ sâu 512LCA(12, 9)3456789101112

Hình 1. Cây ví dụ dùng xuyên suốt bài (gốc 1). Đường đi từ 12 tới 9 đi lên 12 → 11 → 7 → 4 → 2 rồi xuống 2 → 5 → 9. Đỉnh cao nhất trên đường đi chính là LCA(12, 9) = 2.

Cặp đỉnhTổ tiên của đỉnh thứ nhấtTổ tiên của đỉnh thứ haiLCA
12, 912, 11, 7, 4, 2, 19, 5, 2, 12
12, 812, 11, 7, 4, 2, 18, 4, 2, 14
10, 1210, 6, 3, 112, 11, 7, 4, 2, 11
7, 117, 4, 2, 111, 7, 4, 2, 17 (7 là tổ tiên của 11)

Bảng 1. LCA là tổ tiên chung đầu tiên gặp được khi đi từ dưới lên. Nếu $u$ là tổ tiên của $v$ thì LCA chính là $u$.

Vì sao LCA quan trọng? Trên cây, giữa hai đỉnh chỉ có đúng một đường đi đơn, và đường đi đó luôn gồm hai đoạn: từ $u$ đi lên tới $l = $ LCA(u, v), rồi từ $l$ đi xuống tới $v$. Biết $l$ là biết hình dạng đường đi, ví dụ độ dài đường đi là $\text{dep}[u] + \text{dep}[v] - 2 \cdot \text{dep}[l]$. Với cặp (12, 9): $5 + 3 - 2 \cdot 1 = 6$ cạnh, đúng như Hình 1.

Cách ngây thơ. Trước hết duyệt BFS một lần từ gốc để có par[v] và dep[v]. Với mỗi truy vấn:

  1. Đỉnh nào sâu hơn thì cho leo lên cha từng bước, tới khi hai đỉnh cùng độ sâu.
  2. Cho cả hai cùng leo lên cha từng bước, tới khi chúng trùng nhau. Đỉnh gặp nhau chính là LCA.

Bước 2 đúng vì hai đỉnh cùng độ sâu thì tổ tiên chung của chúng cũng nằm ở cùng độ sâu với nhau: cứ leo song song, lần đầu tiên trùng nhau là tổ tiên chung sâu nhất.

Bướcu (độ sâu)v (độ sâu)Việc làm
112 (5)9 (3)u sâu hơn → u lên cha
211 (4)9 (3)u vẫn sâu hơn → u lên cha
37 (3)9 (3)cùng độ sâu, 7 ≠ 9 → cả hai lên cha
44 (2)5 (2)4 ≠ 5 → cả hai lên cha
52 (1)2 (1)trùng nhau → LCA = 2

Bảng 2. Tìm LCA(12, 9) bằng cách leo từng bước: mất 6 lần leo, đúng bằng độ dài đường đi.

Xem code C++ / Python: lca ngây thơ, leo từng bước (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005;
int n, q, par[MAXN], dep[MAXN];
vector<int> adj[MAXN];

int lca(int u, int v){
    while(dep[u] > dep[v]) u = par[u];      // đưa đỉnh sâu hơn lên cùng độ sâu
    while(dep[v] > dep[u]) v = par[v];
    while(u != v){                          // cùng leo từng bước tới khi gặp nhau
        u = par[u];
        v = par[v];
    }
    return u;
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    for(int i = 1; i < n; i++){
        int u, v; cin >> u >> v;
        adj[u].push_back(v); adj[v].push_back(u);
    }
    vector<int> order = {1};                // BFS từ gốc 1: tính cha và độ sâu
    vector<bool> seen(n + 1, false);
    seen[1] = true; par[1] = 0;
    for(int i = 0; i < (int)order.size(); i++){
        int u = order[i];
        for(int v : adj[u]) if(!seen[v]){
            seen[v] = true; par[v] = u; dep[v] = dep[u] + 1;
            order.push_back(v);
        }
    }
    while(q--){
        int u, v; cin >> u >> v;
        cout << lca(u, v) << "\n";
    }
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    adj = [[] for _ in range(n + 1)]
    p = 2
    for _ in range(n - 1):
        u, v = int(data[p]), int(data[p + 1]); p += 2
        adj[u].append(v); adj[v].append(u)
    par = [0] * (n + 1); dep = [0] * (n + 1)
    seen = [False] * (n + 1); seen[1] = True
    order = [1]                          # BFS từ gốc 1: tính cha và độ sâu
    for u in order:
        for v in adj[u]:
            if not seen[v]:
                seen[v] = True; par[v] = u; dep[v] = dep[u] + 1
                order.append(v)
    out = []
    for _ in range(q):
        u, v = int(data[p]), int(data[p + 1]); p += 2
        while dep[u] > dep[v]: u = par[u]       # đưa đỉnh sâu hơn lên
        while dep[v] > dep[u]: v = par[v]
        while u != v:                           # cùng leo từng bước
            u = par[u]; v = par[v]
        out.append(u)
    print('\n'.join(map(str, out)))

main()
Ngây thơ chậm ở đâu?
Mỗi truy vấn tốn $O(\text{độ sâu})$. Nếu đề cho cây dạng đường thẳng 1 – 2 – 3 – … – $n$ (người ra đề rất hay cài sẵn test này) thì độ sâu tới $n - 1$. Với $n = q = 2 \cdot 10^5$, tổng cộng cỡ $4 \cdot 10^{10}$ bước leo: chắc chắn quá thời gian. Ta cần leo nhanh hơn từng bước một.

Phần 2Bảng nhảy nhị phân up[k][u]

Ý tưởng: thay vì chỉ biết cha (tổ tiên cách 1 bước), mỗi đỉnh nhớ thêm tổ tiên cách 2, 4, 8, 16, … bước. Gọi

up[k][u] = tổ tiên thứ $2^k$ của $u$ (đi lên từ $u$ đúng $2^k$ cạnh).

Vì sao lại là lũy thừa của 2? Vì mọi số tự nhiên đều viết được thành tổng các lũy thừa của 2 khác nhau, chính là biểu diễn nhị phân (bài 20). Muốn đi lên 13 bước, ta viết $13 = 1101_2 = 8 + 4 + 1$ và nhảy 3 cú: 8 bước, 4 bước, 1 bước. Mỗi cú tra bảng $O(1)$, nên đi lên $d$ bước chỉ tốn $O(\log d)$ thay vì $O(d)$.

Xây bảng. Hàng $k = 0$ chính là cha: up[0][u] = par[u]. Với $k \ge 1$, nhảy $2^k$ bước bằng nhảy $2^{k-1}$ bước hai lần:

$\text{up}[k][u] = \text{up}[k-1]\big[\,\text{up}[k-1][u]\,\big]$

Đây đúng là một công thức quy hoạch động: hàng $k$ chỉ cần hàng $k - 1$ đã tính xong. Ta tính lần lượt từng hàng $k = 1, 2, \ldots$, mỗi hàng duyệt mọi đỉnh. Còn nhảy quá gốc thì sao? Ta quy ước cha của gốc là chính nó (up[0][1] = 1): nhảy quá gốc thì đứng yên ở gốc. Quy ước này làm thuật toán LCA ở phần 3 gọn hơn nhiều.

u123456789101112
dep[u]011222333345
up[0] (2^0 = 1 bước)1112234456711
up[1] (2^1 = 2 bước)111111222347
up[2] (2^2 = 4 bước)111111111112

Bảng 3. Bảng nhảy nhị phân của cây ở Hình 1. Ví dụ up[2][12]: nhảy 2 bước 12 → 7 (up[1][12] = 7), rồi thêm 2 bước 7 → 2 (up[1][7] = 2), nên up[2][12] = 2. Ô xanh là chỗ đã nhảy chạm hoặc vượt quá gốc. Cây sâu nhất 5 nên 3 hàng ($2^0, 2^1, 2^2$) là đủ.

Đọc công thức thì thấy đơn giản, nhưng tự điền vài ô mới thấm. Em bấm Tiếp để xem từng ô được điền: đường vàng là cú nhảy $2^{k-1}$ đầu tiên, đường xanh lá là cú nhảy $2^{k-1}$ thứ hai. Có thể đổi cây tuỳ thích:

MÔ PHỎNGXây bảng nhảy nhị phân up[k][u] từng ôNhập cha của các đỉnh 2, 3, …, n (tối đa 16 đỉnh, gốc là 1). Mỗi bước điền một ô của bảng.
đỉnh u đang điềnđỉnh giữa up[k−1][u]kết quả up[k][u]ô chưa tính
Khuôn mẫu xây bảng nhảy nhị phân
const int LOG = 18;              // 2^LOG phải lớn hơn độ sâu lớn nhất (n ≤ 2e5 → 18)
int up[LOG][MAXN], dep[MAXN];
// sau khi BFS/DFS từ gốc: up[0][v] = cha của v, up[0][root] = root
for(int k = 1; k < LOG; k++)
    for(int v = 1; v <= n; v++)
        up[k][v] = up[k - 1][up[k - 1][v]];
Thời gian và bộ nhớ đều $O(n \log n)$: với $n = 2 \cdot 10^5$ là $18 \cdot 2 \cdot 10^5 = 3{,}6$ triệu số int, khoảng 14 MB.
Ba lỗi hay gặp khi xây bảng
1. Đảo thứ tự vòng lặp. Vòng $k$ phải nằm ngoài, vòng đỉnh nằm trong, vì ô up[k][v] cần ô up[k-1][x] của một đỉnh $x$ khác. (Nếu muốn để vòng đỉnh ngoài thì phải duyệt đỉnh theo thứ tự BFS/DFS, cha trước con.)
2. LOG quá nhỏ. Cần $2^{\text{LOG}} >$ độ sâu lớn nhất, nếu không cú nhảy lớn nhất không đủ xa. Với cây đường thẳng $n$ đỉnh, độ sâu là $n - 1$.
3. Khai báo up[MAXN][LOG] cũng đúng, nhưng up[LOG][MAXN] chạy nhanh hơn vì khi xây bảng ta duyệt liên tiếp một hàng trong bộ nhớ.

Phần 3LCA bằng nhảy nhị phân trong O(log n)

Giữ nguyên hai giai đoạn của cách ngây thơ, nhưng thay “leo từng bước” bằng “nhảy theo lũy thừa của 2”:

  1. Đưa về cùng độ sâu. Giả sử $u$ sâu hơn, đặt $d = \text{dep}[u] - \text{dep}[v]$. Với mỗi bit $k$ bằng 1 trong $d$, cho u = up[k][u]. Sau đó $u$ và $v$ cùng độ sâu. Nếu lúc này $u = v$ thì $v$ là tổ tiên của đỉnh ban đầu, đáp án là $u$.
  2. Cùng nhảy, càng xa càng tốt nhưng không được gặp nhau. Duyệt $k$ từ lớn xuống nhỏ: nếu up[k][u] != up[k][v] thì cho cả hai nhảy: u = up[k][u], v = up[k][v]. Nếu hai đích trùng nhau thì không nhảy. Kết thúc vòng lặp, $u$ và $v$ đứng ngay dưới LCA, đáp án là up[0][u].

Vì sao giai đoạn 2 đúng? Gọi $D$ là số bước từ $u$ (và từ $v$, vì cùng độ sâu) lên tới LCA. Xét câu hỏi “sau $j$ bước, $u$ và $v$ đã gặp nhau chưa?”. Với $j < D$: chưa gặp (hai đỉnh khác nhau). Với $j \ge D$: đã gặp (cùng ở LCA hoặc cao hơn). Câu trả lời có dạng chưa, chưa, …, chưa, rồi, rồi, …, giống hệt điều kiện đơn điệu của tìm kiếm nhị phân. Ta đang tìm $j$ lớn nhất mà vẫn “chưa gặp”, tức $j = D - 1$, bằng cách dựng từng bit của nó từ cao xuống thấp: thử cộng thêm $2^k$, nếu vẫn chưa gặp thì giữ lại cú nhảy đó. Đây chính là chặt nhị phân trên các bit. Sau vòng lặp, $u, v$ đã đi đúng $D - 1$ bước, chỉ cần thêm 1 bước nữa là tới LCA.

Vì sao không được nhảy khi hai đích trùng nhau? Vì đỉnh trùng đó là một tổ tiên chung, nhưng có thể cao hơn LCA (đã vượt quá). Ta không biết đã vượt bao xa, nên an toàn nhất là không nhảy và thử cú nhỏ hơn.

Giai đoạnkup[k][u]up[k][v]Quyết địnhu, v sau bước
1: đưa về cùng độ sâu1up[1][12] = 7–$d = 5 - 3 = 2 = 10_2$: bit 1 bật → u nhảy 2 bướcu = 7, v = 9
2: cùng nhảy2up[2][7] = 1up[2][9] = 1trùng → không nhảyu = 7, v = 9
2: cùng nhảy1up[1][7] = 2up[1][9] = 2trùng → không nhảyu = 7, v = 9
2: cùng nhảy0up[0][7] = 4up[0][9] = 5khác → nhảyu = 4, v = 5
kết thúcđáp án = up[0][4]LCA = 2

Bảng 4. Tìm LCA(12, 9) bằng nhảy nhị phân với bảng 3. Chú ý ở k = 1: đỉnh 2 trùng nhau và đó đúng là LCA, nhưng thuật toán không biết điều đó, nên vẫn không nhảy. Nó dừng ngay dưới LCA rồi mới bước lên.

Em xem từng cú nhảy trên cây trong mô phỏng dưới đây. Thử thêm cặp $u, v$ mà một đỉnh là tổ tiên của đỉnh kia (ví dụ 7 và 12), và một cây dạng đường thẳng (nhập 1 2 3 4 5 6 7 8):

MÔ PHỎNGLCA bằng nhảy nhị phân: đưa u, v lên từng bướcNhập cây và hai đỉnh u, v. Đường vàng là quãng u đã nhảy, đường xanh dương là quãng v đã nhảy.
uvtổ tiên chung, không nhảyLCA
Xem code C++ / Python: lca bằng nhảy nhị phân (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005, LOG = 18;          // 2^18 > 200000
int n, q, up[LOG][MAXN], dep[MAXN];
vector<int> adj[MAXN];

int lca(int u, int v){
    if(dep[u] < dep[v]) swap(u, v);         // u là đỉnh sâu hơn
    int d = dep[u] - dep[v];
    for(int k = 0; k < LOG; k++)            // giai đoạn 1: đưa u lên cùng độ sâu với v
        if(d >> k & 1) u = up[k][u];
    if(u == v) return u;
    for(int k = LOG - 1; k >= 0; k--)       // giai đoạn 2: nhảy xa nhất mà chưa gặp nhau
        if(up[k][u] != up[k][v]){
            u = up[k][u];
            v = up[k][v];
        }
    return up[0][u];                        // u, v đang đứng ngay dưới LCA
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    for(int i = 1; i < n; i++){
        int u, v; cin >> u >> v;
        adj[u].push_back(v); adj[v].push_back(u);
    }
    vector<int> order = {1};                // BFS từ gốc 1: cha up[0][v] và độ sâu dep[v]
    vector<bool> seen(n + 1, false);
    seen[1] = true; up[0][1] = 1;           // quy ước: cha của gốc là chính nó
    for(int i = 0; i < (int)order.size(); i++){
        int u = order[i];
        for(int v : adj[u]) if(!seen[v]){
            seen[v] = true; up[0][v] = u; dep[v] = dep[u] + 1;
            order.push_back(v);
        }
    }
    for(int k = 1; k < LOG; k++)            // xây bảng: nhảy 2^k = nhảy 2^(k-1) hai lần
        for(int v = 1; v <= n; v++)
            up[k][v] = up[k - 1][up[k - 1][v]];
    while(q--){
        int u, v; cin >> u >> v;
        cout << lca(u, v) << "\n";
    }
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    adj = [[] for _ in range(n + 1)]
    p = 2
    for _ in range(n - 1):
        u, v = int(data[p]), int(data[p + 1]); p += 2
        adj[u].append(v); adj[v].append(u)
    LOG = max(1, n.bit_length())            # 2^LOG > n > độ sâu lớn nhất
    par = [0] * (n + 1); dep = [0] * (n + 1)
    par[1] = 1                              # quy ước: cha của gốc là chính nó
    seen = [False] * (n + 1); seen[1] = True
    order = [1]
    for u in order:                         # BFS từ gốc 1
        for v in adj[u]:
            if not seen[v]:
                seen[v] = True; par[v] = u; dep[v] = dep[u] + 1
                order.append(v)
    up = [par]
    for k in range(1, LOG):                 # nhảy 2^k = nhảy 2^(k-1) hai lần
        prev = up[-1]
        up.append([prev[prev[v]] for v in range(n + 1)])
    rows_desc = up[::-1]                    # các hàng theo thứ tự k giảm dần

    def lca(u, v):
        if dep[u] < dep[v]:
            u, v = v, u
        d, k = dep[u] - dep[v], 0
        while d:                            # giai đoạn 1: theo từng bit của d
            if d & 1:
                u = up[k][u]
            d >>= 1; k += 1
        if u == v:
            return u
        for row in rows_desc:               # giai đoạn 2: k từ LOG - 1 về 0
            a, b = row[u], row[v]
            if a != b:                      # nhảy xa nhất mà chưa gặp nhau
                u, v = a, b
        return par[u]

    out = []
    for _ in range(q):
        u, v = int(data[p]), int(data[p + 1]); p += 2
        out.append(lca(u, v))
    print('\n'.join(map(str, out)))

main()

Khung chạy tay dưới đây chạy đúng chương trình trên với cây nhỏ (LOG = 4 đủ cho cây tối đa 15 đỉnh). Em quan sát cách BFS điền hàng up[0] và dep, cách mỗi hàng up[k] được suy ra từ hàng trên, rồi hai giai đoạn của hàm lca:

C++▶ Chạy tay từng dòng codeBFS tính cha và độ sâu, xây bảng up theo từng hàng, rồi trả lời truy vấn LCA bằng hai giai đoạn nhảy (cột vàng: u, cột đỏ: v).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4const int LOG = 4;
5int n, q, up[LOG][20], dep[20];
6vector<int> adj[20];
7
8int lca(int u, int v){
9 if(dep[u] < dep[v]) swap(u, v);
10 int d = dep[u] - dep[v];
11 for(int k = 0; k < LOG; k++)
12 if(d >> k & 1) u = up[k][u];
13 if(u == v) return u;
14 for(int k = LOG - 1; k >= 0; k--)
15 if(up[k][u] != up[k][v]){
16 u = up[k][u]; v = up[k][v];
17 }
18 return up[0][u];
19}
20
21int main(){
22 cin >> n >> q;
23 for(int i = 1; i < n; i++){
24 int u, v; cin >> u >> v;
25 adj[u].push_back(v); adj[v].push_back(u);
26 }
27 vector<int> order = {1};
28 vector<bool> seen(n + 1, false);
29 seen[1] = true; up[0][1] = 1;
30 for(int i = 0; i < (int)order.size(); i++){
31 int u = order[i];
32 for(int v : adj[u]) if(!seen[v]){
33 seen[v] = true; up[0][v] = u; dep[v] = dep[u] + 1; order.push_back(v);
34 }
35 }
36 for(int k = 1; k < LOG; k++)
37 for(int v = 1; v <= n; v++)
38 up[k][v] = up[k - 1][up[k - 1][v]];
39 while(q--){
40 int u, v; cin >> u >> v;
41 cout << lca(u, v) << "\n";
42 }
43 return 0;
44}
Độ phức tạp
Xây bảng $O(n \log n)$, mỗi truy vấn $O(\log n)$ (mỗi giai đoạn tối đa LOG bước). Với $n = q = 2 \cdot 10^5$: khoảng $2 \cdot 10^5 \cdot 18 \cdot 2 \approx 7 \cdot 10^6$ phép tính cho tất cả truy vấn, so với $4 \cdot 10^{10}$ của cách ngây thơ. Với Python, nhớ đọc dữ liệu bằng sys.stdin.buffer và dùng BFS thay cho DFS đệ quy (Python giới hạn độ sâu đệ quy khoảng 1000).
Cẩn thận ở giai đoạn 2
Vòng lặp giai đoạn 2 bắt buộc chạy $k$ từ lớn xuống nhỏ. Chạy từ nhỏ lên lớn sai, vì sau khi nhảy cú nhỏ, các cú lớn có thể đưa hai đỉnh vượt quá LCA mà ta không phát hiện được. Ngược lại, giai đoạn 1 (đi lên đúng $d$ bước) thì duyệt bit theo thứ tự nào cũng được, vì tổng các cú nhảy luôn bằng $d$. Và đừng quên dòng if(u == v) return u; sau giai đoạn 1: thiếu nó, thuật toán trả về cha của LCA khi một đỉnh là tổ tiên của đỉnh kia.

Phần 4Ứng dụng: khoảng cách, tổ tiên thứ k, cạnh lớn nhất

1. Khoảng cách giữa hai đỉnh. Với $l = $ LCA(u, v), đường đi gồm $\text{dep}[u] - \text{dep}[l]$ cạnh đi lên và $\text{dep}[v] - \text{dep}[l]$ cạnh đi xuống:

$\text{dist}(u, v) = \text{dep}[u] + \text{dep}[v] - 2 \cdot \text{dep}[l]$

Nếu cạnh có trọng số, thay độ sâu bằng $D[v]$ = tổng trọng số từ gốc tới $v$ (tính luôn trong lúc BFS: $D[v] = D[\text{cha}] + w$), khi đó độ dài đường đi là $D[u] + D[v] - 2D[l]$. Phần đường từ gốc tới $l$ được tính hai lần trong $D[u] + D[v]$ nên phải trừ đi hai lần. Nhớ dùng long long cho $D$.

2. Tổ tiên thứ k. Đây chính là giai đoạn 1 của LCA: viết $k$ ở hệ nhị phân, bit $b$ nào bằng 1 thì nhảy u = up[b][u]. Nhớ kiểm tra $k \le \text{dep}[u]$ trước: nếu $k$ lớn hơn độ sâu thì không có tổ tiên đó (với quy ước cha của gốc là gốc, nhảy quá sẽ “dính” ở gốc và cho đáp án sai). Ví dụ tổ tiên thứ 5 của 12: $5 = 101_2$, nhảy 1 bước 12 → 11, rồi 4 bước 11 → 1.

Mô phỏng sau tách $k$ thành từng bit và nhảy từng cú. Em thử $k$ lớn hơn độ sâu của $u$ để thấy trường hợp “không tồn tại”:

MÔ PHỎNGTổ tiên thứ k: nhảy theo từng bit 1 của kNhập cây, đỉnh u và k (0 ≤ k ≤ 15). Bit nào bằng 1 thì nhảy cú 2^bit tương ứng.
đỉnh xuất phátbit đang xét, chỗ đang đứngđáp ánbit chưa xét

3. Cạnh lớn nhất (hoặc nhỏ nhất) trên đường đi. Nhảy nhị phân không chỉ lưu “đi tới đâu” mà còn lưu được “đi qua những gì”. Thêm bảng thứ hai:

mx[k][u] = trọng số lớn nhất trong $2^k$ cạnh đi từ $u$ lên up[k][u].

Hàng đầu mx[0][u] là trọng số cạnh nối $u$ với cha. Nhảy $2^k$ = hai lần nhảy $2^{k-1}$, nên cạnh lớn nhất là số lớn hơn trong hai đoạn: mx[k][u] = max(mx[k-1][u], mx[k-1][up[k-1][u]]). Khi tìm LCA, mỗi lần nhảy ta cập nhật best = max(best, mx[k][u]), và cuối giai đoạn 2 cộng thêm hai cạnh cuối cùng đi vào LCA. Cách làm y hệt cho cạnh nhỏ nhất, tổng (thay vì dùng $D$), hay bất cứ phép toán nào có tính kết hợp.

Gộp thông tin theo đường nhảy
Bất cứ đại lượng nào tính được bằng cách ghép hai nửa đường đi (max, min, tổng, gcd, xor…) đều lưu được vào bảng song song với up: val[k][u] = ghep(val[k-1][u], val[k-1][up[k-1][u]]). Đây cũng là ý tưởng của Sparse Table mà em sẽ học ở bài 37.

Chương trình dưới đây nhận cây có trọng số, mỗi truy vấn in ra độ dài đường đi và cạnh lớn nhất trên đường đi giữa $u$ và $v$. Ví dụ với cây 5 đỉnh: 1 – 2 (3), 1 – 3 (8), 2 – 4 (5), 2 – 5 (1). Đường 4 → 2 → 1 → 3 dài $5 + 3 + 8 = 16$, cạnh lớn nhất 8.

Xem code C++ / Python: khoảng cách và cạnh lớn nhất trên đường đi (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005, LOG = 18;
int n, q, up[LOG][MAXN], mx[LOG][MAXN], dep[MAXN];
long long D[MAXN];                              // tổng trọng số từ gốc tới v
vector<pair<int, int>> adj[MAXN];               // (đỉnh kề, trọng số)

// trả về (LCA, cạnh lớn nhất trên đường đi u - v)
pair<int, int> query(int u, int v){
    int best = 0;
    if(dep[u] < dep[v]) swap(u, v);
    int d = dep[u] - dep[v];
    for(int k = 0; k < LOG; k++)
        if(d >> k & 1){ best = max(best, mx[k][u]); u = up[k][u]; }
    if(u == v) return {u, best};
    for(int k = LOG - 1; k >= 0; k--)
        if(up[k][u] != up[k][v]){
            best = max({best, mx[k][u], mx[k][v]});
            u = up[k][u]; v = up[k][v];
        }
    best = max({best, mx[0][u], mx[0][v]});     // hai cạnh cuối đi vào LCA
    return {up[0][u], best};
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    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> order = {1};
    vector<bool> seen(n + 1, false);
    seen[1] = true; up[0][1] = 1; mx[0][1] = 0;
    for(int i = 0; i < (int)order.size(); i++){
        int u = order[i];
        for(auto [v, w] : adj[u]) if(!seen[v]){
            seen[v] = true; up[0][v] = u; mx[0][v] = w;   // mx[0][v] = trọng số cạnh v - cha
            dep[v] = dep[u] + 1; D[v] = D[u] + w;
            order.push_back(v);
        }
    }
    for(int k = 1; k < LOG; k++)
        for(int v = 1; v <= n; v++){
            int mid = up[k - 1][v];
            up[k][v] = up[k - 1][mid];
            mx[k][v] = max(mx[k - 1][v], mx[k - 1][mid]);
        }
    while(q--){
        int u, v; cin >> u >> v;
        auto [l, best] = query(u, v);
        cout << D[u] + D[v] - 2 * D[l] << " " << best << "\n";
    }
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    adj = [[] for _ in range(n + 1)]
    p = 2
    for _ in range(n - 1):
        u, v, w = int(data[p]), int(data[p + 1]), int(data[p + 2]); p += 3
        adj[u].append((v, w)); adj[v].append((u, w))
    LOG = max(1, n.bit_length())
    par = [0] * (n + 1); m0 = [0] * (n + 1); dep = [0] * (n + 1); D = [0] * (n + 1)
    par[1] = 1
    seen = [False] * (n + 1); seen[1] = True
    order = [1]
    for u in order:
        for v, w in adj[u]:
            if not seen[v]:
                seen[v] = True; par[v] = u; m0[v] = w     # m0[v] = trọng số cạnh v - cha
                dep[v] = dep[u] + 1; D[v] = D[u] + w
                order.append(v)
    up, mx = [par], [m0]
    for k in range(1, LOG):
        pu, pm = up[-1], mx[-1]
        up.append([pu[pu[v]] for v in range(n + 1)])
        mx.append([max(pm[v], pm[pu[v]]) for v in range(n + 1)])

    def query(u, v):                    # (LCA, cạnh lớn nhất trên đường đi)
        best = 0
        if dep[u] < dep[v]:
            u, v = v, u
        d, k = dep[u] - dep[v], 0
        while d:
            if d & 1:
                best = max(best, mx[k][u]); u = up[k][u]
            d >>= 1; k += 1
        if u == v:
            return u, best
        for k in range(LOG - 1, -1, -1):
            if up[k][u] != up[k][v]:
                best = max(best, mx[k][u], mx[k][v])
                u = up[k][u]; v = up[k][v]
        return up[0][u], max(best, mx[0][u], mx[0][v])

    out = []
    for _ in range(q):
        u, v = int(data[p]), int(data[p + 1]); p += 2
        l, best = query(u, v)
        out.append('%d %d' % (D[u] + D[v] - 2 * D[l], best))
    print('\n'.join(out))

main()

Phần 5Thứ tự DFS tin/tout và LCA bằng Euler tour

Khi duyệt DFS từ gốc, ghi lại thời điểm vào tin[u] (đếm từ 1, tăng mỗi khi vào một đỉnh mới) và thời điểm ra tout[u] = giá trị bộ đếm lúc rời $u$, tức là tin lớn nhất trong cây con của $u$. Vì DFS đi hết cây con của $u$ rồi mới rời $u$, các đỉnh trong cây con của $u$ có tin nằm liền một đoạn $[\text{tin}[u], \text{tout}[u]]$. Từ đó:

$u$ là tổ tiên của $v$ $\iff$ $\text{tin}[u] \le \text{tin}[v] \le \text{tout}[u]$

Kiểm tra tổ tiên chỉ còn $O(1)$, không cần leo gì cả.

độ sâu 0độ sâu 1độ sâu 2độ sâu 3độ sâu 4độ sâu 51[1, 12]2[2, 9]3[10, 12]4[3, 7]5[8, 9]6[11, 12]7[4, 6]8[7, 7]9[9, 9]10[12, 12]11[5, 6]12[6, 6]

Hình 2. Đoạn $[\text{tin}, \text{tout}]$ của mỗi đỉnh (DFS thăm con theo thứ tự số tăng dần). Đoạn của 4 là [3, 7]; 12 có tin = 6 nằm trong đoạn nên 4 là tổ tiên của 12; 9 có tin = 9 nằm ngoài nên 4 không phải tổ tiên của 9.

LCA bằng tin/tout. Có hàm isAnc rồi, LCA viết gọn hơn nữa và không cần mảng độ sâu: nếu $u$ là tổ tiên của $v$ (hoặc ngược lại) thì trả lời ngay. Nếu không, đưa $u$ lên cao nhất có thể mà chưa là tổ tiên của $v$: duyệt $k$ từ lớn xuống nhỏ, nếu up[k][u] chưa phải tổ tiên của $v$ thì nhảy. Cuối cùng cha của $u$ là LCA. Đây vẫn là chặt nhị phân trên các bit, chỉ đổi điều kiện “chưa gặp nhau” thành “chưa là tổ tiên”.

Xem code C++ / Python: lca bằng tin/tout và nhảy nhị phân (bấm để mở)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005, LOG = 18;
int n, q, up[LOG][MAXN], tin[MAXN], tout[MAXN];
vector<int> adj[MAXN];

bool isAnc(int u, int v){                       // u có phải tổ tiên của v (kể cả u = v)?
    return tin[u] <= tin[v] && tin[v] <= tout[u];
}

int lca(int u, int v){
    if(isAnc(u, v)) return u;
    if(isAnc(v, u)) return v;
    for(int k = LOG - 1; k >= 0; k--)           // đưa u lên cao nhất mà CHƯA là tổ tiên của v
        if(!isAnc(up[k][u], v)) u = up[k][u];
    return up[0][u];
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    for(int i = 1; i < n; i++){
        int u, v; cin >> u >> v;
        adj[u].push_back(v); adj[v].push_back(u);
    }
    // DFS không đệ quy (cây dạng đường thẳng sâu 2e5 dễ tràn ngăn xếp nếu đệ quy)
    int timer = 0;
    vector<int> st = {1}, it(n + 1, 0);
    up[0][1] = 1; tin[1] = ++timer;
    while(!st.empty()){
        int u = st.back();
        if(it[u] < (int)adj[u].size()){
            int v = adj[u][it[u]++];
            if(v == up[0][u]) continue;          // bỏ qua cạnh về cha
            up[0][v] = u; tin[v] = ++timer;      // vào đỉnh v
            st.push_back(v);
        } else {
            tout[u] = timer;                     // rời u: tout = tin lớn nhất trong cây con
            st.pop_back();
        }
    }
    for(int k = 1; k < LOG; k++)
        for(int v = 1; v <= n; v++)
            up[k][v] = up[k - 1][up[k - 1][v]];
    while(q--){
        int u, v; cin >> u >> v;
        cout << lca(u, v) << "\n";
    }
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    adj = [[] for _ in range(n + 1)]
    p = 2
    for _ in range(n - 1):
        u, v = int(data[p]), int(data[p + 1]); p += 2
        adj[u].append(v); adj[v].append(u)
    LOG = max(1, n.bit_length())
    par = [0] * (n + 1); par[1] = 1
    tin = [0] * (n + 1); tout = [0] * (n + 1)
    timer = 1; tin[1] = 1
    st, it = [1], [0] * (n + 1)
    while st:                                # DFS không đệ quy
        u = st[-1]
        if it[u] < len(adj[u]):
            v = adj[u][it[u]]; it[u] += 1
            if v == par[u]:
                continue
            par[v] = u; timer += 1; tin[v] = timer
            st.append(v)
        else:
            tout[u] = timer                  # tin lớn nhất trong cây con của u
            st.pop()
    up = [par]
    for k in range(1, LOG):
        prev = up[-1]
        up.append([prev[prev[v]] for v in range(n + 1)])

    def is_anc(u, v):
        return tin[u] <= tin[v] <= tout[u]

    def lca(u, v):
        if is_anc(u, v):
            return u
        if is_anc(v, u):
            return v
        for k in range(LOG - 1, -1, -1):
            w = up[k][u]
            if not is_anc(w, v):
                u = w
        return up[0][u]

    out = []
    for _ in range(q):
        u, v = int(data[p]), int(data[p + 1]); p += 2
        out.append(lca(u, v))
    print('\n'.join(map(str, out)))

main()
Vì sao viết DFS không đệ quy?
Cây dạng đường thẳng $2 \cdot 10^5$ đỉnh làm DFS đệ quy sâu $2 \cdot 10^5$ tầng: C++ có thể tràn ngăn xếp trên một số máy chấm, Python thì chắc chắn lỗi. Cách viết bằng ngăn xếp ở trên mô phỏng đúng DFS đệ quy: mảng it[u] nhớ đã xét tới đỉnh kề thứ mấy của $u$, hết đỉnh kề thì rời $u$ (ghi tout).

LCA bằng Euler tour + RMQ. Có một cách nhìn hoàn toàn khác. Khi DFS, ghi đỉnh hiện tại vào một dãy mỗi khi vào đỉnh đó và mỗi khi quay về nó từ một con. Dãy này (gọi là Euler tour) có đúng $2n - 1$ phần tử. Gọi first[u] là vị trí xuất hiện đầu tiên của $u$. Trên đoạn từ first[u] tới first[v], DFS đi từ $u$ sang $v$ nên chắc chắn đi qua LCA, và không bao giờ lên cao hơn LCA. Vậy:

LCA(u, v) = đỉnh có độ sâu nhỏ nhất trong đoạn Euler tour từ first[u] tới first[v]
vị trí012345678910111213141516171819202122
đỉnh124711121174842595213610631
độ sâu01234543232123210123210

Bảng 5. Euler tour của cây Hình 1 (23 phần tử). Đoạn từ first[12] = 5 tới first[9] = 13 (tô màu) có độ sâu nhỏ nhất bằng 1 tại đỉnh 2, đúng là LCA(12, 9).

Bài toán LCA biến thành bài tìm min trên đoạn (RMQ, Range Minimum Query) của một mảng cố định. Dùng Segment Tree (bài 31) thì mỗi truy vấn $O(\log n)$. Dùng Sparse Table thì sau khi chuẩn bị $O(n \log n)$, mỗi truy vấn chỉ $O(1)$. Em sẽ học Sparse Table kỹ ở bài 37, và sẽ thấy nó cũng dùng đúng ý tưởng “đoạn dài $2^k$ = hai đoạn dài $2^{k-1}$” của hôm nay.

Cách làmChuẩn bịMỗi truy vấnBộ nhớDùng khi
Leo từng bước$O(n)$$O(\text{độ sâu})$, xấu nhất $O(n)$$O(n)$ít truy vấn, hoặc cây được đảm bảo thấp
Nhảy nhị phân$O(n \log n)$$O(\log n)$$O(n \log n)$mặc định khi đi thi; làm được cả tổ tiên thứ k, max cạnh
Euler tour + Segment Tree$O(n)$$O(\log n)$$O(n)$khi bộ nhớ chật
Euler tour + Sparse Table$O(n \log n)$$O(1)$$O(n \log n)$số truy vấn rất lớn ($10^6$ trở lên)

Bảng 6. So sánh các cách tìm LCA.

Nên chọn cách nào khi đi thi?
Hầu hết bài LCA trong đề HSG tỉnh và quốc gia giải thoải mái bằng nhảy nhị phân: code ngắn, ít lỗi, lại dùng chung được cho tổ tiên thứ $k$ và thông tin trên đường đi. Mảng tin/tout thì nên tính luôn, vì nó còn biến cây con thành một đoạn liên tiếp, nhờ đó mọi truy vấn “cộng/tính tổng trên cây con” quy về bài toán trên mảng mà Fenwick, Segment Tree xử lý được.
Luyện tập trên web
Các bài dưới đây trên blog đều có lời giải chi tiết bằng C++ và Python, xếp từ dễ tới khó. Em hãy tự làm trước rồi mới xem lời giải nhé.Xem toàn bộ bài cùng chủ đề: nhãn Cây.

Bài tậpLuyện LCA và nhảy nhị phân

Bài 1Tổ tiên thứ kDễ

Công ty có $n$ nhân viên ($1 \le n \le 2 \cdot 10^5$), nhân viên 1 là giám đốc. Với $i \ge 2$, sếp trực tiếp của nhân viên $i$ là $p_i$ (đảm bảo tạo thành một cây gốc 1). Có $q$ câu hỏi ($q \le 2 \cdot 10^5$), mỗi câu cho $u$ và $k$ ($0 \le k \le n$): đi lên $k$ cấp từ $u$ thì gặp ai? In $-1$ nếu không tồn tại.

Input: dòng đầu $n, q$; dòng hai $p_2, p_3, \ldots, p_n$; $q$ dòng sau mỗi dòng $u, k$.

Input
12 4
1 1 2 2 3 4 4 5 6 7 11
12 5
12 3
9 4
10 0
Output
1
4
-1
10

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Nhảy theo từng bit của $k$ như phần 4. Bài này dùng một quy ước khác cho gốc: up[0][1] = 0, và đỉnh 0 có up[k][0] = 0, nghĩa là “đã rơi ra ngoài cây”. Nhờ vậy không cần mảng độ sâu: nhảy xong mà đứng ở 0 thì in $-1$. Thêm nữa, hàng up[k] chỉ cần hàng up[k-1] nên không cần BFS, cha cho theo thứ tự nào cũng được. Ví dụ: 12 lên 5 cấp: 11, 7, 4, 2, 1. Đỉnh 9 chỉ có 3 cấp phía trên nên lên 4 cấp là không tồn tại.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005, LOG = 18;
int up[LOG][MAXN];                      // up[k][v] = 0 nghĩa là "không tồn tại"

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    int n, q;
    cin >> n >> q;
    up[0][1] = 0;                       // gốc không có cha
    for(int i = 2; i <= n; i++) cin >> up[0][i];
    for(int k = 1; k < LOG; k++)
        for(int v = 1; v <= n; v++)
            up[k][v] = up[k - 1][up[k - 1][v]];   // up[k][0] luôn bằng 0
    while(q--){
        int u, k;
        cin >> u >> k;
        for(int b = 0; b < LOG && u != 0; b++)
            if(k >> b & 1) u = up[b][u];
        cout << (u == 0 ? -1 : u) << "\n";
    }
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    par = [0, 0] + [int(x) for x in data[2:n + 1]]   # par[1] = 0: gốc không có cha
    LOG = max(1, n.bit_length())
    up = [par]
    for k in range(1, LOG):
        prev = up[-1]
        up.append([prev[prev[v]] for v in range(n + 1)])   # prev[0] = 0
    p = n + 1
    out = []
    for _ in range(q):
        u, k = int(data[p]), int(data[p + 1]); p += 2
        b = 0
        while k and u:
            if k & 1:
                u = up[b][u]
            k >>= 1; b += 1
        out.append(u if u else -1)
    print('\n'.join(map(str, out)))

main()
Bài 2Khoảng cách hai đỉnhDễ

Cho cây $n$ đỉnh ($n, q \le 2 \cdot 10^5$) và $q$ truy vấn, mỗi truy vấn hỏi đường đi từ $u$ tới $v$ gồm bao nhiêu cạnh.

Input: dòng đầu $n, q$; $n - 1$ dòng tiếp mỗi dòng một cạnh $u, v$; $q$ dòng sau mỗi dòng $u, v$.

Input
12 4
1 2
1 3
2 4
2 5
3 6
4 7
4 8
5 9
6 10
7 11
11 12
12 9
10 12
8 8
7 11
Output
6
8
0
1

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Chọn gốc 1, tìm $l = $ LCA(u, v), đáp án $\text{dep}[u] + \text{dep}[v] - 2 \cdot \text{dep}[l]$. Ví dụ (10, 12): LCA là gốc 1 nên khoảng cách $3 + 5 - 0 = 8$. Hai đỉnh trùng nhau thì khoảng cách 0.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005, LOG = 18;
int n, q, up[LOG][MAXN], dep[MAXN];
vector<int> adj[MAXN];

int lca(int u, int v){
    if(dep[u] < dep[v]) swap(u, v);
    int d = dep[u] - dep[v];
    for(int k = 0; k < LOG; k++)
        if(d >> k & 1) u = up[k][u];
    if(u == v) return u;
    for(int k = LOG - 1; k >= 0; k--)
        if(up[k][u] != up[k][v]){ u = up[k][u]; v = up[k][v]; }
    return up[0][u];
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    for(int i = 1; i < n; i++){
        int u, v; cin >> u >> v;
        adj[u].push_back(v); adj[v].push_back(u);
    }
    vector<int> order = {1};
    vector<bool> seen(n + 1, false);
    seen[1] = true; up[0][1] = 1;
    for(int i = 0; i < (int)order.size(); i++){
        int u = order[i];
        for(int v : adj[u]) if(!seen[v]){
            seen[v] = true; up[0][v] = u; dep[v] = dep[u] + 1;
            order.push_back(v);
        }
    }
    for(int k = 1; k < LOG; k++)
        for(int v = 1; v <= n; v++)
            up[k][v] = up[k - 1][up[k - 1][v]];

    while(q--){
        int u, v; cin >> u >> v;
        cout << dep[u] + dep[v] - 2 * dep[lca(u, v)] << "\n";
    }
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    adj = [[] for _ in range(n + 1)]
    p = 2
    for _ in range(n - 1):
        u, v = int(data[p]), int(data[p + 1]); p += 2
        adj[u].append(v); adj[v].append(u)
    LOG = max(1, n.bit_length())
    par = [0] * (n + 1); dep = [0] * (n + 1); par[1] = 1
    seen = [False] * (n + 1); seen[1] = True
    order = [1]
    for u in order:
        for v in adj[u]:
            if not seen[v]:
                seen[v] = True; par[v] = u; dep[v] = dep[u] + 1
                order.append(v)
    up = [par]
    for k in range(1, LOG):
        prev = up[-1]
        up.append([prev[prev[v]] for v in range(n + 1)])

    def lca(u, v):
        if dep[u] < dep[v]:
            u, v = v, u
        d, k = dep[u] - dep[v], 0
        while d:
            if d & 1:
                u = up[k][u]
            d >>= 1; k += 1
        if u == v:
            return u
        for k in range(LOG - 1, -1, -1):
            if up[k][u] != up[k][v]:
                u = up[k][u]; v = up[k][v]
        return up[0][u]

    out = []
    for _ in range(q):
        u, v = int(data[p]), int(data[p + 1]); p += 2
        out.append(dep[u] + dep[v] - 2 * dep[lca(u, v)])
    print('\n'.join(map(str, out)))

main()
Bài 3Có phải tổ tiên?Trung bình

Cho cây $n$ đỉnh gốc 1 ($n, q \le 2 \cdot 10^5$). Mỗi truy vấn cho $u, v$: hỏi $u$ có phải tổ tiên của $v$ không (một đỉnh được coi là tổ tiên của chính nó). In YES hoặc NO.

Input: dòng đầu $n, q$; $n - 1$ dòng tiếp là các cạnh; $q$ dòng sau mỗi dòng $u, v$.

Input
12 5
1 2
1 3
2 4
2 5
3 6
4 7
4 8
5 9
6 10
7 11
11 12
1 12
4 12
12 4
3 9
5 5
Output
YES
YES
NO
NO
YES

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Không cần bảng nhảy nhị phân! Chỉ cần một lần DFS tính tin, tout như phần 5, rồi kiểm tra $\text{tin}[u] \le \text{tin}[v] \le \text{tout}[u]$ trong $O(1)$. Ở Hình 2: đoạn của 4 là [3, 7], tin[12] = 6 nằm trong nên YES; đoạn của 12 là [6, 6], tin[4] = 3 nằm ngoài nên NO. Nhớ viết DFS không đệ quy.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005;
int n, q, par[MAXN], tin[MAXN], tout[MAXN];
vector<int> adj[MAXN];

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    for(int i = 1; i < n; i++){
        int u, v; cin >> u >> v;
        adj[u].push_back(v); adj[v].push_back(u);
    }
    int timer = 0;
    vector<int> st = {1}, it(n + 1, 0);
    par[1] = 0; tin[1] = ++timer;
    while(!st.empty()){
        int u = st.back();
        if(it[u] < (int)adj[u].size()){
            int v = adj[u][it[u]++];
            if(v == par[u]) continue;
            par[v] = u; tin[v] = ++timer;
            st.push_back(v);
        } else {
            tout[u] = timer;
            st.pop_back();
        }
    }
    while(q--){
        int u, v; cin >> u >> v;
        bool ok = tin[u] <= tin[v] && tin[v] <= tout[u];
        cout << (ok ? "YES" : "NO") << "\n";
    }
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    adj = [[] for _ in range(n + 1)]
    p = 2
    for _ in range(n - 1):
        u, v = int(data[p]), int(data[p + 1]); p += 2
        adj[u].append(v); adj[v].append(u)
    par = [0] * (n + 1); tin = [0] * (n + 1); tout = [0] * (n + 1)
    timer = 1; tin[1] = 1
    st, it = [1], [0] * (n + 1)
    while st:
        u = st[-1]
        if it[u] < len(adj[u]):
            v = adj[u][it[u]]; it[u] += 1
            if v == par[u]:
                continue
            par[v] = u; timer += 1; tin[v] = timer
            st.append(v)
        else:
            tout[u] = timer
            st.pop()
    out = []
    for _ in range(q):
        u, v = int(data[p]), int(data[p + 1]); p += 2
        out.append('YES' if tin[u] <= tin[v] <= tout[u] else 'NO')
    print('\n'.join(out))

main()
Bài 4Đường đi có trọng sốTrung bình

Có $n$ thành phố nối với nhau bởi $n - 1$ con đường tạo thành một cây ($n, q \le 2 \cdot 10^5$), con đường thứ $i$ dài $w_i$ ($1 \le w_i \le 10^9$). Mỗi truy vấn hỏi độ dài đường đi giữa hai thành phố $u$ và $v$.

Input: dòng đầu $n, q$; $n - 1$ dòng tiếp mỗi dòng $u, v, w$; $q$ dòng sau mỗi dòng $u, v$.

Input
6 3
1 2 3
1 3 5
2 4 2
2 5 7
3 6 1
4 5
4 6
6 6
Output
9
11
0

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Trong lúc BFS từ gốc, tính $D[v] = D[\text{cha}] + w$ là tổng độ dài từ gốc tới $v$. Đáp án $D[u] + D[v] - 2D[l]$ với $l = $ LCA(u, v). Đường đi dài tới $2 \cdot 10^5 \cdot 10^9$ nên $D$ phải là long long. Ví dụ (4, 6): LCA là 1, $D[4] = 5$, $D[6] = 6$, đáp án $5 + 6 = 11$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005, LOG = 18;
int n, q, up[LOG][MAXN], dep[MAXN];
long long D[MAXN];                               // tổng trọng số từ gốc tới v
vector<pair<int, int>> adj[MAXN];

int lca(int u, int v){
    if(dep[u] < dep[v]) swap(u, v);
    int d = dep[u] - dep[v];
    for(int k = 0; k < LOG; k++)
        if(d >> k & 1) u = up[k][u];
    if(u == v) return u;
    for(int k = LOG - 1; k >= 0; k--)
        if(up[k][u] != up[k][v]){ u = up[k][u]; v = up[k][v]; }
    return up[0][u];
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    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> order = {1};
    vector<bool> seen(n + 1, false);
    seen[1] = true; up[0][1] = 1;
    for(int i = 0; i < (int)order.size(); i++){
        int u = order[i];
        for(auto [v, w] : adj[u]) if(!seen[v]){
            seen[v] = true; up[0][v] = u; dep[v] = dep[u] + 1; D[v] = D[u] + w;
            order.push_back(v);
        }
    }
    for(int k = 1; k < LOG; k++)
        for(int v = 1; v <= n; v++)
            up[k][v] = up[k - 1][up[k - 1][v]];
    while(q--){
        int u, v; cin >> u >> v;
        cout << D[u] + D[v] - 2 * D[lca(u, v)] << "\n";
    }
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    adj = [[] for _ in range(n + 1)]
    p = 2
    for _ in range(n - 1):
        u, v, w = int(data[p]), int(data[p + 1]), int(data[p + 2]); p += 3
        adj[u].append((v, w)); adj[v].append((u, w))
    LOG = max(1, n.bit_length())
    par = [0] * (n + 1); dep = [0] * (n + 1); D = [0] * (n + 1); par[1] = 1
    seen = [False] * (n + 1); seen[1] = True
    order = [1]
    for u in order:
        for v, w in adj[u]:
            if not seen[v]:
                seen[v] = True; par[v] = u; dep[v] = dep[u] + 1; D[v] = D[u] + w
                order.append(v)
    up = [par]
    for k in range(1, LOG):
        prev = up[-1]
        up.append([prev[prev[v]] for v in range(n + 1)])

    def lca(u, v):
        if dep[u] < dep[v]:
            u, v = v, u
        d, k = dep[u] - dep[v], 0
        while d:
            if d & 1:
                u = up[k][u]
            d >>= 1; k += 1
        if u == v:
            return u
        for k in range(LOG - 1, -1, -1):
            if up[k][u] != up[k][v]:
                u = up[k][u]; v = up[k][v]
        return up[0][u]

    out = []
    for _ in range(q):
        u, v = int(data[p]), int(data[p + 1]); p += 2
        out.append(D[u] + D[v] - 2 * D[lca(u, v)])
    print('\n'.join(map(str, out)))

main()
Bài 5Đỉnh thứ k trên đường điTrung bình

Cho cây $n$ đỉnh ($n, q \le 2 \cdot 10^5$). Mỗi truy vấn cho $u, v, k$ ($0 \le k \le n$): liệt kê các đỉnh trên đường đi từ $u$ tới $v$ theo thứ tự, đánh số từ 0 (đỉnh số 0 là $u$). In ra đỉnh số $k$, hoặc $-1$ nếu đường đi có ít hơn $k + 1$ đỉnh.

Input: dòng đầu $n, q$; $n - 1$ dòng tiếp là các cạnh; $q$ dòng sau mỗi dòng $u, v, k$.

Input
12 6
1 2
1 3
2 4
2 5
3 6
4 7
4 8
5 9
6 10
7 11
11 12
12 9 0
12 9 3
12 9 5
12 9 6
12 9 7
9 12 2
Output
12
4
5
9
-1
2

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Gọi $l = $ LCA(u, v), $a = \text{dep}[u] - \text{dep}[l]$ (số cạnh đi lên), $\text{dist} = a + \text{dep}[v] - \text{dep}[l]$. Nếu $k > \text{dist}$: in $-1$. Nếu $k \le a$: đỉnh cần tìm nằm trên đoạn đi lên, là tổ tiên thứ $k$ của $u$. Ngược lại nó nằm trên đoạn đi xuống, và tính từ phía $v$ thì là tổ tiên thứ $\text{dist} - k$ của $v$. Ví dụ đường 12 → 11 → 7 → 4 → 2 → 5 → 9 có $a = 4$, dist = 6; $k = 5 > 4$ nên lấy tổ tiên thứ $6 - 5 = 1$ của 9, là 5.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005, LOG = 18;
int n, q, up[LOG][MAXN], dep[MAXN];
vector<int> adj[MAXN];

int lca(int u, int v){
    if(dep[u] < dep[v]) swap(u, v);
    int d = dep[u] - dep[v];
    for(int k = 0; k < LOG; k++)
        if(d >> k & 1) u = up[k][u];
    if(u == v) return u;
    for(int k = LOG - 1; k >= 0; k--)
        if(up[k][u] != up[k][v]){ u = up[k][u]; v = up[k][v]; }
    return up[0][u];
}

int kth(int u, int k){                       // tổ tiên thứ k của u (k <= dep[u])
    for(int b = 0; b < LOG; b++)
        if(k >> b & 1) u = up[b][u];
    return u;
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    for(int i = 1; i < n; i++){
        int u, v; cin >> u >> v;
        adj[u].push_back(v); adj[v].push_back(u);
    }
    vector<int> order = {1};
    vector<bool> seen(n + 1, false);
    seen[1] = true; up[0][1] = 1;
    for(int i = 0; i < (int)order.size(); i++){
        int u = order[i];
        for(int v : adj[u]) if(!seen[v]){
            seen[v] = true; up[0][v] = u; dep[v] = dep[u] + 1;
            order.push_back(v);
        }
    }
    for(int k = 1; k < LOG; k++)
        for(int v = 1; v <= n; v++)
            up[k][v] = up[k - 1][up[k - 1][v]];

    while(q--){
        int u, v, k; cin >> u >> v >> k;
        int l = lca(u, v);
        int du = dep[u] - dep[l], dist = du + dep[v] - dep[l];
        if(k > dist) cout << -1 << "\n";
        else if(k <= du) cout << kth(u, k) << "\n";          // đỉnh nằm trên đoạn u -> l
        else cout << kth(v, dist - k) << "\n";               // đỉnh nằm trên đoạn l -> v
    }
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    adj = [[] for _ in range(n + 1)]
    p = 2
    for _ in range(n - 1):
        u, v = int(data[p]), int(data[p + 1]); p += 2
        adj[u].append(v); adj[v].append(u)
    LOG = max(1, n.bit_length())
    par = [0] * (n + 1); dep = [0] * (n + 1); par[1] = 1
    seen = [False] * (n + 1); seen[1] = True
    order = [1]
    for u in order:
        for v in adj[u]:
            if not seen[v]:
                seen[v] = True; par[v] = u; dep[v] = dep[u] + 1
                order.append(v)
    up = [par]
    for k in range(1, LOG):
        prev = up[-1]
        up.append([prev[prev[v]] for v in range(n + 1)])

    def lca(u, v):
        if dep[u] < dep[v]:
            u, v = v, u
        d, k = dep[u] - dep[v], 0
        while d:
            if d & 1:
                u = up[k][u]
            d >>= 1; k += 1
        if u == v:
            return u
        for k in range(LOG - 1, -1, -1):
            if up[k][u] != up[k][v]:
                u = up[k][u]; v = up[k][v]
        return up[0][u]

    def kth(u, k):                           # tổ tiên thứ k của u (k <= dep[u])
        b = 0
        while k:
            if k & 1:
                u = up[b][u]
            k >>= 1; b += 1
        return u

    out = []
    for _ in range(q):
        u, v, k = int(data[p]), int(data[p + 1]), int(data[p + 2]); p += 3
        l = lca(u, v)
        du = dep[u] - dep[l]
        dist = du + dep[v] - dep[l]
        if k > dist:
            out.append(-1)
        elif k <= du:
            out.append(kth(u, k))            # trên đoạn u -> l
        else:
            out.append(kth(v, dist - k))     # trên đoạn l -> v
    print('\n'.join(map(str, out)))

main()
Bài 6Đường đi an toàn nhấtKhá

Mạng giao thông có $n$ thành phố và $m$ con đường hai chiều ($n \le 10^5$, $n - 1 \le m \le 2 \cdot 10^5$), đồ thị liên thông, con đường $i$ có độ nguy hiểm $w_i$ ($1 \le w_i \le 10^9$). Độ nguy hiểm của một lộ trình là độ nguy hiểm lớn nhất của các con đường trên lộ trình. Có $q$ câu hỏi ($q \le 2 \cdot 10^5$): đi từ $u$ tới $v$ thì độ nguy hiểm nhỏ nhất có thể là bao nhiêu? (In 0 nếu $u = v$.)

Input: dòng đầu $n, m, q$; $m$ dòng tiếp mỗi dòng $u, v, w$; $q$ dòng sau mỗi dòng $u, v$.

Input
5 7 5
1 2 4
1 3 1
2 3 2
2 4 7
3 4 5
4 5 3
3 5 8
1 2
1 5
2 4
4 5
3 3
Output
2
5
5
3
0

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Một tính chất đẹp của cây khung nhỏ nhất (bài 30): đường đi giữa $u$ và $v$ trên cây khung nhỏ nhất chính là lộ trình có cạnh lớn nhất nhỏ nhất. (Nếu có lộ trình tốt hơn, ta đổi được một cạnh của cây khung lấy cạnh nhẹ hơn, mâu thuẫn với “nhỏ nhất”.) Vậy: chạy Kruskal lấy cây khung, rồi trả lời mỗi câu hỏi bằng bảng mx[k][u] như phần 4. Ví dụ: cây khung gồm 1–3 (1), 2–3 (2), 4–5 (3), 3–4 (5); đường từ 1 tới 5 là 1 – 3 – 4 – 5, cạnh lớn nhất là 5.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005, LOG = 18;
int n, m, q, up[LOG][MAXN], mx[LOG][MAXN], dep[MAXN], dsu[MAXN];
vector<pair<int, int>> adj[MAXN];

int find(int x){ return dsu[x] == x ? x : dsu[x] = find(dsu[x]); }

int query(int u, int v){                         // cạnh lớn nhất trên đường đi u - v trong cây
    int best = 0;
    if(dep[u] < dep[v]) swap(u, v);
    int d = dep[u] - dep[v];
    for(int k = 0; k < LOG; k++)
        if(d >> k & 1){ best = max(best, mx[k][u]); u = up[k][u]; }
    if(u == v) return best;
    for(int k = LOG - 1; k >= 0; k--)
        if(up[k][u] != up[k][v]){
            best = max({best, mx[k][u], mx[k][v]});
            u = up[k][u]; v = up[k][v];
        }
    return max({best, mx[0][u], mx[0][v]});
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> m >> q;
    vector<array<int, 3>> E(m);
    for(auto &e : E) cin >> e[1] >> e[2] >> e[0];          // (w, u, v)
    sort(E.begin(), E.end());
    for(int i = 1; i <= n; i++) dsu[i] = i;
    for(auto &e : E){                                      // Kruskal: cây khung nhỏ nhất
        int a = find(e[1]), b = find(e[2]);
        if(a == b) continue;
        dsu[a] = b;
        adj[e[1]].push_back({e[2], e[0]}); adj[e[2]].push_back({e[1], e[0]});
    }
    vector<int> order = {1};
    vector<bool> seen(n + 1, false);
    seen[1] = true; up[0][1] = 1;
    for(int i = 0; i < (int)order.size(); i++){
        int u = order[i];
        for(auto [v, w] : adj[u]) if(!seen[v]){
            seen[v] = true; up[0][v] = u; mx[0][v] = w; dep[v] = dep[u] + 1;
            order.push_back(v);
        }
    }
    for(int k = 1; k < LOG; k++)
        for(int v = 1; v <= n; v++){
            int mid = up[k - 1][v];
            up[k][v] = up[k - 1][mid];
            mx[k][v] = max(mx[k - 1][v], mx[k - 1][mid]);
        }
    while(q--){
        int u, v; cin >> u >> v;
        cout << query(u, v) << "\n";
    }
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n, m, q = int(data[0]), int(data[1]), int(data[2])
    p = 3
    E = []
    for _ in range(m):
        u, v, w = int(data[p]), int(data[p + 1]), int(data[p + 2]); p += 3
        E.append((w, u, v))
    E.sort()
    dsu = list(range(n + 1))

    def find(x):
        while dsu[x] != x:
            dsu[x] = dsu[dsu[x]]
            x = dsu[x]
        return x

    adj = [[] for _ in range(n + 1)]
    for w, u, v in E:                        # Kruskal: cây khung nhỏ nhất
        a, b = find(u), find(v)
        if a != b:
            dsu[a] = b
            adj[u].append((v, w)); adj[v].append((u, w))
    LOG = max(1, n.bit_length())
    par = [0] * (n + 1); m0 = [0] * (n + 1); dep = [0] * (n + 1); par[1] = 1
    seen = [False] * (n + 1); seen[1] = True
    order = [1]
    for u in order:
        for v, w in adj[u]:
            if not seen[v]:
                seen[v] = True; par[v] = u; m0[v] = w; dep[v] = dep[u] + 1
                order.append(v)
    up, mx = [par], [m0]
    for k in range(1, LOG):
        pu, pm = up[-1], mx[-1]
        up.append([pu[pu[v]] for v in range(n + 1)])
        mx.append([max(pm[v], pm[pu[v]]) for v in range(n + 1)])

    def query(u, v):
        best = 0
        if dep[u] < dep[v]:
            u, v = v, u
        d, k = dep[u] - dep[v], 0
        while d:
            if d & 1:
                best = max(best, mx[k][u]); u = up[k][u]
            d >>= 1; k += 1
        if u == v:
            return best
        for k in range(LOG - 1, -1, -1):
            if up[k][u] != up[k][v]:
                best = max(best, mx[k][u], mx[k][v])
                u = up[k][u]; v = up[k][v]
        return max(best, mx[0][u], mx[0][v])

    out = []
    for _ in range(q):
        u, v = int(data[p]), int(data[p + 1]); p += 2
        out.append(query(u, v))
    print('\n'.join(map(str, out)))

main()
Bài 7Điểm hẹn của ba ngườiKhá

Ba bạn đang ở ba đỉnh $a, b, c$ của một cây $n$ đỉnh ($n, q \le 2 \cdot 10^5$, mỗi cạnh dài 1). Họ muốn chọn một đỉnh $x$ để gặp nhau sao cho tổng quãng đường ba người đi là nhỏ nhất. Với mỗi truy vấn $a, b, c$, in ra đỉnh $x$ (đảm bảo duy nhất) và tổng quãng đường nhỏ nhất.

Input: dòng đầu $n, q$; $n - 1$ dòng tiếp là các cạnh; $q$ dòng sau mỗi dòng $a, b, c$.

Input
12 3
1 2
1 3
2 4
2 5
3 6
4 7
4 8
5 9
6 10
7 11
11 12
12 8 9
10 9 3
5 5 5
Output
4 7
3 6
5 0

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Tính ba LCA: LCA(a, b), LCA(b, c), LCA(a, c). Luôn có ít nhất hai giá trị trùng nhau, và điểm hẹn tốt nhất là LCA sâu nhất trong ba (chỗ ba đường đi “giao nhau”). Tổng quãng đường bằng $\frac{\text{dist}(a, b) + \text{dist}(b, c) + \text{dist}(a, c)}{2}$, vì mỗi cạnh trên “hình chữ Y” nối ba đỉnh được đếm đúng hai lần trong tổng ba khoảng cách. Ví dụ (12, 8, 9): ba LCA là 4, 2, 2, sâu nhất là 4; tổng $(4 + 4 + 6) / 2 = 7$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005, LOG = 18;
int n, q, up[LOG][MAXN], dep[MAXN];
vector<int> adj[MAXN];

int lca(int u, int v){
    if(dep[u] < dep[v]) swap(u, v);
    int d = dep[u] - dep[v];
    for(int k = 0; k < LOG; k++)
        if(d >> k & 1) u = up[k][u];
    if(u == v) return u;
    for(int k = LOG - 1; k >= 0; k--)
        if(up[k][u] != up[k][v]){ u = up[k][u]; v = up[k][v]; }
    return up[0][u];
}

int dist(int u, int v){ return dep[u] + dep[v] - 2 * dep[lca(u, v)]; }

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    for(int i = 1; i < n; i++){
        int u, v; cin >> u >> v;
        adj[u].push_back(v); adj[v].push_back(u);
    }
    vector<int> order = {1};
    vector<bool> seen(n + 1, false);
    seen[1] = true; up[0][1] = 1;
    for(int i = 0; i < (int)order.size(); i++){
        int u = order[i];
        for(int v : adj[u]) if(!seen[v]){
            seen[v] = true; up[0][v] = u; dep[v] = dep[u] + 1;
            order.push_back(v);
        }
    }
    for(int k = 1; k < LOG; k++)
        for(int v = 1; v <= n; v++)
            up[k][v] = up[k - 1][up[k - 1][v]];

    while(q--){
        int a, b, c; cin >> a >> b >> c;
        int x = lca(a, b), y = lca(b, c), z = lca(a, c);
        int best = x;                                   // điểm hẹn = LCA sâu nhất trong ba cặp
        if(dep[y] > dep[best]) best = y;
        if(dep[z] > dep[best]) best = z;
        long long s = (long long)dist(a, b) + dist(b, c) + dist(a, c);
        cout << best << " " << s / 2 << "\n";
    }
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    adj = [[] for _ in range(n + 1)]
    p = 2
    for _ in range(n - 1):
        u, v = int(data[p]), int(data[p + 1]); p += 2
        adj[u].append(v); adj[v].append(u)
    LOG = max(1, n.bit_length())
    par = [0] * (n + 1); dep = [0] * (n + 1); par[1] = 1
    seen = [False] * (n + 1); seen[1] = True
    order = [1]
    for u in order:
        for v in adj[u]:
            if not seen[v]:
                seen[v] = True; par[v] = u; dep[v] = dep[u] + 1
                order.append(v)
    up = [par]
    for k in range(1, LOG):
        prev = up[-1]
        up.append([prev[prev[v]] for v in range(n + 1)])

    def lca(u, v):
        if dep[u] < dep[v]:
            u, v = v, u
        d, k = dep[u] - dep[v], 0
        while d:
            if d & 1:
                u = up[k][u]
            d >>= 1; k += 1
        if u == v:
            return u
        for k in range(LOG - 1, -1, -1):
            if up[k][u] != up[k][v]:
                u = up[k][u]; v = up[k][v]
        return up[0][u]

    def dist(u, v):
        return dep[u] + dep[v] - 2 * dep[lca(u, v)]

    out = []
    for _ in range(q):
        a, b, c = int(data[p]), int(data[p + 1]), int(data[p + 2]); p += 3
        best = max((lca(a, b), lca(b, c), lca(a, c)), key=lambda x: dep[x])   # LCA sâu nhất
        s = dist(a, b) + dist(b, c) + dist(a, c)
        out.append('%d %d' % (best, s // 2))
    print('\n'.join(out))

main()
Bài 8Đếm đường đi qua mỗi đỉnhKhó

Cho cây $n$ đỉnh ($n \le 2 \cdot 10^5$) và $m$ đường đi ($m \le 2 \cdot 10^5$), đường đi thứ $i$ nối $u_i$ với $v_i$ (có thể $u_i = v_i$). Với mỗi đỉnh, đếm xem có bao nhiêu đường đi đi qua nó (tính cả hai đầu mút).

Input: dòng đầu $n, m$; $n - 1$ dòng tiếp là các cạnh; $m$ dòng sau mỗi dòng $u_i, v_i$. Output: $n$ số trên một dòng.

Input
12 3
1 2
1 3
2 4
2 5
3 6
4 7
4 8
5 9
6 10
7 11
11 12
12 9
10 8
6 6
Output
1 2 1 2 1 2 1 1 1 1 1 1

📝 Nộp bài, chấm tự động trên ClueOJ ↗

💡 Gợi ý
Đây là mảng hiệu trên cây, phiên bản cây của mảng hiệu ở bài 13. Quy ước: đáp án của đỉnh $x$ = tổng giá trị cnt trong cây con của $x$. Với đường đi $u$ – $v$ có $l = $ LCA(u, v): tăng cnt[u] và cnt[v] thêm 1, giảm cnt[l] đi 1 và giảm cnt[cha(l)] đi 1 (nếu $l$ không phải gốc). Thử kiểm tra: mỗi đỉnh trên đường đi nhận đúng +1, các đỉnh khác nhận 0. Cuối cùng cộng dồn từ lá lên gốc: duyệt các đỉnh theo thứ tự BFS ngược, cộng giá trị của con vào cha. Tổng $O((n + m) \log n)$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005, LOG = 18;
int n, q, up[LOG][MAXN], dep[MAXN];
vector<int> adj[MAXN];
long long cnt[MAXN];

int lca(int u, int v){
    if(dep[u] < dep[v]) swap(u, v);
    int d = dep[u] - dep[v];
    for(int k = 0; k < LOG; k++)
        if(d >> k & 1) u = up[k][u];
    if(u == v) return u;
    for(int k = LOG - 1; k >= 0; k--)
        if(up[k][u] != up[k][v]){ u = up[k][u]; v = up[k][v]; }
    return up[0][u];
}

int main(){
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> q;
    for(int i = 1; i < n; i++){
        int u, v; cin >> u >> v;
        adj[u].push_back(v); adj[v].push_back(u);
    }
    vector<int> order = {1};
    vector<bool> seen(n + 1, false);
    seen[1] = true; up[0][1] = 1;
    for(int i = 0; i < (int)order.size(); i++){
        int u = order[i];
        for(int v : adj[u]) if(!seen[v]){
            seen[v] = true; up[0][v] = u; dep[v] = dep[u] + 1;
            order.push_back(v);
        }
    }
    for(int k = 1; k < LOG; k++)
        for(int v = 1; v <= n; v++)
            up[k][v] = up[k - 1][up[k - 1][v]];

    while(q--){                                         // q đường đi
        int u, v; cin >> u >> v;
        int l = lca(u, v);
        cnt[u]++; cnt[v]++; cnt[l]--;                   // đánh dấu hiệu trên cây
        if(l != 1) cnt[up[0][l]]--;
    }
    for(int i = n - 1; i > 0; i--)                      // cộng dồn từ lá lên (BFS ngược)
        cnt[up[0][order[i]]] += cnt[order[i]];
    for(int v = 1; v <= n; v++) cout << cnt[v] << " ";
    return 0;
}
import sys

def main():
    data = sys.stdin.buffer.read().split()
    n, q = int(data[0]), int(data[1])
    adj = [[] for _ in range(n + 1)]
    p = 2
    for _ in range(n - 1):
        u, v = int(data[p]), int(data[p + 1]); p += 2
        adj[u].append(v); adj[v].append(u)
    LOG = max(1, n.bit_length())
    par = [0] * (n + 1); dep = [0] * (n + 1); par[1] = 1
    seen = [False] * (n + 1); seen[1] = True
    order = [1]
    for u in order:
        for v in adj[u]:
            if not seen[v]:
                seen[v] = True; par[v] = u; dep[v] = dep[u] + 1
                order.append(v)
    up = [par]
    for k in range(1, LOG):
        prev = up[-1]
        up.append([prev[prev[v]] for v in range(n + 1)])

    def lca(u, v):
        if dep[u] < dep[v]:
            u, v = v, u
        d, k = dep[u] - dep[v], 0
        while d:
            if d & 1:
                u = up[k][u]
            d >>= 1; k += 1
        if u == v:
            return u
        for k in range(LOG - 1, -1, -1):
            if up[k][u] != up[k][v]:
                u = up[k][u]; v = up[k][v]
        return up[0][u]

    cnt = [0] * (n + 1)
    for _ in range(q):                       # q đường đi
        u, v = int(data[p]), int(data[p + 1]); p += 2
        l = lca(u, v)
        cnt[u] += 1; cnt[v] += 1; cnt[l] -= 1
        if l != 1:
            cnt[par[l]] -= 1
    for i in range(n - 1, 0, -1):            # cộng dồn từ lá lên (BFS ngược)
        x = order[i]
        cnt[par[x]] += cnt[x]
    print(*cnt[1:])

main()
Rất vui khi anh em nào đã đọc đến những dòng cuối cùng này. Anh em đọc chắc cũng mỏi mắt luôn chứ 😅 vậy anh em nghĩ người viết ra hết đống ở trên còn thế nào nữa 🤪. Vậy nên đừng tiếc gì cho mình một lời bình luận hoặc chia sẻ bài viết để động viên mình nè. Chúc anh em học tốt và gặt hái nhiều thành công 😇
Bản quyền thuộc về Thầy Nguyễn Việt Nam Sơn. Bài viết, lời giải, hình minh hoạ và mã nguồn trên trang là công sức biên soạn của thầy qua nhiều năm giảng dạy. Vui lòng không sao chép, đăng lại hay sử dụng dưới mọi hình thức khi chưa được sự đồng ý của tác giả. Nếu thấy hữu ích, hãy chia sẻ bằng đường link bài gốc nhé. Cảm ơn bạn!
Tự học thuật toán: khó ở đâu?
Đọc xong một bài giải là bước đầu. Phần lớn học sinh tự học sẽ gặp những trở ngại sau:
  • Không biết học gì trước, học gì sau, học lan man nên kiến thức bị hổng.
  • Bí bài không ai gỡ, ngồi cả buổi vẫn không ra rồi bỏ cuộc.
  • Code chạy được nhưng không đủ điểm, không biết sai ở đâu, chậm ở đâu.
  • Không có đề thi thử và người chấm, nên không biết mình đang ở mức nào.
  • Dễ nản giữa chừng khi không có ai theo sát.
Học cùng thầy Nam Sơn, các trở ngại đó được gỡ bỏ:
  • Lộ trình 111 buổi video sắp sẵn từ dòng code đầu tiên đến đề HSG thật của các tỉnh.
  • Thầy trực tiếp gỡ bí, chữa bài cho từng em qua nhóm Zalo riêng.
  • Hệ thống chấm bài tự động 24/7 và thi thử HSG hằng tuần để biết chính xác mình đang ở đâu.
  • Đóng học phí một lần, thầy kèm lâu dài: từ thi chuyên Tin đến HSG quốc gia và cả khi lên đại học.

Nhận xét