Chương 3·Bài 36

Lowest Common Ancestor

Trong một cây gia phả, hai người có chung tổ tiên nào? Người chung sâu nhất, gần hai người nhất, gọi là tổ tiên chung gần nhất (lowest common ancestor, LCA).

Đường đi giữa uu và vv trên cây đi lên từ uu tới LCA rồi đi xuống tới vv. Nhờ vậy mọi câu hỏi trên đường đi (độ dài, đỉnh thứ kk, cạnh nặng nhất, số đường đi qua một đỉnh) tách thành hai đoạn dọc, mỗi đoạn đi từ một đỉnh lên tổ tiên. Nhảy nhị phân (bài 35) tìm LCA trong O(log⁡n)O(\log n); thứ tự Euler cùng bảng thưa (bài 22) cho O(1)O(1).

Nâng hai đỉnh

Dựng bảng up[j][v] (tổ tiên cách 2j2^j bậc) và độ sâu dep[v]. Tìm LCA của aa và bb:

  1. Giả sử aa sâu hơn; nâng aa lên đúng depa−depbdep_a - dep_b bậc theo các bit của hiệu. Nếu lúc này a=ba = b thì đó là LCA.
  2. Ngược lại hai đỉnh cùng độ sâu nhưng khác nhau. Duyệt jj từ cao xuống: nếu up[j][a] != up[j][b] thì nhảy cả hai. Hai tổ tiên còn khác nhau nghĩa là vẫn ở dưới LCA, nhảy an toàn; bằng nhau thì có thể đã vượt quá LCA, không nhảy.
  3. Kết thúc, aa và bb là hai con khác nhau của LCA, đáp án là up[0][a].
Đỉnh cam là u, xanh dương là v; mỗi bước nâng theo một lũy thừa hai. Đỉnh xanh lá là LCA.
C++
ll lca(ll a, ll b) {
    if (dep[a] < dep[b]) swap(a, b);
    for (ll j = LG - 1; j >= 0; j--)
        if (dep[a] - (1LL << j) >= dep[b]) a = up[j][a];
    if (a == b) return a;
    for (ll j = LG - 1; j >= 0; j--)
        if (up[j][a] != up[j][b]) a = up[j][a], b = up[j][b];
    return up[0][a];
}

Hỏi nhanh

Ở bước hai, vì sao chỉ nhảy khi up[j][a] != up[j][b] chứ không nhảy khi chúng bằng nhau?

Ví dụTổ Tiên Chung Gần Nhất

Đề. Cây n≤2⋅105n \le 2 \cdot 10^5 đỉnh gốc 1 cho bằng cha pv<vp_v < v. q≤2⋅105q \le 2 \cdot 10^5 câu hỏi LCA.

Lời giải. LCA bằng bảng nhảy, hai pha: đưa hai đỉnh về cùng độ sâu, rồi nâng cả hai cùng lúc tới ngay dưới tổ tiên chung. Ở đây cha của gốc là chính gốc: nhảy quá gốc thì dừng lại ở gốc, không cần lính canh 0.

Bước 1: cha, độ sâu và bảng. Cha có chỉ số nhỏ hơn con nên một vòng v=2..nv = 2..n cho cả độ sâu lẫn tầng 0; các tầng sau dựng theo jj. LG = 18 vì 217<2⋅105≤2182^{17} < 2 \cdot 10^5 \le 2^{18}.

C++
up[0][1] = 1;
for (ll v = 2; v <= n; v++) {
    cin >> up[0][v];
    dep[v] = dep[up[0][v]] + 1;
}
for (ll j = 1; j < LG; j++)
    for (ll v = 1; v <= n; v++) up[j][v] = up[j - 1][up[j - 1][v]];

Bước 2: hàm LCA. Pha một nâng aa (sâu hơn) lên cùng độ sâu với bb, thử bước lớn trước, tức viết hiệu độ sâu dưới dạng nhị phân. Pha hai nâng cả hai cùng lúc, chỉ khi tổ tiên 2j2^j của chúng còn khác nhau. “Tổ tiên chung” đơn điệu theo số bước nhảy, nên thử bước lớn trước và chỉ nhảy khi chưa chung là chặt nhị phân trên số bước. Xong vòng, aa và bb là hai con khác nhau của LCA, nên đáp án là cha của chúng.

