线性基

线性基是一种擅长处理异或问题的数据结构。

可以用来:

  1. 查询一个数是否可以被一堆数异或出来。
  2. 查询一堆数可以异或出来的最值.
  3. 查询一堆数可以异或出来的第 \(k\) 大值。

以上三点均可以在 \(log\) 级别的复杂度下稳定实现。

线性基本质上是一个通过对二进制的判断来从原集合中取出一些数成为个新的集合。
它有一些性质:

  1. 线性基具有普通集合所具有的性质,即确定性、互异性、无序性。

  2. 线性基中每个数二进制下的 \(1\) 的最高位都是不同的,也就是说线性基中每个元素的二进制位数不同。

  3. 线性基中没有异或和为 \(0\) 的子集。

  4. 线性基中任意多元素的异或和的值域等于原集合中任意多元素的异或和的值域。

  5. 线性基在满足上一个条件的情况下,所包含元素个数是最少的。

  6. 线性基中不同的元素异或出来的值是不同的。

考虑构造一个线性基。假设我们要插入一个数 \(x\)\(x\) 的二进制最高位为 \(d\)

若第 \(d\) 位还未插入值,则插入 \(x\);若 \(d\) 位已经有值 \(y\),则 \(x\) 替换成 \(x \oplus y\),然后 \(d=d-1\) 继续向低位扫描,直到 \(x\)被插入或者变成 \(x=0\)

那为什么当前位有值时需要异或当前位置上的数呢?

因为任何一个数二进制的最高位都是 \(1\), 在同一个位置遇到了已经插入过的数证明两个数最高位相同,显然不满足线性基的性质。此时需异或一下,使要插入的值最高位变成 \(0\),再去判断新的最高位找一个新的位置插入。是为了尽可能使二进制的每一位都可以有 \(1\)。可以证明对答案无影响。

P3812 【模板】线性基

模版题,要求异或和最大。

我们从高位往低位扫,如果当前答案更大就更新答案。

为什么这样可以求出最大异或和?

因为线性基中每个数最高位不同,也就是说每一个位是最高位的情况最多只有一次。假设当前扫到了第 \(i\) 位,如果目前答案中第 \(i\) 位上的答案为 \(0\),且线性基内存在一个元素满足其最高位为第 \(i\) 位,那么显然进行一次异或操作可以使答案更大。(即使比 \(i\) 低的位数全变成 \(1\),对答案的贡献也比第 \(i\) 位由 \(0\)\(1\) 所得的贡献少)

代码中还给出了此题以外的几种线性基操作。

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

const int kmax = 53;

int n;
long long p[kmax];
bool zero; // 记录有无值为0的元素

void Insert(long long x) { // 插入元素
  for (int i = kmax - 1; ~i; i--) {
    if (x & (1ll << i)) {
      if (!p[i]) { // 未出现过
        p[i] = x;
        return;
      }
      x ^= p[i]; // 否则异或
    }
  }
}

bool In(long long x) { // 判断一个数是否能通过线性基表示
  for (int i = kmax - 1; ~i; i--) {
    if (x & (1ll << i)) {
      x ^= p[i];
    }
  }
  return !x;
}

long long Max() { // 最大值
  long long mx = 0;
  for (int i = kmax - 1; ~i; i--) {
    mx = max(mx, mx ^ p[i]);
  }
  return mx;
}

long long Min() { // 最小值
  if (zero) return 0;
  for (int i = 0; i < kmax; i++) {
    if (!p[i]) continue;
    return p[i];
  }
  return -1;
}

int main() {
  scanf("%d", &n);
  for (int i = 1; i <= n; i++) {
    long long x;
    scanf("%lld", &x);
    Insert(x);
  }
  printf("%lld\n", Max());
  return 0;
}

回顾求解线性基的过程,会发现类似于高斯消元。我们尝试借助高斯消元法理解线性基的过程。

