异或线性基

题单

模型

模板:洛谷 P3812

问题:给定 \(n(n \le 50)\) 个整数 \(a_i(0 \le a_i \le 2^{50})\),求在这些数中选取任意个,使得他们的异或和最大。

首先有一个暴力的 DP 状态,令 \(dp_{i, j}\) 表示前 \(i\) 个数能否凑出 \(j\),然后转移,时间复杂度:\(O(n \times 2^{50})\),最后求答案。

考虑优化,令 \(X_i\) 为前 \(i\) 个数能凑出的数的集合,如果 \(x\)\(X_i\) 中,\(y\) 也在 \(X_i\) 中,那么显然 \(x \oplus y\) 也在 \(X_i\) 中。反过来如果 \(x\)\(X_i\) 中,\(y\) 不在 \(X_i\) 中,那么 \(x \oplus y\) 肯定不在 \(X_i\) 中,否则 \(y\) 就可以通过 \(x \oplus (x \oplus y)\) 得到。

如果 \(a_{i + 1} \in X_i\),可以忽略 \(a_{i + 1}\) 显然 \(X_{i + 1} = X_i\)。否则 \(X_i\) 中的每个数异化 \(a_{i + 1}\) 后都是一个新元素。这样 \(X_{i + 1}\) 对于 \(X_i\) 大小翻一倍,最多 \(50\) 次,能稍微优化这个算法。


但是 \(X_i\) 还是太大了,能不能压缩一下?要做到这一点,可以发现只有使得 \(X\) 大小翻倍的最多 \(50\) 个元素是重要的,其他的无关紧要,\(X\) 就是这些重要元素 \(a_{b_1}, a_{b_2}, \dots , a_{b_k}\) 的子集异或和所组成的集合。

但是这样,却失去了查询 \(a_{i + 1}\) 是否在 \(X_i\) 的能力,我需要找回它。

如果只直接存储这些元素很难做到这一点。但是我们想要的不是这些元素,而是这些元素的异或和。并且可以发现存储 \(a_{b_1}, a_{b_2}, \dots , a_{b_k}\)\(a_{b_1} \oplus a_{b_2}, a_{b_2}, \dots , a_{b_k}\) 的效果是一样的。推广一下,将存储的数字随意地进行异或操作得到的 \(X\) 都是不变的,因为通过异或操作能将操作后的序列又变回原序列,这个操作是可逆的。

有了这个操作,就可以判断了。可以采取以下策略:对于每个 \(j = 0, 1, 2, \dots 49\),我们让存储的集合内都只有至多一个数最高位为 \(j\),令 \(visxor_j\) 表示最高位为 \(j\) 的这个数字,没有则为 \(0\)。然后判断 \(a_{i + 1}\) 是否在 \(X_i\) 时(插入 \(X_i\) 时),从高到低枚举每一位 \(j\) ,分以下两种情况:

  • \(a_{i + 1}\) 在这一位为 \(0\),不作任何操作(让 \(visxor_{0}\sim visxor_{j - 1}\)\(a_{i + 1}\))。
  • 否则,如果 \(visxor_j = 0\),即这一位没有数字,那么 \(a_{i + 1}\) 不在 \(X_i\) 中,\(visxor_j \leftarrow a_{i + 1}\),退出。否则 \(a_{i + 1} \leftarrow a_{i + 1} \oplus visxor_j\),表示若 \(a_{i + 1} \in X_i\),后面的数要凑出 \(a_{i + 1} \oplus visxor_j\),同时保证了 \(a_{i + 1}\) 在第 \(j\) 位为 \(0\)\(visxor_{0}\sim visxor_{j - 1}\)\(j\) 位均为 \(0\))。

求答案时,同样从大到小枚举 \(j\),如果 \(ans\) 这一位为 \(1\),不做操作。否则 \(ans \leftarrow ans \oplus visxor_j\)

时间复杂度:\(O(50n)\)

void Insert(ll x) { 插入 x
  for (int i = 49; i >= 0; i--) {
    if ((x >> i) & 1) {
      if (!visxor[i]) {
        visxor[i] = x;
        return ;
      }
      x ^= visxor[i];
    }
  }
}