C++
ll lca(ll a, ll b) {
    if (dep[a] < dep[b]) swap(a, b);
    for (ll j = LG - 1; j >= 0; j--)
        if (dep[a] - (1LL << j) >= dep[b]) a = up[j][a];
    if (a == b) return a;
    for (ll j = LG - 1; j >= 0; j--)
        if (up[j][a] != up[j][b]) a = up[j][a], b = up[j][b];
    return up[0][a];
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 2e5 + 5, LG = 18;
ll n, q, dep[N], up[LG][N];

ll lca(ll a, ll b) {
    if (dep[a] < dep[b]) swap(a, b);
    for (ll j = LG - 1; j >= 0; j--)
        if (dep[a] - (1LL << j) >= dep[b]) a = up[j][a];
    if (a == b) return a;
    for (ll j = LG - 1; j >= 0; j--)
        if (up[j][a] != up[j][b]) a = up[j][a], b = up[j][b];
    return up[0][a];
}

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

    cin >> n >> q;
    up[0][1] = 1;
    for (ll v = 2; v <= n; v++) {
        cin >> up[0][v];
        dep[v] = dep[up[0][v]] + 1;
    }
    for (ll j = 1; j < LG; j++)
        for (ll v = 1; v <= n; v++) up[j][v] = up[j - 1][up[j - 1][v]];
    while (q--) {
        ll a, b;
        cin >> a >> b;
        cout << lca(a, b) << '\n';
    }
}
8 5
1 1 2 2 3 6 6
4 5
4 7
7 8
3 8
1 5

Độ phức tạp: dựng O(nlog⁡n)O(n \log n), mỗi câu hỏi O(log⁡n)O(\log n).

Khoảng cách, đỉnh thứ k

Với l=lca(u,v)l = \mathrm{lca}(u, v), số cạnh trên đường là depu+depv−2depldep_u + dep_v - 2 dep_l. Có trọng số thì thay độ sâu bằng tổng trọng số từ gốc wdvwd_v: wdu+wdv−2wdlwd_u + wd_v - 2 wd_l.

Đỉnh thứ kk (đếm từ 1, bắt đầu ở uu) nằm trên đoạn lên nếu k−1≤depu−deplk - 1 \le dep_u - dep_l, khi đó là tổ tiên cách uu đúng k−1k - 1 bậc. Ngược lại nó nằm trên đoạn xuống, là tổ tiên của vv cách vv đúng (độ dài đường −(k−1)- (k - 1)) bậc.

Ví dụKhoảng Cách Và Đỉnh Thứ K

Đề. Cây N≤105N \le 10^5 đỉnh có trọng số cạnh, M≤105M \le 10^5 câu hỏi: tổng trọng số trên đường (u,v)(u, v), hoặc đỉnh thứ kk trên đường từ uu tới vv.

Lời giải. Kèm wdvwd_v là tổng trọng số từ gốc tới vv; tổng trên đường (u,v)(u, v) là wdu+wdv−2 wdlwd_u + wd_v - 2\,wd_l với ll là LCA. Đỉnh thứ kk trên đường đi tách theo hai đoạn: đoạn đi lên từ uu tới ll, và đoạn đi xuống từ ll tới vv.

Bước 1: BFS kèm tổng trọng số. wd tính cùng lúc với dep.

C++
for (pair<ll, ll> e : g[u])
    if (e.first != up[0][u]) {
        up[0][e.first] = u;
        dep[e.first] = dep[u] + 1;
        wd[e.first] = wd[u] + e.second;
        ord.push_back(e.first);
    }

Bước 2: nhảy lên đúng kk bậc. kth(v, k) nhảy theo từng bit của kk; lca dùng nó cho pha nâng độ sâu.

C++
ll kth(ll v, ll k) {
    for (ll j = 0; j < LG; j++)
        if (k >> j & 1) v = up[j][v];
    return v;
}

Bước 3: đỉnh thứ kk. d1 là số cạnh từ uu lên LCA, len là độ dài cả đường. Đỉnh thứ kk cách uu đúng k−1k - 1 cạnh; nếu còn trên đoạn đi lên thì là tổ tiên của uu, ngược lại là tổ tiên của vv cách vv đúng len - (k - 1) cạnh.