设原数组 \(A={a_1,a_2,\cdots,a_n}\),其中所有数的最大的二进制位数为 \(k\)。将所有数用二进制表示,就形成了一个 \(n\)\(k\) 列的 \(01\) 矩阵。把该矩阵当作异或方程组进行求解得到最终矩阵。最终矩阵只包含一个单位矩阵,即只有对角线上是 \(1\)

这里以求解 \(A=\{8, 13, 15\}\)\(A=\{2, 3, 5, 6, 7\}\) 的线性基为例。

\(A=\{8, 13, 15\}\Rightarrow\begin{bmatrix}1000\\1101\\1111\end{bmatrix}\Rightarrow\begin{bmatrix}1000\\0101\\0111\end{bmatrix}\Rightarrow\begin{bmatrix}1000\\0101\\0010\end{bmatrix}\)

\(A=\{2, 3, 5, 6,7\}\Rightarrow\begin{bmatrix}010\\011\\101\\110\\111\end{bmatrix}\Rightarrow\begin{bmatrix}111\\010\\011\\101\\110\end{bmatrix}\Rightarrow\begin{bmatrix}111\\010\\001\\000\\000\end{bmatrix}\Rightarrow\begin{bmatrix}100\\010\\001\\000\\000\end{bmatrix}\)

正确性证明:

  1. \(A\) 中元素的组合可以由最初矩阵中行的组合表示。
  2. 最初矩阵和最终矩阵可以相互转化。
  3. 最终矩阵每行位数不同,满足线性基的性质。

此外,当最终矩阵中有不少于两行全为 \(0\),说明 \(A\) 中元素可以异或出 \(0\)

代码:

void Gauss() {  // 高斯消元求线性基
  int num = 1;
  for (long long i, j = 1ll << 62; j; j >>= 1) {
    for (i = num; i <= n; i++) {
      if (p[i] & j) break;
    }
    if (i <= n) {
      swap(p[i], p[num]);
      for (i = 1; i <= n; i++) {
        if (i == num) continue;
        if (p[i] & j) {
          p[i] ^= p[num];
        }
      }
      num++;
    }
  }
  zero = --num != n;
  n = num;
}

接下来是一些练习题。

P3857 [TJOI2008] 彩灯

就是问原数组能异或出多少种不同的状态。

对原数组求解线性基,由线性基的性质,线性基中每种组合异或出来的状态都是不同的。

设线性基中有 \(k\) 个元素,答案为 \(2^k\)

代码:

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

const int kmax = 53;
const int Mod = 2008;

int n, m;
long long res;
long long num, p[kmax];
char ch[kmax];

void Insert(long long x) {
  for (int i = kmax - 1; ~i; i--) {
    if (x >> i & 1) {
      if (!p[i]) {
        p[i] = x; // 插入线性基
        res++; // 个数累计
        return;
      }
      x ^= p[i];
    }
  }
}

int main() {
  scanf("%d%d", &n, &m);
  for (int i = 1; i <= m; i++) {
    scanf("%s", ch);
    num = 0;
    for (int i = 0; i < strlen(ch); i++) {
      num += (1ll << (n - i)) * (ch[i] == 'O'); // 将字符转化
    }
    Insert(num); // 插入
  }
  res = (1ll << res) % Mod;
  printf("%lld\n", res);
  return 0;
}

P4301 [CQOI2013] 新Nim游戏

先手要胜利,必须让后手无论拿掉哪几堆都无法使异或和等于 \(0\)

这时要用到线性基。

如果一个数没能插入线性基,说明该数能与线性基中一些数异或成 \(0\),这个数先手一定要在第一回合拿走。

先手必然胜利,因此只需考虑如何使第一回合先手拿的火柴数最少。

容易想到将原数组中的元素从大到小插入,这样不能插入的数都尽可能的小,能使总和最小。

代码:

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

const int kmax = 105;
const int kmaxM = 31;

int n, a[kmax];
long long p[kmax];
long long res;

