Mục lục
Mở Mục lục
Giới thiệu
Đây là một kỹ thuật được dùng để giải quyết một số bài toán có liên quan đến tổng xor của đoạn con (liên tiếp hoặc không liên tiếp) của mảng các số nguyên cho trước hoặc thêm phần tử vào mảng song song với việc truy vấn.
Kỹ thuật này có thể được chia làm hai phần chính:
- Biểu diễn các số nguyên ở cơ số 2 và xem mỗi phần tử là một vector trong không gian vector , với là số bit tối đa cần dùng để biểu diễn. Lúc này phép xor giữa các phần tử tương đương với phép cộng giữa các vector tương ứng trong không gian vector .
- Tìm mối liên hệ giữa yêu cầu của các truy vấn và cơ sở của không gian vector tìm được ở trên.
Một số khái niệm
Không gian Vector
Một tập được gọi là một không gian vector nếu được trang bị hai phép toán: phép cộng và phép nhân với một vô hướng. Các phần tử trong được gọi là các vector.
- phải có tính chất đóng, tức là và với là một đại lượng vô hướng.
- Phép cộng phải có tính chất kết hợp và giao hoán.
- Phép nhân với một vô hướng phải có tính chất kết hợp và phân phối.
- Trong phải có phần tử gọi là vector không (chú ý không lẫn với số ).
Một số không gian vector đặc biệt:
- Không gian vector các tọa độ .
- Không gian vector các ma trận kích cỡ .
- Không gian vector các đa thức bậc không quá .
- Không gian vector các hàm số.
- …
Độc lập tuyến tính và phụ thuộc tuyến tính
Một tập các vector được gọi là độc lập tuyến tính nếu phương trình:
chỉ có duy nhất một nghiệm tầm thường .
Ngược lại nếu tồn tại không đồng thời bằng không, sao cho:
thì khi đó được gọi là phụ thuộc tuyến tính.
Không gian vector
: là tập các số dư khi chia lấy dư cho và các phép toán trên cũng chia lấy dư cho . Suy ra là tập các số dư khi chia lấy dư cho do đó .
: là một không gian vector chiều bao gồm tất cả các vector có tọa độ, mỗi tọa độ là một phần tử của .
- Nếu hai vector thì được định nghĩa là ( là phép xor bit). Chú ý phải thõa mãn. Cụ thể hơn:
Tổng Tổng mod 2 0 0 0 0 0 0 1 1 1 1 1 0 1 1 1 1 1 0 2 0 - Với , kí hiệu . Ta có nếu chẵn và nếu lẻ.
Kể từ đây chúng ta chỉ quan tâm đến không gian vector , mọi phép tính đều chia lấy dư cho 2 và xem phép (xor bit) tương đương với phép trong .
Bao tuyến tính (Linear span, gọi tắt là span)
Một không gian vector được span bởi một tập các vector chứa tất cả các vector được biểu diễn thông qua một tổ hợp tuyến tính của các vector trong . lúc này ta nói được span bởi , được sinh bởi hay sinh ra . Chú ý: do chỉ xét trên nên cũng phải thuộc . Do đó trong tổ hợp trên mỗi chỉ có hai trạng thái là xuất hiện hoặc không xuất hiện. Ví dụ: tìm span, span và span?
- Ta có , suy ra:
Một số tính chất quan trọng:
-
Nếu thì:
-
Cơ sở (Basis)
Một tập các vector được gọi là cơ sở của một không gian vector nếu span và là độc lập tuyến tính. Khi đó được gọi là số chiều của và kí hiệu là .
Ví dụ: Xét không gian vector , ta có là một cơ sở của nhưng thì không vì tập này là không độc lập tuyến tính do . Và dĩ nhiên và cũng là các cơ sở của .
Chú ý: số phần tử phân biệt trong không gian vector được span bởi một cơ sở là .
Ma trận
Không gian hàng và không gian cột
Xét một ma trận kích cỡ ( hàng và cột):
- Chia ma trận thành các vector hàng, ta được:
- Chia ma trận thành các vector cột, ta được:
- Khi đó không gian hàng và không gian cột của ma trận được định nghĩa như sau:
Hạng của ma trận
Người ta chứng minh được rằng dim dim và được kí hiệu là dim hay còn được biết đến là hạng (rank) của ma trận , kí hiệu rank. (Tham khảo phần chứng minh tại đây)
Không gian hạch (Null space)
Để hiểu rõ được phần này, bạn cần phải quen thuộc với phép nhân ma trận.
Không gian hạch của một ma trận , kí hiệu là null là tập các vector (được viết ở dạng cột) sao cho:
Giả sử:
Khi đó là nghiệm của hệ phương trình:
Đặc biệt: nếu ma trận kích cỡ gồm vector và vector (các vector và được viết ở dạng cột):
Khi đó:
Để ý rằng không gian hạch cũng là một không gian vector, vì:
Số vô hiệu (Nullity)
Số vô hiệu của một ma trận chính là số chiều của không gian hạch của :
Định lý về hạng và số vô hiệu
Định lý này phát biểu rằng với một ma trận kích cỡ thì:
Phần chứng minh khá phức tạp và đòi hỏi giới thiệu thêm nhiều khái niệm nên các bạn có thể tham khảo tại đây.
Thuật toán tìm cơ sở của một không gian vector
Tiếp theo chúng ta sẽ đi vào phần chính của bài viết này đó chính là làm thế nào để có thể tìm cơ sở của một không gian vector, trong đó mỗi vector là một phần tử của một cách hiệu quả. Chúng ta sẽ phân tích thuật toán thông qua bài toán dưới đây.
XOR Closure
Đề bài
Cho một mảng có phần tử phân biệt . Yêu cầu tìm số phần tử ít nhất cần thêm vào mảng sao cho điều sau luôn đúng: với mọi thuộc thì cũng thuộc .
Giới hạn
- .
- .
Lời giải
Để ý rằng chúng ta cần thỏa mãn điều kiện thì . Do đó cần phải xây dựng được cơ sở của không gian vector được span bởi mảng . Khi đó đáp án của bài toán chính là .
Chúng ta sẽ xét lần lượt từng phần tử như sau:
- Giả sử chúng ta đã xét đến và có cơ sở . Chúng ta cần cập nhật sao cho cũng có thể được biểu diễn thông qua các vector trong .
- Kiểm tra xem có thể được biểu diễn thông qua các vector trong hay không.
- Nếu có thì không cần làm gì cả.
- Ngược lại chỉ cần thêm vào .
Phần khó nhất đó chính là làm thế nào để có thể kiểm tra xem có thể được biểu diễn thông qua các vector trong hay không.
- Nếu xem xét tất cả tổ hợp tuyến tính của các vector trong thì rõ ràng sẽ không đủ thời gian.
- Với một số nguyên dương , định nghĩa msb vị trí của most significant bit trong . Ví dụ:
- Giả sử chúng ta có cơ sở của sao cho .
- Khi đó chỉ cần duyệt từ . Nếu thì thay . Nếu cuối cùng thì có thể được biểu diễn thông qua các vector trong . Ngược lại thêm giá trị hiện tại của vào vị trí thích hợp trong sao cho vẫn giữ được thứ tự giảm dần của msb.
- Ví dụ: kiểm tra xem có thuộc hay không?
- cập nhật .
- cập nhật .
- .
- cập nhật .
Vậy và .
-
Tổng quát: thuật toán trên cho phép tìm giá trị nhỏ nhất của với .
-
Ví dụ: xét và . Khi đó giá trị nhỏ nhất có thể đạt được là:
Code mẫu:
vector<long long> basis;
int sz;
void insertVector(long long mask) {
// duyệt các phần tử theo thứ tự giá trị msb giảm dần.
for (int i = 0; i < sz; ++i) {
mask = min(mask, mask ^ basis[i]);
}
if (mask != 0) {
basis.push_back(mask);
sz++;
int i = sz - 1;
// giữ các giá trị msb sao cho vẫn theo thứ tự giảm dần.
while (i > 0 && basis[i - 1] < basis[i]) {
swap(basis[i - 1], basis[i]);
i--;
}
}
}
Phân tích độ phức tạp
Vì các phần tử có kiểu long long nên cần tối đa 64 bit để biểu diễn. Đặt thì độ phức tạp mỗi lần thêm một vector là . Độ phức tạp tổng cộng cho việc thêm vector là .
Để ý rằng trong thuật toán trên chỉ được thêm vào mảng basis khi không có phần tử nào mà . Do đó các giá trị trong là phân biệt. Khi đó có thể cải tiến code như sau:
Code cải tiến
vector<long long> basis;
void insertVector(long long mask) {
for (int i = 0; i < (int) basis.size(); ++i) {
mask = min(mask, mask ^ basis[i]);
}
if (mask != 0) {
basis.push_back(mask);
// chỉ cần thêm vào cuối mảng, không cần giữ giá trị msb theo thứ tự giảm dần.
}
}
Code hoàn chỉnh
#include <bits/stdc++.h>
using namespace std;
vector<long long> basis;
void insertVector(long long mask) {
for (int i = 0; i < (int) basis.size(); ++i) {
mask = min(mask, mask ^ basis[i]);
}
if (mask != 0) {
basis.push_back(mask);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<long long> a(n);
for (int i = 0; i < n; ++i) {
cin >> a[i];
insertVector(a[i]);
}
long long ret = (1LL << basis.size()) - n;
cout << ret << '\n';
return 0;
}
Một số bài tập ví dụ
Codechef - XORCMPNT
Tóm tắt đề bài
Cho trạm điện (được đánh số từ đến ). Ban đầu không có đường nối giữa các trạm. Bạn được cho số nguyên , giữa hai trạm khác nhau sẽ có một đường nối trực tiếp nếu tồn tại chỉ số thõa mãn . Hai trạm được gọi là cùng một thành phần liên thông nếu chúng có đường nối trực tiếp với nhau hoặc gián tiếp qua các trạm khác. Bạn cần xử lý trường hợp. Mỗi trường hợp yêu cầu tìm số thành phần liên thông được tạo thành.
Giới hạn
- .
- .
- .
- .
- Tổng trong tất cả các trường hợp không quá .
Lời giải
- Do nên chúng ta không thể xem xét hết tất cả trạm.
- Giả sử chúng ta có ba trạm như sau: . Khi đó hai trạm và sẽ có đường đi gián tiếp qua trạm và thuộc cùng một thành phần liên thông và .
- Do đó hai trạm bất kì sẽ thuộc cùng một thành phần liên thông nếu , với là một tập con bất kì của . Đặt , suy ra và đặt .
- Xét một trạm bất kì, ta có các trạm cùng một thành phần liên thông với là () sao cho . Có tất cả giá trị khác nhau (bao gồm cả và ) nên sẽ có tất cả trạm như vậy.
- Như vậy mỗi trạm bất kì sẽ liên thông với trạm khác. Cho nên mỗi thành phần liên thông đều có trạm và do đó sẽ có tất cả thành phần liên thông.
Code mẫu
#include <bits/stdc++.h>
using namespace std;
vector<int> basis;
void insertVector(int mask) {
for (int i = 0; i < (int) basis.size(); ++i) {
mask = min(mask, mask ^ basis[i]);
}
if (mask != 0) {
basis.push_back(mask);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
int k, m;
cin >> k >> m;
vector<int> a(m);
for (int i = 0; i < m; ++i) {
cin >> a[i];
}
// xây dựng mảng cơ sở của không gian vector được span bởi {x_1, x_2,..., x_m}.
basis.clear();
for (int i = 0; i < m; ++i) {
insertVector(a[i]);
}
int d = (int) basis.size();
cout << (1 << k) / (1 << d) << '\n';
}
return 0;
}
Phân tích độ phức tạp
Hàm insertVector có độ phức tạp là , với là số chiều của không gian vector được span bởi (kích thước của mảng basis) trong trường hợp này nên . Độ phức tạp tổng cộng .
Maximum XOR over all subsets
Đề bài
Cho tập có phần tử . Hãy cho biết tổng XOR lớn nhất trong tất cả các tập con của là bao nhiêu?
Giới hạn
- .
- .
Lời giải
- Do khá lớn nên ta không thể xét hết tất cả tập con của .
- Nhận xét rằng do nên ta có thể tìm cơ sở của không gian vector được span bởi . Khi đó chỉ cần duyệt hết tất cả tập con của (). Độ phức tạp .
Chứng minh
- Giả sử .
- Do mỗi phần tử trong tập đều được biểu diễn thông qua các phần tử trong () nên xét một tập con bất kì , khi đó cũng sẽ biểu diễn được thông qua các phần tử trong .
Code mẫu
int k = (int) basis.size();
int answer = 0;
// duyệt qua tất cả 2^k tập con.
for (int mask = 0; mask < (1 << k); ++mask) {
int cur = 0; // lưu tổng xor của tập con hiện tại.
for (int i = 0; i < k; ++i) {
if (mask & (1 << i)) {
cur ^= basis[i];
}
}
answer = max(answer, cur);
}
cout << answer << '\n';
Tối ưu hơn
- Chúng ta có thể dùng ý tưởng tham lam bằng cách ưu tiên set cho bit có
indexlớn hơn trongresbằng 1. Vì các bit phía sau dù đều bằng 1 thì vẫn sẽ không tối ưu nếu bit bằng 0: . Độ phức tạp .
int k = (int) basis.size();
// không cần duyệt theo thứ tự giảm dần msb do các giá trị msb trong mảng basis là phân biệt.
int answer = 0;
for (int i = 0; i < k; ++i) {
answer = max(answer, answer ^ basis[i]);
}
cout << answer << '\n';
Codefoces 895C - Square subsets
Tóm tắt đề bài
Cho một mảng gồm số nguyên dương. Tìm số cách khác nhau để chọn ra từ mảng một tập con khác rỗng sao cho tích của các phần tử được chọn là một số chính phương. Hai cách chọn được coi là khác nhau nếu tồn tại một vị trí được chọn bởi tập này mà không được chọn bởi tập kia. Do đáp án có thể rất lớn nên bạn cần in ra đáp án sau khi chia lấy dư cho .
Giới hạn
- .
- .
Lời giải
Bài này có thể giải bằng quy hoạch động bitmask. Nhưng mình sẽ giới thiệu một cách tiếp cận khác đơn giản hơn và độ phức tạp thấp hơn, áp dụng định lí về hạng và số vô hiệu của ma trận.
- Ta biết rằng mọi số nguyên dương bất kì đều được biểu diễn thành tích các thừa số nguyên tố (trong đó là các số nguyên tố, ). là số chính phương khi và chỉ khi các số đều là số chẵn.
- Với mỗi số, chúng ta chỉ cần quan tâm các vị trí mà là số lẻ. Chỉ có số nguyên tố trong đoạn nên có thể coi mỗi số là một vector trong và phép nhân hai số tương đương với phép cộng hai vector tương ứng. Khi đó số chính phương chính là một vector không ().
- Đặt là vector tương ứng của trong . Khi đó ta biểu diễn lại mảng thành một ma trận kích cỡ như sau (các vector được viết ở dạng cột):
- Chúng ta cần tìm tất cả các vector (), trong đó tương ứng với không được chọn và tương ứng với được chọn, sao cho:
-
Số vector thõa mãn chính bằng số vector trong không gian hạch của và bằng (bao gồm cả cách chọn tập rỗng từ ). Khi đó đáp án chính là .
-
Theo định lí về hạng và số vô hiệu:
Dễ dàng suy ra:
Code mẫu
#include <bits/stdc++.h>
using namespace std;
const int MOD = (int) 1e9 + 7;
vector<int> primes, basis;
const int MAX_VAL = 70;
void precompute() {
// Tìm các số nguyên tố <= 70.
for (int i = 2; i <= MAX_VAL; ++i) {
bool is_prime = true;
for (int j = 2; j * j <= i; ++j) {
if (i % j == 0) {
is_prime = false;
break;
}
}
if (is_prime) {
primes.push_back(i);
}
}
}
void insertVector(int mask) {
for (int i = 0; i < (int) basis.size(); ++i) {
mask = min(mask, mask ^ basis[i]);
}
if (mask != 0) {
basis.push_back(mask);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
precompute();
int n;
cin >> n;
vector<int> a(n), cnt(MAX_VAL + 1);
for (int i = 0; i < n; ++i) {
cin >> a[i];
cnt[a[i]]++;
}
// Tìm vector v tương ứng.
for (int i = 0; i <= MAX_VAL; ++i) {
if (!cnt[i]) continue;
int v = 0, cur = i;
for (int j = 0; j < (int) primes.size(); ++j) {
int k = 0;
while (cur % primes[j] == 0) {
cur /= primes[j];
k++;
}
// Nếu k lẻ thì đặt bit thứ j bằng 1.
if (k & 1) {
v += (1 << j);
}
}
insertVector(v);
}
int d = (int) basis.size();
int nullity = n - d;
int answer = 1;
for (int i = 0; i < nullity; ++i) {
answer = answer * 2 % MOD;
}
cout << (answer == 0 ? MOD - 1 : answer - 1) << '\n';
return 0;
}
Độ phức tạp
- Đặt .
- Độ phức tạp là .
Tổng quát
Cho một mảng gồm phần tử và một số nguyên . Tìm số tập con của sao cho tổng xor các phần tử đúng bằng . Giới hạn: .
Phân tích
- Coi mỗi phần tử là một vector thuộc không gian vector . Biểu diễn lại bằng một ma trận kích cỡ như sau:
- Đầu tiên ta cần kiểm tra xem có thể được biểu diễn thông qua các vector hay không. Nếu không thì đáp án là , ngược lại giả sử (với là một tập con bất kì của ).
- Khi đó ta cần tìm số vector () sao cho: Bằng phép biến đổi tương đương ta có được đáp án tương tự.
(Zero XOR Subset)-less
Đề bài
Cho một mảng các số nguyên gồm phần tử. Nhiệm vụ của bạn là chia mảng đã cho thành nhiều nhất các đoạn, sao cho:
- Mỗi phần tử chỉ thuộc một đoạn.
- Mỗi đoạn chứa ít nhất một phần tử.
- Không tồn tại một tập khác rỗng các đoạn sao cho tổng xor các phần tử bằng .
In ra số đoạn nhiều nhất có thể chia thành. Hoặc nếu không tồn tại cách chia.
Giới hạn
- .
- .
Phân tích
- Ta sẽ xem tổng xor của các phần tử trong mỗi đoạn là một vector trong .
- Dễ thấy rằng trong cách chia của đáp án thì tập các vector phải là độc lập tuyến tính.
- Giả sử ta có một cách chia thõa mãn như sau (với số đoạn là nhiều nhất có thể): . Gọi là tổng xor của đoạn thứ . Khi đó ta có tập là độc lập tuyến tính và .
- Theo tính chất của tập độc lập tuyến tính thì tập (với là tổng xor của phần tử đầu tiên) cũng độc lập tuyến tính và .
- Do cách chia trên đã là tối ưu nên ta có thể thêm bất kì nào vào mà vẫn giữ nguyên số vector độc lập tuyến tính ( không đổi).
- Đáp án cuối cùng là . Ngoại trừ trường hợp thì không có cách chia nào nên in ra .
Code mẫu
#include <bits/stdc++.h>
using namespace std;
vector<int> basis;
void insertVector(int mask) {
for (int i = 0; i < (int) basis.size(); ++i) {
mask = min(mask, mask ^ basis[i]);
}
if (mask != 0) {
basis.push_back(mask);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n);
int p = 0; // lưu tổng xor của i phần tử đầu tiên.
for (int i = 0; i < n; ++i) {
cin >> a[i];
p ^= a[i];
insertVector(p);
}
// nếu p_n = 0 thì không có đáp án.
if (p == 0) {
cout << -1 << '\n';
}
else {
cout << basis.size() << '\n';
}
return 0;
}
Độ phức tạp
Độ phức tạp là (với thì ).
Bài tập áp dụng
- Codeforces - Godzilla and Pretty XOR (bạn cần tham gia nhóm tại đây)
- Codeforces - Round 473 - Div.2 - F
- Atcoder - Xor Battle
- Atcoder - Xor Sum 3
- Atcoder - Spices
- Codeforces - Global round 11 - E. Xum
- Hackerearth - Chef & Chutneys
- Atcoder - Xor Query
- Codeforces - Round 635 - Div.1 - E1