C++
ll d1 = dep[u] - dep[l], len = d1 + dep[v] - dep[l];
if (k - 1 <= d1) cout << kth(u, k - 1) << '\n';
else cout << kth(v, len - (k - 1)) << '\n';

Tổng trọng số tới 105⋅10910^5 \cdot 10^9, dùng long long.

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

const ll N = 1e5 + 5, LG = 17;
ll n, m, dep[N], wd[N], up[LG][N];
vector<pair<ll, ll>> g[N];

ll kth(ll v, ll k) {
    for (ll j = 0; j < LG; j++)
        if (k >> j & 1) v = up[j][v];
    return v;
}

ll lca(ll a, ll b) {
    if (dep[a] < dep[b]) swap(a, b);
    a = kth(a, dep[a] - dep[b]);
    if (a == b) return a;
    for (ll j = LG - 1; j >= 0; j--)
        if (up[j][a] != up[j][b]) a = up[j][a], b = up[j][b];
    return up[0][a];
}

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

    cin >> n;
    for (ll i = 1; i < n; i++) {
        ll a, b, w;
        cin >> a >> b >> w;
        g[a].push_back({b, w});
        g[b].push_back({a, w});
    }
    vector<ll> ord = {1};
    up[0][1] = 1;
    for (ll i = 0; i < (ll)ord.size(); i++) {
        ll u = ord[i];
        for (pair<ll, ll> e : g[u])
            if (e.first != up[0][u]) {
                up[0][e.first] = u;
                dep[e.first] = dep[u] + 1;
                wd[e.first] = wd[u] + e.second;
                ord.push_back(e.first);
            }
    }
    for (ll j = 1; j < LG; j++)
        for (ll v = 1; v <= n; v++) up[j][v] = up[j - 1][up[j - 1][v]];
    cin >> m;
    while (m--) {
        ll t, u, v;
        cin >> t >> u >> v;
        ll l = lca(u, v);
        if (t == 1) cout << wd[u] + wd[v] - 2 * wd[l] << '\n';
        else {
            ll k;
            cin >> k;
            ll d1 = dep[u] - dep[l], len = d1 + dep[v] - dep[l];
            if (k - 1 <= d1) cout << kth(u, k - 1) << '\n';
            else cout << kth(v, len - (k - 1)) << '\n';
        }
    }
}
8
1 2 4
1 3 7
2 4 3
2 5 6
3 6 5
6 7 2
6 8 9
7
1 4 7
2 4 7 1
2 4 7 3
2 4 7 6
2 7 5 3
1 8 5
2 6 6 1

Độ phức tạp: mỗi câu hỏi O(log⁡N)O(\log N).

Gộp trên đường

Kèm vào bảng nhảy giá trị gộp của các cạnh trên đoạn nhảy: mn[j][v] là cạnh nhẹ nhất trên 2j2^j cạnh từ vv đi lên, mx[j][v] là cạnh nặng nhất. Khi nâng uu và vv trong lúc tìm LCA, mỗi bước nhảy gộp thêm giá trị của đoạn vừa nhảy. Đường đi gồm đúng các đoạn đã nhảy của cả hai bên cộng hai cạnh cuối tới LCA, nên kết quả gộp lại là đáp án. Phép gộp chỉ cần kết hợp: min, max, tổng, GCD đều được.

Ví dụCạnh Nhẹ Nhất Và Nặng Nhất

Đề. Cây N≤105N \le 10^5 đỉnh có trọng số cạnh, K≤105K \le 10^5 câu hỏi: cạnh nhẹ nhất và nặng nhất trên đường (u,v)(u, v).

Lời giải. Bảng nhảy kèm giá trị: mn[j][v], mx[j][v] là cạnh nhẹ nhất, nặng nhất trong 2j2^j cạnh từ vv đi lên. Câu hỏi đi đúng hai pha của LCA, mỗi lần nhảy gộp thêm giá trị của đoạn vừa nhảy. Các đoạn đã nhảy của hai bên cộng hai cạnh cuối nối aa, bb với LCA phủ đúng mọi cạnh trên đường, không trùng không sót.

