Chương 6·Bài 61·Nâng cao

XOR Basis

Coi mỗi số là một vector bit, XOR là phép cộng vector trên trường hai phần tử. Tập mọi giá trị XOR của các tập con là không gian sinh của các số; cơ sở XOR là một tập nhỏ (tối đa số bit, như 60) sinh ra đúng không gian đó. Có cơ sở thì trả lời ngay: XOR lớn nhất, số giá trị phân biệt, giá trị thứ kk, một số có biểu diễn được không. Cùng ý tưởng còn giải bài XOR trên đường đi đồ thị và trên đoạn.

Chèn vào cơ sở

Giữ mảng b[i]: hoặc 0, hoặc một số có bit cao nhất đúng là ii (vector trục của bit ii). Chèn xx: duyệt bit từ cao xuống; gặp bit ii bật trong xx thì nếu b[i] trống, đặt b[i] = x và dừng; nếu không, x←x⊕b[i]x \leftarrow x \oplus b[i] để tắt bit ii. Nếu xx về 0 thì nó đã biểu diễn được bằng các số trước, tức phụ thuộc.

Phép x⊕b[i]x \oplus b[i] không đổi không gian sinh, chỉ đổi cách viết. Số vector trục khác 0 là hạng rr. Mỗi giá trị trong không gian sinh có đúng một cách viết bằng các vector trục, nên có đúng 2r2^r giá trị XOR phân biệt; mỗi giá trị đạt được bởi đúng 2n−r2^{n - r} tập con của nn số ban đầu.

Chèn một số: mỗi bit bật đã có trục thì XOR với trục đó, gặp bit trống thì thành trục mới.

Hỏi nhanh

Các số 3,5,63, 5, 6 có bao nhiêu giá trị XOR tập con phân biệt?

Ví dụSố giá trị XOR phân biệt

Đề. n≤2⋅105n \le 2 \cdot 10^5 số tới 10910^9. Đếm số giá trị XOR phân biệt của các tập con, tính cả tập rỗng.

Lời giải. Chèn mọi số vào cơ sở XOR, đếm số lần chèn thành công, tức hạng rr. Các giá trị XOR của tập con chính là không gian sinh bởi cơ sở. Mỗi tập con của rr trục cho một giá trị khác nhau, vì trục cao nhất khác nhau quyết định bit cao nhất khác nhau. Vậy có đúng 2r2^r giá trị, tính cả 0 của tập rỗng.

Bước 1: chèn vào cơ sở. Duyệt bit từ cao xuống; bit ii của xx bật mà đã có trục b[i] thì XOR để tắt bit đó. Gặp bit bật mà chưa có trục thì xx (đã khử) thành trục mới. Đi hết mà không tạo trục thì xx đã về 0: nó là XOR của một số phần tử trước, không làm không gian lớn thêm.

C++
void insert(ll x) {
    for (ll i = 30; i >= 0; i--) {
        if (!(x >> i & 1)) continue;
        if (!b[i]) {
            b[i] = x;
            rk++;
            return;
        }
        x ^= b[i];
    }
}

In 1LL << rk vì hạng tới 30.

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

ll n, b[31], rk;

void insert(ll x) {
    for (ll i = 30; i >= 0; i--) {
        if (!(x >> i & 1)) continue;
        if (!b[i]) {
            b[i] = x;
            rk++;
            return;
        }
        x ^= b[i];
    }
}

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

    cin >> n;
    for (ll i = 1; i <= n; i++) {
        ll x;
        cin >> x;
        insert(x);
    }
    cout << (1LL << rk) << '\n';
}
3
1 2 3

Mỗi lần chèn 31 bước.

XOR lớn nhất và cơ sở rút gọn

XOR lớn nhất. Bắt đầu từ res=0res = 0, duyệt bit từ cao xuống: nếu res⊕b[i]>resres \oplus b[i] > res thì lấy. Vector trục b[i]b[i] chỉ ảnh hưởng bit ii và các bit thấp hơn, nên quyết định ở bit ii không bao giờ bị các bước sau làm hỏng. Bắt đầu từ một giá trị xx thay vì 0 thì được max⁡(x⊕s)\max (x \oplus s) trên không gian sinh.