for (int i = 49; i >= 0; i--) {
  if (!((ans >> i) & 1) && visxor[i]) {
     ans ^= visxor[i];
  }
  // x = max(x, x ^ visxor[i]); 简便写法
}

题目1:AT_abc141_f

题意

给定 \(n(2 \le n \le 10^5)\) 个非负整数 \(a_i(0 \le a_i < 2^{60})\),将它们分成两组。记其中一组异或和为 \(x\),令一组异或和为 \(y\),求 \(x+y\) 的最大值。

思路

\(s\)\(n\) 个数的异或和,确定了 \(x\),那么 \(y = s ^ x\)。用线性基求出可以凑出的数集,从高到低考虑第 \(j\) 位的贡献:

  • 如果 \(s\)\(j\) 位为 \(1\),那么 \(x\) 这一位的取值无所谓了,无论如何都是 \(2^j\),插入时将所有数第 \(j\) 位赋成 \(0\)
  • 否则,尽可能让 \(x\) 的第 \(j\) 位为 \(1\),成功就有 \(2^{j + 1}\) 的贡献。如果 \(x\)\(j\) 位本就是 \(1\),不做操作就可以成功。否则如果 \(visxor_j \ne 0\), \(x \leftarrow x \oplus visxor_j\),成功,否则失败。

时间复杂度:\(O(60n)\)

#include <iostream>

using namespace std;
using ll = long long;

const int MAXN = 1e5 + 5, MAXV = 60;

ll x, s, ans, s1, s2, a[MAXN], visxor[MAXV];
int n;

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

int main() {
  ios::sync_with_stdio(0), cin.tie(0);
  cin >> n;
  for (int i = 1; i <= n; i++) {
    cin >> a[i], s ^= a[i];
  }
  for (int i = 1; i <= n; i++) {
    a[i] &= ((1ll << 60) - 1) ^ s; // 将 s 为 0 的位赋为 0
    Insert(a[i]);
  }
  for (int i = 59; i >= 0; i--) {
    if (!((s >> i) & 1)) {
      if (!((s1 >> i) & 1) && visxor[i]) {
        s1 ^= visxor[i];
      }
    }
  }
  cout << s1 * 2 + s;
  return 0;
}

题目2:CF895C

题意

给定长度为 \(n(1 \le n \le 70)\) 的数组 \(a(1 \le a_i \le 70)\),问存在多少种不同的方式从数组元素中选择非空子集,使得它们的乘积为完全平方数。

思路

题意转化:乘积为完全平方数,即这个乘积分解质因数后的每个数都是偶数次幂,即将每个 \(a_i\) 转成一个长度为 \(19\) 的二进制数 \(b_i\),第 \(j\) 位表示 \(a_i\) 分解质因数后第 \(j\) 个质数的幂次是奇数还是偶数。答案为多少种选择方式使得最后的异或和为 \(0\)

考虑答案,把数插入线性基后。不在线性基上的 \(a_i\),无所谓选不选;在线性基上的 \(a_i\) 选法是固定的。答案为 \(2^c - 1\)\(c\) 为不在线性基上的点,注意减去空集

时间复杂度:\(O(19 n)\)

#include <iostream>

using namespace std;
using ll = long long;

const int MAXV = 19, Mod = 1e9 + 7;
const int prime[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67};

int n, c, ans = 1, visxor[MAXV];

void Insert(ll x) {
  for (int i = 18; i >= 0; i--) {
    if ((x >> i) & 1) {
      if (!visxor[i]) {
        visxor[i] = x, c--; // 不在线性基上的点减少一个
        return ;
      }
      x ^= visxor[i];
    }
  }
  return ;
}

int get_prime(int x) { // 求 b[i]
  int ret = 0;
  for (int i = 0; i < 19; i++) {
    int u = 0;
    for (; x % prime[i] == 0; u ^= 1, x /= prime[i]);
    ret += (u << i);
  }
  return ret;
}

int main() {
  ios::sync_with_stdio(0), cin.tie(0);
  cin >> n, c = n;
  for (int i = 1, x; i <= n; i++) {
    cin >> x, Insert(get_prime(x));
  }
  for (; c--; (ans *= 2) %= Mod);
  cout << (ans + Mod - 1) % Mod;
  return 0;
}

posted @ 2024-09-19 23:46  xiehanrui0817  阅读(24)  评论(0)    收藏  举报