为了能到远方,脚下的每一步都不能少.|

异或线性基

题单

模型

模板:洛谷 P3812

问题:给定 n(n50) 个整数 ai(0ai250),求在这些数中选取任意个,使得他们的异或和最大。

首先有一个暴力的 DP 状态,令 dpi,j 表示前 i 个数能否凑出 j,然后转移,时间复杂度:O(n×250),最后求答案。

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

如果 ai+1Xi,可以忽略 ai+1 显然 Xi+1=Xi。否则 Xi 中的每个数异或 ai+1 后都是一个新元素。这样 Xi+1 对于 Xi 大小翻一倍,最多 50 次,能稍微优化这个算法,时间复杂度:O(n+250)


但是 Xi 还是太大了,能不能压缩一下?要做到这一点,可以发现只有使得 X 大小翻倍的最多 50 个元素是重要的,其他的无关紧要,X 就是这些重要元素 ab1,ab2,,abk 的子集异或和所组成的集合。

但是这样,却失去了查询 ai+1 是否在 Xi 的能力,我需要找回它。

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

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

  • ai+1 在这一位为 0,如果 ai+1Xi 里,肯定异或和里不会有 visxorj,不作任何操作。
  • 否则,如果 visxorj=0,即这一位没有数字,那么 ai+1 不在 Xi 中,visxorj=ai+1,退出。否则如果 ai+1Xi 里,肯定异或和里一定有 visxorj,将 ai+1ai+1visxorj,表示后面的数要凑出 ai+1visxorj,同时保证了 ai+1 在第 j 位为 0

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

时间复杂度: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];
  }
}

题目1:AT_abc141_f

题意

给定 n(2n105) 个非负整数 ai(0ai<260),将它们分成两组。记其中一组异或和为 x,令一组异或和为 y,求 x+y 的最大值。

思路

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

  • 如果 sj 位为 1,那么 x 这一位的取值无所谓了,无论如何都是 2j,插入时将所有数第 j 位赋成 0
  • 否则,尽可能让 x 的第 j 位为 1,成功就有 2j+1 的贡献。如果 xj 位本就是 1,不做操作就可以成功。否则如果 visxorj0, xxvisxorj,成功,否则失败。

时间复杂度: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(1n70) 的数组 a(1ai70),问存在多少种不同的方式从数组元素中选择非空子集,使得它们的乘积为完全平方数。

思路

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

考虑答案,把数插入线性基后。不在线性基上的 ai,无所谓选不选;在线性基上的 ai 选法是固定的。答案为 2c1c 为不在线性基上的点,注意减去空集

时间复杂度:O(19n)

#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;
}

本文作者:xiehanrui0817

本文链接:https://www.cnblogs.com/xhr0817-blog/p/18421593

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   xiehanrui0817  阅读(5)  评论(0编辑  收藏  举报
加载中…

{{tag.name}}

{{tran.text}}{{tran.sub}}
无对应文字
有可能是
{{input}}
尚未录入,我来提交对应文字
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起