Cơ sở rút gọn. Khử thêm: với mỗi trục ii, xóa bit ii khỏi mọi trục cao hơn có bit đó. Khi ấy bit trục ii chỉ xuất hiện trong b[i]b[i]. Xếp các trục theo bit tăng dần c0<c1<⋯<cr−1c_0 < c_1 < \dots < c_{r-1}; giá trị nhỏ thứ tt (đếm từ 0, giá trị 0 là thứ 0) là XOR các cjc_j mà bit jj của tt bật, vì so hai tổ hợp chỉ cần so trục cao nhất khác nhau.

Ví dụXOR tập con lớn nhất

Đề. n≤2⋅105n \le 2 \cdot 10^5 số tới 10910^9. Tìm XOR lớn nhất của một tập con.

Lời giải. Chèn mọi số vào cơ sở như ví dụ trước, rồi dựng đáp án từ bit cao xuống: với mỗi trục b[i]b[i], lấy nó nếu làm kết quả lớn hơn. Trục b[i]b[i] có bit cao nhất là ii, nên chỉ đổi bit ii và các bit thấp hơn; quyết định ở bit ii vì vậy không bị các bước sau làm hỏng.

C++
ll res = 0;
for (ll i = 30; i >= 0; i--) res = max(res, res ^ b[i]);

Lấy trục khi bit ii của res đang tắt, bỏ qua khi đang bật; trục chưa có (b[i] = 0) không làm đổi gì.

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

ll n, b[31];

void insert(ll x) {
    for (ll i = 30; i >= 0; i--) {
        if (!(x >> i & 1)) continue;
        if (!b[i]) {
            b[i] = x;
            return;
        }
        x ^= b[i];
    }
}

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

    cin >> n;
    for (ll i = 1; i <= n; i++) {
        ll x;
        cin >> x;
        insert(x);
    }
    ll res = 0;
    for (ll i = 30; i >= 0; i--) res = max(res, res ^ b[i]);
    cout << res << '\n';
}
4
1 2 7 8

O(31n)O(31n).

Ví dụMột đoạn trong thứ tự XOR

Đề. Dãy N≤2⋅105N \le 2 \cdot 10^5 số dưới 2602^{60}. Sắp các giá trị XOR phân biệt (gồm 0) tăng dần thành s1<s2<…s_1 < s_2 < \dots; in sL,…,sRs_L, \dots, s_R với R−L+1≤2⋅105R - L + 1 \le 2 \cdot 10^5.

Lời giải. Rút gọn cơ sở để bit trục ii chỉ còn xuất hiện trong b[i]b[i]. Xếp các trục theo bit tăng dần c0<c1<⋯<cr−1c_0 < c_1 < \dots < c_{r-1}. So hai giá trị sinh từ cơ sở rút gọn chỉ cần xem trục cao nhất mà một bên dùng còn bên kia không, nên thứ tự các giá trị trùng với thứ tự nhị phân của tập trục được dùng. Giá trị nhỏ thứ tt (đếm từ 0) là XOR các cjc_j mà bit jj của tt bật; sLs_L ứng với t=L−1t = L - 1.

Bước 1: rút gọn. Với trục ii từ thấp lên, mọi trục cao hơn có bit ii bật thì XOR với b[i]b[i] để tắt nó. Trục b[i]b[i] chỉ chứa các bit không cao hơn ii, nên phép XOR không làm hỏng bit trục cao.

C++
for (ll i = 0; i < 60; i++)
    if (b[i])
        for (ll j = i + 1; j < 60; j++)
            if (b[j] >> i & 1) b[j] ^= b[i];

Bước 2: giá trị thứ tt. c là các trục khác 0 theo thứ tự bit tăng; bit jj của tt quyết định có dùng cjc_j không.