Bước 1: tầng 0 khi BFS. Tầng 0 là trọng số cạnh vv với cha. Gốc đặt mn = INF, mx = -INF để các bước nhảy “quá gốc” (dừng ở gốc) không làm sai min, max.

C++
up[0][1] = 1;
mn[0][1] = INF;
mx[0][1] = -INF;
C++
ll v = e.first;
up[0][v] = u;
mn[0][v] = mx[0][v] = e.second;
dep[v] = dep[u] + 1;
ord.push_back(v);

Bước 2: ghép hai nửa. Tầng jj gộp hai nửa liền nhau, giống cách dựng up.

C++
ll p = up[j - 1][v];
up[j][v] = up[j - 1][p];
mn[j][v] = min(mn[j - 1][v], mn[j - 1][p]);
mx[j][v] = max(mx[j - 1][v], mx[j - 1][p]);

Bước 3: hai pha có gộp. Pha một nâng aa về độ sâu của bb, gộp từng đoạn nhảy. Nếu a=ba = b thì một đầu là tổ tiên của đầu kia, xong. Ngược lại pha hai nâng cả hai, gộp đoạn của cả hai bên, rồi gộp nốt hai cạnh cuối up[0] của aa và bb.

C++
if (a != b) {
    for (ll j = LG - 1; j >= 0; j--)
        if (up[j][a] != up[j][b]) {
            lo = min({lo, mn[j][a], mn[j][b]});
            hi = max({hi, mx[j][a], mx[j][b]});
            a = up[j][a];
            b = up[j][b];
        }
    lo = min({lo, mn[0][a], mn[0][b]});
    hi = max({hi, mx[0][a], mx[0][b]});
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 1e5 + 5, LG = 17, INF = 1e18;
ll n, k, dep[N], up[LG][N], mn[LG][N], mx[LG][N];
vector<pair<ll, ll>> g[N];

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

    cin >> n;
    for (ll i = 1; i < n; i++) {
        ll a, b, w;
        cin >> a >> b >> w;
        g[a].push_back({b, w});
        g[b].push_back({a, w});
    }
    vector<ll> ord = {1};
    up[0][1] = 1;
    mn[0][1] = INF;
    mx[0][1] = -INF;
    for (ll i = 0; i < (ll)ord.size(); i++) {
        ll u = ord[i];
        for (pair<ll, ll> e : g[u])
            if (e.first != up[0][u]) {
                ll v = e.first;
                up[0][v] = u;
                mn[0][v] = mx[0][v] = e.second;
                dep[v] = dep[u] + 1;
                ord.push_back(v);
            }
    }
    for (ll j = 1; j < LG; j++)
        for (ll v = 1; v <= n; v++) {
            ll p = up[j - 1][v];
            up[j][v] = up[j - 1][p];
            mn[j][v] = min(mn[j - 1][v], mn[j - 1][p]);
            mx[j][v] = max(mx[j - 1][v], mx[j - 1][p]);
        }
    cin >> k;
    while (k--) {
        ll a, b;
        cin >> a >> b;
        ll lo = INF, hi = -INF;
        if (dep[a] < dep[b]) swap(a, b);
        for (ll j = LG - 1; j >= 0; j--)
            if (dep[a] - (1LL << j) >= dep[b]) {
                lo = min(lo, mn[j][a]);
                hi = max(hi, mx[j][a]);
                a = up[j][a];
            }
        if (a != b) {
            for (ll j = LG - 1; j >= 0; j--)
                if (up[j][a] != up[j][b]) {
                    lo = min({lo, mn[j][a], mn[j][b]});
                    hi = max({hi, mx[j][a], mx[j][b]});
                    a = up[j][a];
                    b = up[j][b];
                }
            lo = min({lo, mn[0][a], mn[0][b]});
            hi = max({hi, mx[0][a], mx[0][b]});
        }
        cout << lo << ' ' << hi << '\n';
    }
}
7
1 2 8
1 3 3
2 4 6
2 5 10
3 6 4
6 7 12
4
4 5
4 7
3 7
1 5

