Cho vài số, chọn một số trong chúng sao cho XOR lại được kết quả lớn nhất. Thử mọi tập con thì có cách. Nhưng nhiều tập cho cùng kết quả: nếu thì thêm số 6 vào không đem lại giá trị mới nào.
Coi mỗi số là một vector bit, XOR là phép cộng vector. Mọi giá trị XOR của các tập con tạo thành không gian sinh, và cơ sở XOR là 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ứ , 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à (vector trục của bit ). Chèn : duyệt bit từ cao xuống; gặp bit bật trong thì nếu b[i] trống, đặt b[i] = x và dừng; nếu không, để tắt bit . Nếu về 0 thì nó đã biểu diễn được bằng các số trước, tức phụ thuộc.
Phép không đổi không gian sinh, chỉ đổi cách viết. Số vector trục khác 0 là hạng . 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 giá trị XOR phân biệt (tính cả tập rỗng). Mỗi giá trị đạt được bởi đúng tập con của số ban đầu.
Trong hình, bit bật mà đã có trục thì XOR với trục đó; gặp bit trống thì số đang xét thành trục mới.
Hỏi nhanh
Các số có bao nhiêu giá trị XOR tập con phân biệt?
nên hạng chỉ là 2, có giá trị: .
XOR lớn nhất
XOR lớn nhất. Bắt đầu từ , duyệt bit từ cao xuống: nếu thì lấy. Trục chỉ ảnh hưởng bit và các bit thấp hơn, nên quyết định ở bit không bao giờ bị các bước sau làm hỏng. Bắt đầu từ một giá trị thay vì 0 thì được trên không gian sinh.
Cơ sở rút gọn. Khử thêm: với mỗi trục , xóa bit khỏi mọi trục cao hơn có bit đó. Khi ấy bit chỉ xuất hiện trong . Xếp các trục theo bit tăng dần . Giá trị nhỏ thứ (đếm từ 0, giá trị 0 là thứ 0) là XOR các mà bit của 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
Đề. số tới . 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ở, rồi dựng đáp án từ bit cao xuống: với mỗi trục , lấy nó nếu làm kết quả lớn hơn. Trục có bit cao nhất là , nên chỉ đổi bit và các bit thấp hơn; quyết định ở bit vì vậy không bị các bước sau làm hỏng.
Bước 1: chèn vào cơ sở. Duyệt bit từ cao xuống; bit của 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ì (đã khử) thành trục mới. Đi hết mà không tạo trục thì đã về 0: nó là XOR của vài phần tử trước, không làm không gian lớn thêm.
Bước 2: tham lam.
ll res = 0;
for (ll i = 30; i >= 0; i--) res = max(res, res ^ b[i]);
Lấy trục khi bit 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ì.
#include <bits/stdc++.h>
using namespace std;
#define 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
15
. Vòng chèn duyệt đủ mọi bit dù đa số bit của đã tắt. Nhảy thẳng tới bit cao nhất còn bật sẽ bớt được nhiều bước:
Dùng __builtin_clzll để nhảy tới bit cao nhất nhanh hơn duyệt đủ 60 bit khoảng 2 lần. Quy tắc: vòng lặp theo bit mà phần lớn bước không làm gì thì thay bằng lệnh nhảy tới bit cần.
Ví dụMột đoạn trong thứ tự XOR
Đề. Dãy số dưới . Sắp các giá trị XOR phân biệt (gồm 0) tăng dần thành ; in với .
Lời giải. Rút gọn cơ sở để bit trục chỉ còn xuất hiện trong . Xếp các trục theo bit tăng dần . 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ứ (đếm từ 0) là XOR các mà bit của bật; ứng với .
Bước 1: rút gọn. Với trục từ thấp lên, mọi trục cao hơn có bit bật thì XOR với để tắt nó. Trục chỉ chứa các bit không cao hơn , nên phép XOR không làm hỏng bit trục cao.
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ứ . c là các trục khác 0 theo thứ tự bit tăng; bit của quyết định có dùng không.
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 << ' ';
}
#include <bits/stdc++.h>
using namespace std;
#define 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
1 2 3
bước để in.
Tập độc lập
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ữ 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à đổ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ố theo dạng (số, trọng số). Tham lam chọn những số nào?
Thứ tự trọng số giảm là 3, 1, 2. Chèn 3 rồi 1 thành công; 2 thì nên bị loại. Tổng .
Ví dụTập mã có trọng số
Đề. loại quặng, mỗi loại có mã tới và sức mạnh tới . 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 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.
sort(a + 1, a + n + 1);
for (ll i = n; i >= 1; i--)
if (insert(a[i].second)) sum += a[i].first;
#include <bits/stdc++.h>
using namespace std;
#define 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
15
Độ phức tạp: .
Cơ sở trên đoạn
Hỏi XOR lớn nhất của tập con trong đoạn . Quét tăng dần và giữ một cơ sở của tiền tố , nhưng mỗi trục ghi kèm vị trí , và ưu tiên trục có vị trí lớn nhất: khi chèn (vị trí ) gặp trục cũ hơn (), đổi chỗ với cùng vị trí, rồi tiếp tục khử với phần tử cũ. Khi đó các trục có sinh ra đúng không gian của đoạn , 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 ) tốn 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 số tới , đoạn : 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 . Quét tăng dần, giữ cơ sở của tiền tố 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ó sinh ra đúng không gian của các phần tử trong . Truy vấn là tham lam XOR lớn nhất trên các trục đó. Giá trị dưới nên cơ sở 20 bit.
Bước 1: chèn ưu tiên phần tử mới. Ở bit đã có trục mà trục đó cũ hơn () 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.
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 là phần tử mới nhất có thể đứng ở đó. Các trục có vị trí được dựng hoàn toàn từ phần tử trong , và mọi phần tử của đề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.
for (int r = 1; r <= n; r++) {
insert(c[r], r);
for (auto q : at[r]) {
int l = q.first, id = q.second, res = 0;
for (int i = 19; i >= 0; i--)
if (pb[i] >= l && (res ^ b[i]) > res) res ^= b[i];
ans[id] = res;
}
}
#include <bits/stdc++.h>
using namespace std;
#define 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 q : at[r]) {
int l = q.first, id = q.second, 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
7
14
4
Độ phức tạp: , chỉ một cơ sở trong bộ nhớ.
Chu trình đồ thị
XOR của một walk (được lặp đỉnh, cạnh) từ tới : đ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 là XOR từ gốc tới , mỗi cạnh ngoài cây cho chu trình .
Đáp án lớn nhất là tham lam 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 đỉnh, cạnh trọng số tới (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 .
Lời giải. Dựng cây BFS từ đỉnh 1, là XOR các cạnh trên đường cây từ 1 tới . Mọi walk từ 1 tới có giá trị bằng XOR một tổ hợp các chu trình, và các chu trình cơ bản (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ừ thay vì từ 0.
Bước 1: BFS và chu trình. Gặp đỉnh mới thì ghi ; gặp đỉnh đã thăm thì chèn . 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 , cạnh song song cho XOR hai trọng số: tự được xử lý.
for (auto e : adj[u]) {
int v = e.first;
ll w = e.second;
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ừ . Kết quả là trên mọi của không gian chu trình.
ll res = d[n];
for (int i = 59; i >= 0; i--) res = max(res, res ^ b[i]);
#include <bits/stdc++.h>
using namespace std;
#define 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 e : adj[u]) {
int v = e.first;
ll w = e.second;
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
7
Độ phức tạp: .
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 mỗi số. Dùng
long longvà đủ bit, như 60 bit cho số tới . - Giá trị thứ , số tập con cho một giá trị: cơ sở rút gọn, mỗi giá trị có 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 cho 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ứ ứng với các bit của .
- Tập độc lập là matroid: tham lam theo trọng số cho tập tốt nhất.