C++
for (ll t = L - 1; t <= R - 1; t++) {
    ll v = 0;
    for (ll j = 0; j < (ll)c.size(); j++)
        if (t >> j & 1) v ^= c[j];
    cout << v << ' ';
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

ll n, L, R, b[60];

void insert(ll x) {
    for (ll i = 59; i >= 0; i--) {
        if (!(x >> i & 1)) continue;
        if (!b[i]) {
            b[i] = x;
            return;
        }
        x ^= b[i];
    }
}

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

    cin >> n >> L >> R;
    for (ll i = 1; i <= n; i++) {
        ll x;
        cin >> x;
        insert(x);
    }
    for (ll i = 0; i < 60; i++)
        if (b[i])
            for (ll j = i + 1; j < 60; j++)
                if (b[j] >> i & 1) b[j] ^= b[i];
    vector<ll> c;
    for (ll i = 0; i < 60; i++)
        if (b[i]) c.push_back(b[i]);
    for (ll t = L - 1; t <= R - 1; t++) {
        ll v = 0;
        for (ll j = 0; j < (ll)c.size(); j++)
            if (t >> j & 1) v ^= c[j];
        cout << v << ' ';
    }
    cout << '\n';
}
3 2 4
1 2 4

60(R−L+1)60(R - L + 1) bước để in.

Tập độc lập có trọng số lớn nhất

Chọn tập con độc lập (không tập con khác rỗng nào có XOR bằng 0) có tổng trọng số lớn nhất: sắp theo trọng số giảm dần, chèn lần lượt, giữ lại phần tử nào chèn thành công. Tham lam đúng vì các tập độc lập tạo thành một matroid: mọi tập độc lập cực đại có cùng kích thước (bằng hạng), và có thể đổi phần tử để tăng trọng số như trong thuật toán Kruskal. Cũng cách này cho bài “mua ít tiền nhất để đủ hạng”: sắp theo giá tăng dần.

Hỏi nhanh

Trọng số (1,5),(2,4),(3,10)(1, 5), (2, 4), (3, 10) theo dạng (số, trọng số). Tham lam chọn những số nào?

Ví dụTập mã có trọng số

Đề. N≤1000N \le 1000 loại quặng, mỗi loại có mã tới 101810^{18} và sức mạnh tới 10410^4. Chọn các loại sao cho không tập con khác rỗng nào có XOR mã bằng 0, tổng sức mạnh lớn nhất.

Lời giải. Tập mã không có tập con XOR bằng 0 chính là tập độc lập tuyến tính. Các tập độc lập tạo thành matroid, nên tham lam như Kruskal là đúng: xét các loại theo sức mạnh giảm dần, giữ loại nào chèn vào cơ sở thành công. Chèn thất bại nghĩa là mã đó là XOR của một số mã đã chọn, tức thêm nó sẽ tạo tập con XOR bằng 0.

Bước 1: chèn có báo kết quả. insert trả về có tạo trục mới hay không. Mã tới 1018<26010^{18} < 2^{60} nên cơ sở 60 bit là đủ.

Bước 2: tham lam. a[i] lưu cặp (sức mạnh, mã) để sort sắp theo sức mạnh; duyệt từ cuối mảng là sức mạnh giảm dần.

C++
sort(a + 1, a + n + 1);
for (ll i = n; i >= 1; i--)
    if (insert(a[i].second)) sum += a[i].first;
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

ll n, b[60];
pair<ll, ll> a[1005];

bool insert(ll x) {
    for (ll i = 59; i >= 0; i--) {
        if (!(x >> i & 1)) continue;
        if (!b[i]) {
            b[i] = x;
            return 1;
        }
        x ^= b[i];
    }
    return 0;
}

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

    cin >> n;
    for (ll i = 1; i <= n; i++) cin >> a[i].second >> a[i].first;
    sort(a + 1, a + n + 1);
    ll sum = 0;
    for (ll i = n; i >= 1; i--)
        if (insert(a[i].second)) sum += a[i].first;
    cout << sum << '\n';
}
3
1 5
2 4
3 10

O(Nlog⁡N+60N)O(N \log N + 60N).

Cơ sở theo tiền tố, giữ vị trí mới nhất

Hỏi XOR lớn nhất của tập con trong đoạn [l,r][l, r]. Quét rr tăng dần và giữ một cơ sở của tiền tố 1..r1..r, nhưng mỗi trục ghi kèm vị trí pospos, và ưu tiên trục có vị trí lớn nhất: khi chèn xx (vị trí pp) gặp trục b[i]b[i] cũ hơn (posi<ppos_i < p), đổi chỗ xx với b[i]b[i] cùng vị trí, rồi tiếp tục khử với phần tử cũ. Khi đó các trục có pos≥lpos \ge l sinh ra đúng không gian của đoạn [l,r][l, r], nên truy vấn chỉ tham lam trên các trục đó.