Độ phức tạp: dựng O(Nlog⁡N)O(N \log N), mỗi câu hỏi O(log⁡N)O(\log N).

Mảng hiệu trên cây

Muốn cộng 1 cho mọi đỉnh trên đường (u,v)(u, v) rồi cuối cùng mới đọc kết quả, dùng mảng hiệu trên cây: cộng 1 vào uu và vv, trừ 1 ở l=lca(u,v)l = \mathrm{lca}(u, v) và trừ 1 ở cha của ll. Giá trị thật của một đỉnh là tổng các dấu trong cây con của nó. Một đỉnh xx trên đoạn từ uu lên ll có uu trong cây con nên nhận +1+1; ll nhận +1+1−1=1+1 + 1 - 1 = 1; cha của ll trở lên thì các dấu triệt tiêu hết. Cộng trên cạnh thay vì đỉnh thì trừ 2 ở ll và không đụng tới cha của ll, mỗi cạnh ứng với đỉnh con của nó.

Cam là hai đầu đường, xanh dương là LCA, đỏ là cha của LCA. Sau đó cộng dồn từ tầng sâu nhất lên.

Hỏi nhanh

Đánh dấu đường (u,v)(u, v) bằng +1+1 ở uu và vv, −1-1 ở l=lca(u,v)l = \mathrm{lca}(u, v). Còn thiếu dấu nào để mỗi đỉnh trên đường nhận đúng 1?

Ví dụDấu Chân Trên Các Đỉnh

Đề. Cây n≤2⋅105n \le 2 \cdot 10^5 đỉnh và m≤2⋅105m \le 2 \cdot 10^5 đường đi. Với mỗi đỉnh, đếm số đường đi chứa nó.

Lời giải. Cộng hiệu trên cây: mỗi đường (u,v)(u, v) đặt bốn dấu, +1+1 ở uu và vv, −1-1 ở LCA ll và ở cha của ll. Sau khi cộng dồn từ lá lên, giá trị ở mỗi đỉnh là tổng dấu trong cây con của nó, và bốn dấu trên làm tổng đó bằng 1 đúng với các đỉnh nằm trên đường: đỉnh trên đoạn u..lu..l hoặc v..lv..l nhận một dấu +1+1 (riêng ll nhận hai dấu +1+1 và một dấu −1-1), đỉnh từ cha của ll trở lên nhận đủ cả bốn dấu, tổng 0.

Bước 1: BFS và bảng. Như ví dụ trước, nhưng ở đây up[0][1] = 0 (mảng toàn cục sẵn bằng 0): ô 0 là lính canh nhận dấu −1-1 khi LCA là gốc, và không bao giờ được in. up[j][0] = 0 nên mọi bước nhảy vượt gốc dừng ở 0; hàm lca vẫn đúng vì chỉ nhảy khi còn đủ độ sâu và khi hai tổ tiên còn khác nhau.

Bước 2: đặt dấu cho mỗi đường.

C++
ll l = lca(u, v);
c[u]++;
c[v]++;
c[l]--;
c[up[0][l]]--;

Bước 3: cộng dồn một lần. Duyệt ord từ cuối lên là duyệt con trước cha; cộng giá trị của mỗi đỉnh vào cha.

C++
for (ll i = n - 1; i >= 1; i--) c[up[0][ord[i]]] += c[ord[i]];
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 2e5 + 5, LG = 18;
ll n, m, dep[N], up[LG][N], c[N];
vector<ll> g[N];

