线性基知识整理
尽量看吧, 我不是我不想说人话, 是说人话太难了
定义
先给你一堆数, 线性基就是一个集合, 满足集合内的元素能够通过之间的异或操作来得到原序列且这个集合内的个数最少
通俗一点的讲法就是由一个集合构造出来的另一个集合
这样求出来有很多种线性基
定义先了解就行(比较难懂)
求法
我们定义 \(p_i\) 是第一个且第 \(i\) 位上为 \(1\) 的数, 且第 \(i\) 位之前的数为 \(0\)
(二进制下)
( \(x\) 指未知的数)
(求得时候不需要管之后的数)
(最高位为第 \(1\) 位)
注意: 在操作时, 为了方便, 通常让最低位 \(p_1\) , 以此类推(这里纯属是为了方便讲解)
例如:
\(p_1 = 1xxxxx\)
\(p_2 = 01xxxx\)
\(p_3 = 001xxx\)
\(p_4 = 0001xx\)
\(p_5 = 00001x\)
\(p_6 = 000001\)
也可能是
\(p_1 = 1xxxxx\)
\(p_2 = 01xxxx\)
\(p_3 = 000000\)
\(p_4 = 0001xx\)
\(p_5 = 00001x\)
\(p_6 = 000001\)
特别的, 如果所有数在第 \(i\) 都为 \(0\) 那么 \(p_i\) 就没有一个数, 那么 \(p_i\) 就为0
这个集合是有原来的那一堆数求出来的(先从大到小排序, 因为是从最高位开始处理的)
void insert(int x) {
for (int i = 50; i >= 0; i--)
if ((x >> i) & 1) {
if (!p[i]) {p[i] = x, break;}
x ^= p[i];
}
}
为什么要这么写呢?
发现对于每一个 \(x\), 都可以用任意几个 \(p_i\) 表示出来
比如: 假设一个数 \(x\) 为 \(011000\), 且当前 \(p_i\) 数组为
\(p_1 = 110101\)
\(p_2 = 010011\)
\(p_3 = 000000\)
\(p_4 = 000000\)
\(p_5 = 000000\)
\(p_6 = 000000\)
那可以让 \(x \oplus p_2\) 然后呢 \(x\) 就变为了 \(001011\),此时 \(p_3\) 没有值 , 那么 \(p_3\) 为 \(001011\)
那么 \(011000\) 便可以由 \(p_2 \oplus p_3\) 得到
那么 \(p\) 数组的意义就很明白了: \(p\) 数组是由原来的的数组推得而来, 且原来的数都可以被这个集合中的任意数表出
这个地方可以类比一下学过的向量, \(x = (1, 3, 1), y = (1, 1, 1), z = (0, 1, 0)\)
那么 \(x = y + 2 \times z\)
特别的, 线性基与向量的唯一区别就是, 线性基前面不需要乘上一个系数, 因为线性基是基于异或操作的, 所有的数的异或上自己就是 \(0\), 那么用线性基来表示我们原来的数, 那么其中的系数只能是 \(0\) 或者 \(1\).
性质
(以下性质都可以由线性基和 \(p\) 数组的定义简单推得)
- 线性基的元素能相互异或得到原集合的元素的所有相互异或得到的值
证明: 在上面推得原集合的元素一定能被线性基中的数表示出来, 那么原集合的元素的所有相互异或得到的值也能被表示出来
- 线性基没有异或和为 0 的子集
证明: 通过 \(p\) 数组可知, 任意相互异或一定不为 \(0\), 因为 \(p_i\) 是第一个且第 \(i\) 位上为 \(1\) 的数, 且第 \(i\) 位之前的数为 \(0\), 那么就确保了, 在第 \(i\) 位上为 \(1\) 的 \(p\) 至多只有一个, 当第 \(i\) 位为 \(0\) 时, 那么 \(p_i\) 也为 \(0\), 此时 \(p_i\) 对原数组没有贡献, 故不放入线性基内. 所以线性基没有异或和为 0 的子集
3.线性基中每个元素的异或方案唯一,也就是说,线性基中不同的异或组合异或出的数都是不一样的。
证明: 在定义时要求, 集合元素最少, 如果线性基中不同的异或组合异或出的数有一样的, 那么这个数也可以被线性基表示出来, 那么此时,这个数就是多余的, 那么, 据此类推之后, 集合内的数都不可互相表示, 那么线性基中每个元素的异或方案唯一
- 线性基中每个元素的二进制最高位互不相同。
证明: 定义可知.
应用
异或和最大
题目链接: P3812 【模板】线性基
从 \(n\) 个数中选任意个数, 异或和最大
求出线性基之后, 可以分情况讨论.
假设我们求出的线性基长这么个样:
\(p_1 = 1xxxxx\)
\(p_2 = 000000\)
\(p_3 = 001xxx\)
\(p_4 = 0001xx\)
\(p_5 = 00001x\)
\(p_6 = 000001\)
有一个小case: 能让前面为 \(1\) 的数肯定优先让前面的数变为 \(1\).
那就从最高位开始枚举
不知道 \(ans\) 的第 \(i\) 位是 \(1\) 或是 \(0\), 分类讨论 (基于这个前提下:为了让 \(ans\) 最大, 那就要第 \(i\) 位最大, 因为我们是从最高位开始枚举的):
(1) 如果 \(ans\) 为 \(1\) 且 \(p_i\) 为 \(0\), 此时 \(p_i\) 为 \(0\), 那么选不选一样
(2) 如果 \(ans\) 为 \(1\) 且 \(p_i\) 为 \(1\), 那么选了 \(p_i\) 之后, 第 \(i\) 位就变成了 \(0\) 显然不优, 那么就不选 \(p_i\)
(1) 如果 \(ans\) 为 \(0\) 且 \(p_i\) 为 \(1\), 那就要选 \(p_i\)
(1) 如果 \(ans\) 为 \(0\) 且 \(p_i\) 为 \(0\), 此时 \(p_i\) 为 \(0\), 那么选不选一样
那就是简单的判断就好(以下三种一样, 证明就请自己证明吧):
int ask1() {
for (int i = 50; i >= 0; i--)
if (((ans >> i) & 1) ^ (p[i] >> i) & 1) //这个在上面的分类讨论中证过了
ans ^= p[i];
return ans;
}
int ask2() {
for (int i = 50; i >= 0; i--)
if (ans < (ans ^ p[i])) //我们发现当这位为 1 了, 那么 ans^p[i] > ans
ans ^= p[i];
return ans;
}
int ask3() {
for (int i = 50; i >= 0; i--)
if (!((ans >> i) & 1)) // 发现当p[i]的第i 为0时, p[i] 为0, 那么就可以把分类讨论简化掉
ans ^= p[i];
return ans;
}
这样就处理完了
完整代码:
/**
* Author: Aliemo
* Data:
* Problem:
* Time: O()
*/
#include <cstdio>
#include <iostream>
#include <string>
#include <cstring>
#include <cmath>
#include <algorithm>
#define int long long
#define rr register
#define inf 1e9
#define MAXN 60
using namespace std;
inline int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch)) f |= ch == '-', ch = getchar();
while (isdigit(ch)) s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
void print(int x) {
if (x < 0) putchar('-'), x = -x;
if (x > 9) print(x / 10);
putchar(x % 10 + 48);
}
int n, ans;
int p[MAXN];
inline void insert(int x) {
for (int i = 50; i >= 0; i--)
if ((x >> i) & 1) {
if (!p[i]) {p[i] = x; break;}
x ^= p[i];
}
}
inline int ask() {
for (int i = 50; i >= 0; i--)
if (((ans >> i) & 1) ^ ((p[i] >> i) & 1))
ans ^= p[i];
return ans;
}
signed main() {
n = read();
for (rr int i = 1; i <= n; i++) insert(read());
ask();
cout << ans;
}
求一个数能放被表示出来
这个地方就简单了. 放到线性基里面跑一遍就好了, 如果跑完为 \(0\) 就能表出; 否则, 就不能. 具体证明参考性质 \(4\). 因为最高位都互不相同, 那么就可以从高到低为枚举得出结果
bool ask(int x) {
for (int i = 50; i >= 0; i--)
if ((x >> 1) & 1)
x ^= p[i];
return x == 0;
}
线性基合并
只需要把一个线性基插入到另一个线性基中即可
void insert(int x) {
for (int i = 50; i >= 0; i--)
if ((x >> i) & 1) {
if (!p[i]) {p[i] = x, break;}
x ^= p[i];
}
}
void merge() {
for (rr int i = 0; i <= 50; i++)
if (q[i])
insert(q[i]);
}
查询严格次大异或和的值
只需要先求出最大的, 然后从低位向高位枚举, 然后找到两者都为一的异或上, 然后退出即可
int find(int maxx) {
int ans = maxx;
for (int i = 0; i <= 50; i++)
if (((ans >> i) & 1) & ((q[i] >> i) & 1)) {
ans ^= p[i];
break;
}
return ans;
}