Làm ngoại tuyến (sắp truy vấn theo rr) tốn O((n+q)log⁡V)O((n + q) \log V) thời gian và một cơ sở bộ nhớ; muốn trực tuyến thì lưu bản sao cơ sở của mọi tiền tố.

Ví dụXOR lớn nhất trên đoạn

Đề. Dãy n≤5⋅105n \le 5 \cdot 10^5 số tới 10610^6, q≤5⋅105q \le 5 \cdot 10^5 đoạn [l,r][l, r]: XOR lớn nhất của một tập con trong đoạn.

Lời giải. Làm ngoại tuyến: gom truy vấn theo đầu phải rr. Quét rr tăng dần, giữ cơ sở của tiền tố 1..r1..r mà mỗi trục nhớ vị trí pb, và luôn ưu tiên trục có vị trí lớn nhất. Khi đó các trục có pb≥lpb \ge l sinh ra đúng không gian của các phần tử trong [l,r][l, r]. Truy vấn [l,r][l, r] là tham lam XOR lớn nhất trên các trục đó. Giá trị dưới 2202^{20} nên cơ sở 20 bit.

Bước 1: chèn ưu tiên phần tử mới. Ở bit ii đã có trục mà trục đó cũ hơn (pbi<ppb_i < p) thì đổi chỗ: phần tử mới chiếm trục, phần tử cũ tiếp tục đi xuống để khử. Không gian sinh không đổi, chỉ thay vector đứng đại diện.

C++
void insert(int x, int p) {
    for (int i = 19; i >= 0; i--) {
        if (!(x >> i & 1)) continue;
        if (!b[i]) {
            b[i] = x, pb[i] = p;
            return;
        }
        if (p > pb[i]) swap(b[i], x), swap(pb[i], p);
        x ^= b[i];
    }
}

Vì sao đúng: sau mỗi lần chèn, trục ở bit ii là phần tử mới nhất có thể đứng ở đó. Các trục có vị trí ≥l\ge l được dựng hoàn toàn từ phần tử trong [l,r][l, r], và mọi phần tử của [l,r][l, r] đều biểu diễn được qua chúng, vì phần bị đẩy xuống trong lúc chèn luôn cũ hơn chính phần tử đẩy nó.

Bước 2: trả lời theo đầu phải. Tham lam từ bit cao như ví dụ XOR lớn nhất, bỏ qua trục có pb[i] < l.

C++
for (int r = 1; r <= n; r++) {
    insert(c[r], r);
    for (auto [l, id] : at[r]) {
        int res = 0;
        for (int i = 19; i >= 0; i--)
            if (pb[i] >= l && (res ^ b[i]) > res) res ^= b[i];
        ans[id] = res;
    }
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const int N = 5e5 + 5;
int n, q, c[N], ans[N], b[20], pb[20];
vector<pair<int, int>> at[N];

void insert(int x, int p) {
    for (int i = 19; i >= 0; i--) {
        if (!(x >> i & 1)) continue;
        if (!b[i]) {
            b[i] = x, pb[i] = p;
            return;
        }
        if (p > pb[i]) swap(b[i], x), swap(pb[i], p);
        x ^= b[i];
    }
}

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

    cin >> n;
    for (int i = 1; i <= n; i++) cin >> c[i];
    cin >> q;
    for (int i = 1; i <= q; i++) {
        int l, r;
        cin >> l >> r;
        at[r].push_back({l, i});
    }
    for (int r = 1; r <= n; r++) {
        insert(c[r], r);
        for (auto [l, id] : at[r]) {
            int res = 0;
            for (int i = 19; i >= 0; i--)
                if (pb[i] >= l && (res ^ b[i]) > res) res ^= b[i];
            ans[id] = res;
        }
    }
    for (int i = 1; i <= q; i++) cout << ans[i] << '\n';
}
4
1 2 4 8
3
1 3
2 4
3 3

O((n+q)⋅20)O((n + q) \cdot 20), chỉ một cơ sở trong bộ nhớ.

Chu trình trong đồ thị

