Skip to content
Minh-Thien Nguyen
Quay lại

XOR Basis

English

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:

Một số khái niệm

Không gian Vector

Một tập VV \ne \varnothing được gọi là một không gian vector nếu V\mathit{V} đượ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 VV được gọi là các vector.

Một số không gian vector đặc biệt:

Độc lập tuyến tính và phụ thuộc tuyến tính

Một tập các vector {v1,v2,,vn}\{v_1, v_2, \ldots, v_n\} được gọi là độc lập tuyến tính nếu phương trình:

x1v1+x2v2++xnvn=Ox_1v_1 + x_2v_2 + \ldots + x_nv_n = O

chỉ có duy nhất một nghiệm tầm thường x1=x2==xn=0x_1 = x_2 = \ldots = x_n = 0.

Ngược lại nếu tồn tại x1,x2,,xnx_1, x_2, \ldots, x_n không đồng thời bằng không, sao cho:

x1v1+x2v2++xnvn=Ox_1v_1 + x_2v_2 + \ldots + x_nv_n = O

thì khi đó được gọi là phụ thuộc tuyến tính.

Không gian vector Z2d\mathbb{Z}_2^d

Z2\mathbb{Z}_2: Zm\mathbb{Z}_m là tập các số dư khi chia lấy dư cho mm và các phép toán trên Zm\mathbb{Z}_m cũng chia lấy dư cho mm. Suy ra Z2\mathbb{Z}_2 là tập các số dư khi chia lấy dư cho 22 do đó Z2={0,1}\mathbb{Z}_2 = \{0, 1\}.

Z2d\mathbb{Z}_2^d: là một không gian vector dd chiều bao gồm tất cả các vector có dd tọa độ, mỗi tọa độ là một phần tử của Z2\mathbb{Z}_2.

Kể từ đây chúng ta chỉ quan tâm đến không gian vector Z2d\mathbb{Z}_2^d, mọi phép tính đều chia lấy dư cho 2 và xem phép \oplus (xor bit) tương đương với phép ++ trong Z2d\mathbb{Z}_2^d.

Bao tuyến tính (Linear span, gọi tắt là span)

Một không gian vector VV được span bởi một tập các vector S={v1,v2,,vn}S = \{v_1, v_2, \ldots, v_n\} chứa tất cả các vector xx được biểu diễn thông qua một tổ hợp tuyến tính của các vector trong SS. span(S)={i=1ncivi|viS,ci{0,1}}\text{span}(S) = \left\{\sum_{i = 1}^n c_i v _i \, \middle| \, v_i \in S, c_i \in \{0, 1\} \right\} lúc này ta nói VV được span bởi SS, VV được sinh bởi SS hay SS sinh ra VV. Chú ý: do chỉ xét trên Z2d\mathbb{Z}_2^d nên cic_i cũng phải thuộc Z2d\mathbb{Z}_2^d. Do đó trong tổ hợp trên mỗi viv_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({2,5})(\{2, 5\}), span({2,5,7})(\{2, 5, 7\}) và span({})(\{\})?

span({2,5})=span({2,5,7})={0,2,5,7}span({})={0}\text{span}(\{2, 5\}) =\text{span}(\{2, 5, 7\}) = \{0, 2, 5, 7\} \\ \text{span}(\{\})= \{0\}

Một số tính chất quan trọng:

Cơ sở (Basis)

Một tập các vector B={v1,v2,,vn}B = \{v_1, v_2, \ldots, v_n\} được gọi là cơ sở của một không gian vector VV nếu span(B)=V(B) = VBBđộc lập tuyến tính. Khi đó nn được gọi là số chiều của VV và kí hiệu là dim(V)\text{dim}(V).

Ví dụ: Xét không gian vector V={0,2,5,7}V = \{0, 2, 5, 7\}, ta có {2,5}\{2, 5\} là một cơ sở của VV nhưng {2,5,7}\{2, 5, 7\} thì không vì tập này là không độc lập tuyến tính do 257=02 \oplus 5 \oplus 7 = 0. Và dĩ nhiên {2,7}\{2, 7\}{5,7}\{5, 7\} cũng là các cơ sở của VV.

Chú ý: số phần tử phân biệt trong không gian vector VV được span bởi một cơ sở B={v1,v2,,vn}B = \{v_1, v_2, \ldots, v_n\}V=2n|V| = 2^n.

Ma trận

Không gian hàng và không gian cột

Xét một ma trận MM kích cỡ n×mn \times m (nn hàng và mm cột):