ll lca(ll a, ll b) {
    if (dep[a] < dep[b]) swap(a, b);
    for (ll j = LG - 1; j >= 0; j--)
        if (dep[a] - (1LL << j) >= dep[b]) a = up[j][a];
    if (a == b) return a;
    for (ll j = LG - 1; j >= 0; j--)
        if (up[j][a] != up[j][b]) a = up[j][a], b = up[j][b];
    return up[0][a];
}

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

    cin >> n >> m;
    for (ll i = 1; i < n; i++) {
        ll a, b;
        cin >> a >> b;
        g[a].push_back(b);
        g[b].push_back(a);
    }
    vector<ll> ord = {1};
    for (ll i = 0; i < (ll)ord.size(); i++) {
        ll u = ord[i];
        for (ll v : g[u])
            if (v != up[0][u]) {
                up[0][v] = u;
                dep[v] = dep[u] + 1;
                ord.push_back(v);
            }
    }
    for (ll j = 1; j < LG; j++)
        for (ll v = 1; v <= n; v++) up[j][v] = up[j - 1][up[j - 1][v]];
    while (m--) {
        ll u, v;
        cin >> u >> v;
        ll l = lca(u, v);
        c[u]++;
        c[v]++;
        c[l]--;
        c[up[0][l]]--;
    }
    for (ll i = n - 1; i >= 1; i--) c[up[0][ord[i]]] += c[ord[i]];
    for (ll v = 1; v <= n; v++) cout << c[v] << ' ';
    cout << '\n';
}
7 5
1 2
1 3
2 4
2 5
3 6
6 7
4 5
4 7
2 6
3 3
5 7

Độ phức tạp: O((n+m)log⁡n)O((n + m) \log n), phần log⁡\log do LCA.

Euler và RMQ

Ghi lại đỉnh mỗi lần DFS đi qua nó: khi vào, và mỗi lần quay về từ một con. Dãy dài 2n−12n - 1. Giữa lần đầu gặp uu và lần đầu gặp vv trong dãy, DFS đi từ uu qua LCA rồi xuống vv và không lên cao hơn LCA, nên LCA chính là đỉnh có độ sâu nhỏ nhất trong đoạn đó. Bảng thưa (bài 22) trên dãy này trả lời mỗi câu hỏi trong O(1)O(1) sau O(nlog⁡n)O(n \log n) tiền xử lý: hợp khi số câu hỏi rất lớn.

Ví dụXOR Của Hàng Triệu LCA

Đề. Cây n≤105n \le 10^5 đỉnh gốc 1. Có q≤107q \le 10^7 câu hỏi LCA sinh bằng công thức tuyến tính từ sáu tham số; in XOR của mọi đáp án.

Lời giải. 10710^7 câu hỏi nhân log⁡n\log n là quá nhiều, nên cần LCA trong O(1)O(1) mỗi câu. Dùng thứ tự Euler: đi DFS và ghi lại đỉnh mỗi lần đi qua, kể cả lúc quay về từ con. Giữa lần đầu gặp uu và lần đầu gặp vv trong dãy, đỉnh nông nhất chính là LCA. Bài thành min trên đoạn của một dãy cố định, trả lời bằng bảng thưa (bài 22).

Bước 1: dãy Euler. eu dài 2n−12n - 1; fst[v] là lần đầu gặp vv. Đệ quy sâu tới 10510^5 là mức thường chạy được.

C++
void dfs(ll u, ll p) {
    eu[++k] = u;
    fst[u] = k;
    for (ll v : g[u])
        if (v != p) {
            dep[v] = dep[u] + 1;
            dfs(v, u);
            eu[++k] = u;
        }
}

Bước 2: bảng thưa theo độ sâu. st[j][i] là đỉnh nông nhất trong 2j2^j phần tử bắt đầu ở ii của eu; better chọn đỉnh nông hơn.

C++
for (ll i = 1; i <= k; i++) st[0][i] = eu[i];
for (ll j = 1; j < LG; j++)
    for (ll i = 1; i + (1 << j) - 1 <= k; i++) st[j][i] = better(st[j - 1][i], st[j - 1][i + (1 << (j - 1))]);

Bước 3: LCA trong O(1)O(1). Đoạn [fstu,fstv][fst_u, fst_v] phủ bằng hai khối dài 2j2^j chồng lên nhau; lấy min mà chồng lên nhau không sao.

C++
ll lca(ll u, ll v) {
    ll l = fst[u], r = fst[v];
    if (l > r) swap(l, r);
    ll j = lg[r - l + 1];
    return better(st[j][l], st[j][r - (1 << j) + 1]);
}

Bước 4: sinh câu hỏi. u * uj tới 105⋅10910^5 \cdot 10^9, phải là long long.