XOR của một walk (được lặp đỉnh, cạnh) từ 11 tới nn: đi một cạnh hai lần thì nó triệt tiêu. Vì vậy mọi walk có giá trị bằng XOR của một đường đi cố định cộng với XOR của một số chu trình: đi ra chu trình, vòng một lần, đi về bằng đúng đường cũ. Các chu trình sinh ra một không gian, và chỉ cần các chu trình cơ bản: dựng cây BFS với dvd_v là XOR từ gốc tới vv, mỗi cạnh ngoài cây (u,v,w)(u, v, w) cho chu trình du⊕dv⊕wd_u \oplus d_v \oplus w.

Đáp án lớn nhất là tham lam max⁡(dn⊕s)\max(d_n \oplus s) trên cơ sở của các chu trình; nhỏ nhất thì tham lam ngược lại.

Ví dụHành Trình XOR Lớn Nhất

Đề. Đồ thị liên thông N≤5⋅104N \le 5 \cdot 10^4 đỉnh, M≤105M \le 10^5 cạnh trọng số tới 101810^{18} (có thể có khuyên, cạnh song song). Tìm XOR lớn nhất của một walk từ 1 tới NN.

Lời giải. Dựng cây BFS từ đỉnh 1, dvd_v là XOR các cạnh trên đường cây từ 1 tới vv. Mọi walk từ 1 tới NN có giá trị bằng dNd_N XOR một tổ hợp các chu trình, và các chu trình cơ bản du⊕dv⊕wd_u \oplus d_v \oplus w (một cho mỗi cạnh ngoài cây) sinh ra mọi chu trình. Chèn chúng vào cơ sở, rồi tham lam lấy XOR lớn nhất bắt đầu từ dNd_N thay vì từ 0.

Bước 1: BFS và chu trình. Gặp đỉnh mới thì ghi dvd_v; gặp đỉnh đã thăm thì chèn du⊕dv⊕wd_u \oplus d_v \oplus w. Cạnh cây gặp lại từ phía con cho đúng 0, và chèn 0 không làm gì. Cạnh ngoài cây được chèn hai lần (từ hai đầu) cùng một giá trị, lần sau cũng không làm gì. Khuyên cho chu trình ww, cạnh song song cho XOR hai trọng số: tự được xử lý.

C++
for (auto [v, w] : adj[u]) {
    if (!vis[v]) vis[v] = 1, d[v] = d[u] ^ w, q.push(v);
    else insert(d[u] ^ d[v] ^ w);
}

Bước 2: tham lam từ dNd_N. Kết quả là max⁡(dN⊕s)\max(d_N \oplus s) trên mọi ss của không gian chu trình.

C++
ll res = d[n];
for (int i = 59; i >= 0; i--) res = max(res, res ^ b[i]);
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

const int N = 5e4 + 5;
int n, m;
ll d[N], b[60];
bool vis[N];
vector<pair<int, ll>> adj[N];

void insert(ll x) {
    for (int i = 59; i >= 0; i--) {
        if (!(x >> i & 1)) continue;
        if (!b[i]) {
            b[i] = x;
            return;
        }
        x ^= b[i];
    }
}

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

    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int u, v;
        ll w;
        cin >> u >> v >> w;
        adj[u].push_back({v, w});
        adj[v].push_back({u, w});
    }
    queue<int> q;
    q.push(1);
    vis[1] = 1;
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (auto [v, w] : adj[u]) {
            if (!vis[v]) vis[v] = 1, d[v] = d[u] ^ w, q.push(v);
            else insert(d[u] ^ d[v] ^ w);
        }
    }
    ll res = d[n];
    for (int i = 59; i >= 0; i--) res = max(res, res ^ b[i]);
    cout << res << '\n';
}
3 3
1 2 1
2 3 2
1 3 7

O(N+60M)O(N + 60M).

Khi đi thi

  • “XOR một tập con”, “có biểu diễn được không”, “bao nhiêu giá trị”: cơ sở XOR, chèn O(log⁡V)O(\log V) mỗi số. Kiểu long long và đủ bit, như 60 bit cho số tới 101810^{18}.
  • Giá trị thứ kk hay số tập con cho một giá trị: cơ sở rút gọn, mỗi giá trị có 2n−r2^{n - r} tập con.
  • Trên đoạn: cơ sở tiền tố giữ vị trí mới nhất; trên đồ thị: một đường đi cộng cơ sở các chu trình cơ bản.