bool Insert(int x) {
  for (int i = kmaxM - 1; ~i; i--) {
    if (x & (1ll << i)) {
      if (!p[i]) {
        p[i] = x;
        return 1; // 插入成功
      }
      x ^= p[i];
    }
  }
  return 0; // 插入失败
}

int main() {
  scanf("%d", &n);
  for (int i = 1; i <= n; i++) {
    scanf("%d", &a[i]);
  }
  sort(a + 1, a + n + 1, [](int x, int y) { return x > y; }); // 从大到小插入
  for (int i = 1; i <= n; i++) {
    if (!Insert(a[i])) { // 若不能插入
      res += a[i]; // 就要拿走
    }
  }
  printf("%lld\n", res);
  return 0;
}

P4570 [BJWC2011] 元素

贪心,将每种矿石根据魔力从大到小插入线性基。

考虑这样插入的正确性。

假设一个子集 \(S\)\(S\) 中的所有元素异或和为 \(0\),那最优情况就是踢出魔力值最小的元素。从大到小排可以完美的解决这个问题。

代码:

#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

const int kmax = 1003;
const int kmaxM = 64;

struct V {
  long long x, w;
} v[kmax];

int n;
long long p[kmaxM], res;

bool Check(long long x) {
  for (int i = kmaxM - 1; ~i; i--) {
    if (x & (1ll << i)) {
      if (!p[i]) {
        p[i] = x;
        return 1;
      }
      x ^= p[i];
    }
  }
  return 0;
}

int main() {
  cin >> n;
  for (int i = 1; i <= n; i++) {
    cin >> v[i].x >> v[i].w;
  }
  sort(v + 1, v + n + 1, [](V p, V q) { return p.w > q.w; });
  for (int i = 1; i <= n; i++) {
    if (Check(v[i].x)) {
      res += v[i].w;
    }
  }
  cout << res;
  return 0;
}

P4151 [WC2011] 最大XOR和路径

将一条路径拆成环和链两部分。选择一条链,考虑用环对其进行增广。

假设一条路径 \(E\) 连接当前链和一个环,那么这条路径会分别从链到环和从环到链经过两次。由异或的性质,这条路径无需计算。

所以任选一条链为起始值,枚举所有环并将异或和插入线性基。最后求最大异或和即可。

代码:

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

const int kmax = 1e5 + 3;
const int kmaxM = 64;

struct E {
  int p, y;
  long long w;
} e[kmax << 1];

int n, m;
int h[kmax], ec;
long long p[kmaxM], res[kmax], ress;
bool b[kmax];

void Addedge(int x, int y, long long w) {
  e[++ec] = {h[x], y, w};
  h[x] = ec;
}

void Insert(long long x) {
  for (int i = kmaxM - 1; ~i; i--) {
    if (x & (1ll << i)) {
      if (!p[i]) {
        p[i] = x;
        return;
      }
      x ^= p[i];
    }
  }
}

void Dfs(int x, int fa, long long c) {
  b[x] = 1;
  res[x] = c;
  for (int i = h[x]; i; i = e[i].p) {
    int y = e[i].y;
    if (y == fa) continue;
    if (!b[y]) {
      Dfs(y, x, c ^ e[i].w);
    } else {
      Insert(c ^ e[i].w ^ res[y]); // 插入线性基
    }
  }
}

int main() {
  scanf("%d%d", &n, &m);
  for (int i = 1, x, y; i <= m; i++) {
    long long w;
    scanf("%d%d%lld", &x, &y, &w);
    Addedge(x, y, w);
    Addedge(y, x, w);
  }
  Dfs(1, 0, 0);
  ress = res[n];
  for (int i = kmaxM - 1; ~i; i--) {
    ress = max(ress, ress ^ p[i]); // 求最大值
  }
  printf("%lld\n", ress);
  return 0;
}

完结撒花 \ / \ / \ /

posted @ 2023-07-07 17:07  ereoth  阅读(227)  评论(0)    收藏  举报