C++
for (ll i = 1; i <= q; i++) {
    ans ^= lca(u, v);
    u = (u * uj + ua) % n + 1;
    v = (v * vj + va) % n + 1;
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 1e5 + 5, M = 2 * N, LG = 18;
ll n, q, k, dep[N], fst[N], eu[M], lg[M], st[LG][M];
vector<ll> g[N];

void dfs(ll u, ll p) {
    eu[++k] = u;
    fst[u] = k;
    for (ll v : g[u])
        if (v != p) {
            dep[v] = dep[u] + 1;
            dfs(v, u);
            eu[++k] = u;
        }
}

ll better(ll a, ll b) {
    return dep[a] < dep[b] ? a : b;
}

ll lca(ll u, ll v) {
    ll l = fst[u], r = fst[v];
    if (l > r) swap(l, r);
    ll j = lg[r - l + 1];
    return better(st[j][l], st[j][r - (1 << j) + 1]);
}

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

    cin >> n >> q;
    for (ll i = 1; i < n; i++) {
        ll a, b;
        cin >> a >> b;
        g[a].push_back(b);
        g[b].push_back(a);
    }
    dfs(1, 0);
    for (ll i = 2; i <= k; i++) lg[i] = lg[i / 2] + 1;
    for (ll i = 1; i <= k; i++) st[0][i] = eu[i];
    for (ll j = 1; j < LG; j++)
        for (ll i = 1; i + (1 << j) - 1 <= k; i++) st[j][i] = better(st[j - 1][i], st[j - 1][i + (1 << (j - 1))]);
    ll u, v, uj, vj, ua, va;
    cin >> u >> v >> uj >> vj >> ua >> va;
    ll ans = 0;
    for (ll i = 1; i <= q; i++) {
        ans ^= lca(u, v);
        u = (u * uj + ua) % n + 1;
        v = (v * vj + va) % n + 1;
    }
    cout << ans << '\n';
}
7 5
1 2
1 3
2 4
2 5
3 6
6 7
4 5 2 3 0 1

Độ phức tạp: dựng O(nlog⁡n)O(n \log n), mỗi câu hỏi hai lần đọc bảng.

So với nhảy nhị phân khi số câu hỏi rất lớn:

5 triệu câu hỏi LCA trên cây 100 nghìn đỉnh, trung vị 5 lần chạy, i7-10850H, g++ -O2.Euler + bảng thưa251 msnhảy nhị phân1094 ms · gấp 4,4 lần
5 triệu câu hỏi LCA trên cây 100 nghìn đỉnh, tính cả dựng bảng, đo trên i7-10850H, g++ -O2.

Euler cộng bảng thưa nhanh hơn khoảng 4 lần. Với vài trăm nghìn câu hỏi thì nhảy nhị phân đủ và ngắn hơn để viết; hàng triệu câu hỏi trở lên mới đáng dùng Euler.

Khi đi thi

  • Mọi câu hỏi “trên đường (u,v)(u, v)” trên cây tĩnh: tách tại LCA thành hai đoạn dọc; độ dài, tổng tiền tố từ gốc, đỉnh thứ kk đều làm được với bảng nhảy.
  • Nhảy nhị phân đủ cho 10510^5 đến 10610^6 câu hỏi; hàng chục triệu câu hỏi thì Euler cộng bảng thưa. Có cập nhật trên đường thì cần phân rã nặng nhẹ (bài 58).
  • Cộng trên đường rồi đọc cuối cùng: mảng hiệu trên cây với bốn dấu cho đỉnh, ba dấu cho cạnh. Chú ý cha của gốc.

Tóm tắt

  • LCA bằng nhảy nhị phân: cân độ sâu, rồi nhảy từ tầng cao khi tổ tiên còn khác, đáp án là cha chung.
  • Khoảng cách depu+depv−2depldep_u + dep_v - 2dep_l; tổng trọng số dùng tổng từ gốc; đỉnh thứ kk chọn đoạn lên hay đoạn xuống.
  • Gộp min, max theo từng bước nhảy cho câu hỏi trên đường.
  • Mảng hiệu trên cây: +1+1 ở hai đầu, −1-1 ở LCA và cha của LCA, rồi cộng dồn theo cây con.
  • LCA là đỉnh nông nhất giữa hai lần xuất hiện đầu trong thứ tự Euler: RMQ O(1)O(1).

Bài tập

Đọc thêm