Tóm tắt

  • Chèn: khử bit cao nhất bằng trục tương ứng; về 0 nghĩa là phụ thuộc. Hạng rr cho 2r2^r giá trị phân biệt.
  • XOR lớn nhất: tham lam từ bit cao, lấy trục nếu kết quả tăng.
  • Cơ sở rút gọn sắp được không gian sinh theo thứ tự: giá trị thứ tt ứng với các bit của tt.
  • Tập độc lập là matroid: tham lam theo trọng số cho tập tốt nhất.

Bài tập

1
Hạng của một tập sốcanuc80k6100Số phần tử ít nhất phải xóa để tập độc lập là NN trừ hạng.
LQDOJ
2
Bảng công tắccanuc80k6103Mỗi công tắc là một vector trạng thái đèn; số trạng thái đạt được là 2 mũ hạng.
LQDOJ
3
Lượt lấy quyết địnhcanuc80k6105Giữ lại tập độc lập có tổng lớn nhất (tham lam theo kích thước giảm dần) để đối thủ không tạo được XOR bằng 0; lấy đi phần còn lại.
LQDOJ
4
Bộ nhãn đủ hạngcanuc80k6106Sắp gói theo giá tăng dần, mua gói nếu nhãn độc lập với các gói đã mua, tới khi đủ hạng NN.
LQDOJ
5
Hai nhóm XORcanuc80k6107W⊕BW \oplus B bằng tổng XOR XX cố định nên W+B=X+2(W&¬X)W + B = X + 2(W \mathbin{\&} \lnot X); tìm XOR lớn nhất trên các ai&¬Xa_i \mathbin{\&} \lnot X.
LQDOJ
6
Đếm biểu diễn theo tiền tốcanuc80k6108Quét tiền tố: nếu xx thuộc không gian sinh thì đáp án là 2l−r2^{l - r} với rr là hạng, ngược lại 0.
LQDOJ
7
Phân hoạch độc lậpcanuc80k6109XOR các đoạn sinh cùng không gian với các XOR tiền tố; đáp án là hạng của các tiền tố, −1-1 nếu tổng XOR bằng 0.
LQDOJ
8
K giá trị XOR đầu tiêncanuc80k6110Mỗi giá trị phân biệt lặp 2n−r2^{n - r} lần; liệt kê các giá trị nhỏ nhất bằng cơ sở rút gọn.
LQDOJ
9
Vị trí đầu của một giá trị XORcanuc80k6112Trong cơ sở rút gọn, các bit trục có trong QQ cho thứ tự tt của nó; vị trí đầu là t⋅2n−r+1t \cdot 2^{n - r} + 1.
LQDOJ
10
Biểu diễn trong đoạncanuc80k6113Cơ sở tiền tố giữ vị trí mới nhất; XX biểu diễn được khi khử được hết bằng các trục có vị trí không nhỏ hơn LL.
LQDOJ
11
Kho giá trị theo vị trícanuc80k6116Cây phân đoạn theo xô, mỗi nút một cơ sở; hợp hai cơ sở bằng cách chèn trục của bên này vào bên kia.
LQDOJ
12
Quyền kiểm soát XORcanuc80k6117Duyệt ngược: giữ cơ sở các AiA_i của người 0 ở phía sau; gặp lượt người 1 có AiA_i ngoài không gian đó thì người 1 thắng.
LQDOJ
13
Hành Trình XOR Nhỏ Nhấtcanuc80k6122Như ví dụ đồ thị nhưng lấy nhỏ nhất: rút dnd_n bằng các trục từ bit cao, lấy khi kết quả giảm.
LQDOJ
14
XOR trên đường câycanuc80k6123Nhảy nhị phân lưu cơ sở cho mỗi đoạn 2j2^j tổ tiên, hoặc cơ sở tiền tố theo cha giữ đỉnh sâu nhất; ghép hai nửa đường.
LQDOJ
15
Biểu diễn trên đường câycanuc80k6124Cơ sở theo cha giữ đỉnh sâu nhất cho mỗi trục; trên đường dùng các trục có độ sâu không nhỏ hơn độ sâu LCA.
LQDOJ

Đọc thêm

  • XOR BasisUSACO GuideChèn, XOR lớn nhất, cơ sở trên đoạn.
  • 线性基OI WikiCơ sở rút gọn, giá trị thứ kk, hợp cơ sở.