M=[v1,1v1,2v1,mv2,1v2,2v2,mvn,1vn,2vn,m] M = \begin{bmatrix} v_{1,1} & v_{1,2} & \cdots & v_{1,m} \\ v_{2,1} & v_{2,2} & \cdots & v_{2,m} \\ \vdots & \vdots & \ddots & \vdots \\ v_{n,1} & v_{n,2} & \cdots & v_{n,m} \end{bmatrix} RS(M)=span({row1,row2,,rown})CS(M)=span({col1,col2,,colm})RS(M) = \text{span}(\{row_1, row_2, \ldots, row_n\}) \\ CS(M) = \text{span}(\{col_1, col_2, \ldots, col_m\})

Hạng của ma trận

Người ta chứng minh được rằng dim(RS(M))(RS(M)) == dim(CS(M))(CS(M)) và được kí hiệu là dim(M)(M) hay còn được biết đến là hạng (rank) của ma trận MM, kí hiệu rank(M)(M). (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 MM, kí hiệu là null(M)(M) là tập các vector xRmx \in \mathbb{R}^m (được viết ở dạng cột) sao cho:

Mx=OM \cdot x = O

Giả sử:

M=[v1,1v1,2v1,mv2,1v2,2v2,mvn,1vn,2vn,m],x=[x1x2xm] M = \begin{bmatrix} v_{1,1} & v_{1,2} & \cdots & v_{1,m} \\ v_{2,1} & v_{2,2} & \cdots & v_{2,m} \\ \vdots & \vdots & \ddots & \vdots \\ v_{n,1} & v_{n,2} & \cdots & v_{n,m} \end{bmatrix} ,\quad x = \begin{bmatrix} x_1 \\ x_2 \\ \ldots \\ x_m \end{bmatrix}

Khi đó (x1,x2,,xm)(x_1, x_2, \ldots, x_m) là nghiệm của hệ phương trình:

{v1,1x1+v1,2x2++v1,mxm=0v2,1x1+v2,2x2++v2,mxm=0vn,1x1+vn,2x2++vn,mxm=0 \begin{cases} v_{1, 1}x_1 + v_{1, 2}x_2 + \ldots + v_{1, m}x_m = 0 \\ v_{2, 1}x_1 + v_{2, 2}x_2 + \ldots + v_{2, m}x_m = 0 \\ \ldots \\ v_{n, 1}x_1 + v_{n, 2}x_2 + \ldots + v_{n, m}x_m = 0 \end{cases}

Đặc biệt: nếu ma trận MM kích cỡ n×mn \times m gồm mm vector vZ2nv \in \mathbb{Z}_2^n và vector xZ2mx \in \mathbb{Z}_2^m (các vector vvxx được viết ở dạng cột):

M=[v1v2vm][v1,1v2,1vm,1v1,2v2,2vm,2v1,nv2,nvm,n],x=[x1x2xm] M = \begin{bmatrix} | & | & \ldots & | \\ v_1 & v_2 & \ldots & v_m \\ | & | & \ldots & | \end{bmatrix} \equiv \begin{bmatrix} v_{1,1} & v_{2,1} & \cdots & v_{m,1} \\ v_{1,2} & v_{2,2} & \cdots & v_{m,2} \\ \vdots & \vdots & \ddots & \vdots \\ v_{1,n} & v_{2,n} & \cdots & v_{m,n} \end{bmatrix} ,\quad x = \begin{bmatrix} x_1 \\ x_2 \\ \ldots \\ x_m \end{bmatrix}

Khi đó:

Mx=O{v1,1x1+v2,1x2++vm,1xm0(mod2)v1,2x1+v2,2x2++vm,2xm0(mod2)v1,nx1+v2,nx2++vm,nxm0(mod2) M \cdot x = O \\ \Longleftrightarrow \begin{cases} v_{1, 1}x_1 + v_{2, 1}x_2 + \ldots + v_{m, 1}x_m \equiv 0 \pmod 2 \\ v_{1, 2}x_1 + v_{2, 2}x_2 + \ldots + v_{m, 2}x_m \equiv 0 \pmod 2 \\ \ldots \\ v_{1, n}x_1 + v_{2, n}x_2 + \ldots + v_{m, n}x_m \equiv 0 \pmod 2 \end{cases}

Để ý rằng không gian hạch cũng là một không gian vector, vì:

Ma=O,Mb=OM(a+b)=OMa=OM(ca)=O M\cdot a = O, M \cdot b = O \Longrightarrow M \cdot (a + b) = O \\ M \cdot a = O \Longrightarrow M \cdot (ca) = O

Số vô hiệu (Nullity)

Số vô hiệu của một ma trận MM chính là số chiều của không gian hạch của MM:

nullity(M)=dim(null(M))\text{nullity}(M) = \text{dim}(\text{null}(M))

Đị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 MM kích cỡ n×mn \times m thì:

rank(M)+nullity(M)=soˆˊ cột=m\text{rank}(M) + \text{nullity}(M) = \text{số cột} = m

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 Z2d\mathbb{Z}_2^d 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 aann phần tử phân biệt a1,a2,,ana_1, a_2, \ldots, a_n. Yêu cầu tìm số phần tử ít nhất cần thêm vào mảng aa sao cho điều sau luôn đúng: với mọi x,yx, y thuộc aa thì xyx \oplus y cũng thuộc aa.

Giới hạn

Lời giải

Để ý rằng chúng ta cần thỏa mãn điều kiện x,ya\forall x, y \in a thì xyax \oplus y \in a. Do đó cần phải xây dựng được cơ sở BB của không gian vector VV được span bởi mảng aa. Khi đó đáp án của bài toán chính là 2dim(span(a))n=2Bn2^{\text{dim}(\text{span}(a))} - n = 2^{|B|} - n.

Chúng ta sẽ xét lần lượt từng phần tử như sau:

Phần khó nhất đó chính là làm thế nào để có thể kiểm tra xem aia_i có thể được biểu diễn thông qua các vector trong BB hay không.

msb(5)=msb(7)=2msb(3)=1,msb(1)=0 \text{msb}(5) = \text{msb}(7) = 2 \\ \text{msb}(3) = 1, \text{msb}(1) = 0 26=11010215=0111123=0001121=00001220=101002 \begin{align*} 26 &= 11010_2 \\ 15 &= 01111_2 \\ 3 &= 00011_2 \\ 1 &= 00001_2 \\ 20 &= 10100_2 \end{align*}

Vậy 20=2615120 = 26 \oplus 15 \oplus 120span({26,15,3,1})20 \in \text{span}(\{26, 15, 3, 1\}).

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 d=basis.size()=dim(span(a))64d = basis.size() = \text{dim}(\text{span}(a)) \leq 64 thì độ phức tạp mỗi lần thêm một vector là O(d)\mathcal{O}(d). Độ phức tạp tổng cộng cho việc thêm nn vector là O(nd)\mathcal{O}(n d).

Để ý rằng trong thuật toán trên viv_i chỉ được thêm vào mảng basis khi không có phần tử bjb_j nào mà msb(bj)=msb(vi)\text{msb}(b_j) = \text{msb}(v_i). Do đó các giá trị msb(bj)\text{msb}(b_j) trong BB 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 2K2^K trạm điện (được đánh số từ 00 đến 2K12^K - 1). Ban đầu không có đường nối giữa các trạm. Bạn được cho MM số nguyên x1,x2,,xMx_1, x_2, \ldots, x_M, giữa hai trạm u,vu, v khác nhau sẽ có một đường nối trực tiếp nếu tồn tại chỉ số ii thõa mãn uv=xiu \oplus v = x_i. Hai trạm u,vu, v đượ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ý TT 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

Lời giải

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à O(d)\mathcal{O}(d), với dd là số chiều của không gian vector được span bởi {x1,x2,,xM}\{x_1, x_2, \ldots, x_M\} (kích thước của mảng basis) trong trường hợp này xi<2Kx_i < 2^K nên dKd \leq K. Độ phức tạp tổng cộng O(MK)\mathcal{O}(M K).

Maximum XOR over all subsets

Đề bài

Cho tập SSnn phần tử a1,a2,,ana_1, a_2, \ldots, a_n. Hãy cho biết tổng XOR lớn nhất trong tất cả các tập con của SS là bao nhiêu?

Giới hạn

Lời giải

Chứng minh

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

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 aa gồm nn số nguyên dương. Tìm số cách khác nhau để chọn ra từ mảng aa 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 109+710^9 + 7.

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.

M=[v1v2vn] M = \begin{bmatrix} | & | & \ldots & | \\ v_1 & v_2 & \ldots & v_n \\ | & | & \ldots & | \end{bmatrix} Mx=OM \cdot x = O

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ổng quát

Cho một mảng aa gồm nn phần tử và một số nguyên ss. Tìm số tập con của aa sao cho tổng xor các phần tử đúng bằng ss. Giới hạn: 1n105,0ai<231,0s<2311 \leq n \leq 10^5, 0 \leq a_i < 2^{31}, 0 \leq s < 2^{31}.

Phân tích

(Zero XOR Subset)-less

Đề bài

Cho một mảng các số nguyên aa gồm nn 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:

In ra số đoạn nhiều nhất có thể chia thành. Hoặc 1-1 nếu không tồn tại cách chia.

Giới hạn

Phân tích

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à O(nd)\mathcal{O}(nd) (với ai109a_i \leq 10^9 thì d=basis.size()30d = basis.size() \leq 30).

Bài tập áp dụng

Các nguồn tham khảo


Chia sẻ bài viết:

Bài trước
Auxiliary Tree/Virtual Tree (cây phụ/cây ảo)