异或和
给定一个大小为 n n n的集合 A A A,其中所有元素相异或的结果为异或和,即 a 1 x o r a 2 x o r a 3 . . . x o r a n a_1 ~~xor~~a_2~~xor~~a_3~~...~~xor~~a_n a1 xor a2 xor a3 ... xor an。
张成
集合 S S S的任何子集的异或和构成的集合称为 S S S的张成,记做 s p a n ( S ) span(S) span(S)。通俗来讲张成就是 S S S中任取若干个数相异或得到的所有可能结果构成的集合。
线性相关与线性无关
对于集合 S S S,若存在一个元素 S i S_i Si使得: S S S删去 S i S_i Si后得到 S ′ S' S′, S i ∈ s p a n ( S ′ ) S_i \in span(S') Si∈span(S′)或者说 S ′ S' S′的张成仍然包含 S i S_i Si。那么称集合 S S S线性相关。通俗理解就是 S i S_i Si能通过 S S S中不包含 S i S_i Si的一个子集的异或和得到。
若 S S S中不存在上述元素 S i S_i Si则称 S S S线性无关。
线性基
给定集合 A , B A,B A,B,若满足:
- B B B中所有元素都能通过 A A A的子集的异或和得到,且 A A A的任何真子集都不可能表示 B B B中的所有元素。
- 集合 A A A是线性无关的。
那么称 A A A是 B B B的线性基。
实现一
构造
在插入过程中,每次找到 x x x的最高位上的 1 1 1,然后把它消去,如果最终全部被消去,则表示要插入的元素已经可以由当前线性基中一些元素的异或和表示出,此时不需要插入,这样保证了线性基是线性无关的。 这样当我们处理完所有数时,显然当前的线性基可以表示插入集合中的所有数。
可以将插入过程理解为高斯约旦消元的过程,矩阵的初等变换不影响向量之间的线性无关性。维护一个对角矩阵(除了对角线上有元素其他位置均无元素)。设线性基为 A = { a i } A = \{a_i\} A={ai},集合中二进制的最高位为 m a x l e n maxlen maxlen,实际上就是对一个 m a x l e n ∗ m a x l e n maxlen*maxlen maxlen∗maxlen的方阵进行高斯约旦消元的过程。对于 x x x从高到低的每一位 i i i:
- 若 a i ≠ 0 a_i \neq 0 ai=0,则 x = x x o r a i x= x ~~xor~~a_i x=x xor ai消去这一位。
- 否则:
- 枚举 j ∈ [ 0 , i ) j \in [0,i) j∈[0,i),若 x x x的第 j j j位为 1 1 1,那么消去这一位。之所以枚举 [ 0 , i ) [0,i) [0,i)是因为如果前面高位有 1 1 1却仍没有被插入,证明前面的位置线性基元素已经被确定。这两个步骤可以看做将某一行除了主元位置都消为 0 0 0。
- 枚举 j ∈ ( i , m a x l e n ] j \in (i,maxlen] j∈(i,maxlen],若 a i a_i ai的第 j j j位为 1 1 1,则消去。这个步骤可以看做是对主元所在列的上方消去可能存在的 1 1 1。
这样构造线性基之后,因为 0 0 0是线性基的空集,因此要引入一个变量判断线性基是否能表示出 0 0 0,如果一个数插入失败,代表当前线性基已经可以表示出它了,那么显然这两个数异或就能得到 0 0 0。此外还用一个集合 a l l all all记录线性基集合中有多少个不为 0 0 0的元素,也可以用 a l l . s i z e ( ) = = n all.size()==n all.size()==n判断是否能表示出 0 0 0。
void build(int n, ll *s) {
isZero = 0;
for (int i = 1; i <= n; i++) {
if (!insert(s[i])) isZero = 1;
}
all.clear();
for (int i = 0; i <= maxlen; i++) {
if (a[i]) all.push_back(a[i]);
}
}
bool insert(ll x) {
for (int i = maxlen; i >= 0; i--) {
if (!(x & (1LL << i))) continue;
if (a[i])
x ^= a[i];
else {
for (int j = 0; j < i; j++) {
if (x & (1LL << j)) x ^= a[j];
}
for (int j = i + 1; j <= maxlen; j++) {
if (a[j] & (1LL << i)) a[j] ^= x;
}
a[i] = x;
return 1;
}
}
return 0;
}
子集最大异或和
求出线性基后只可能有唯一的一个元素的第 i i i位为 1 1 1,那么只需要将所有的线性基求异或和即为答案。
ll queryMax() {
ll ans = 0;
for (int i = 0; i <= maxlen; i++) {
ans ^= a[i];
}
return ans;
}
子集最小异或和
最小的主元就是子集最小异或和,注意特判 0 0 0。
ll queryMin() {
if (isZero) return 0;
for (int i = 0; i <= maxlen; i++) {
if (a[i]) return a[i];
}
}
查询元素是否在线性基表示的值域中
根据线性基的构造规律,如果能插入则不能被表示出来,否则就能被表示出来。实际上也就是拿 x x x每一位 1 1 1和线性基中的每一个元素异或,如果最后得到 0 0 0,那么就可以被表示出来,否则不能被表示出来。
bool ask(ll x) {
for (int i = maxlen; i >= 0; i--) {
if (x & (1LL << i)) x ^= a[i];
}
return x == 0;
}
查询线性基能表示出的第 k k k小的数
设我们已经求出了线性基,那么显然 [ 0 , m a x l e n ] [0,maxlen] [0,maxlen]只会有若干位置为 1 1 1,用集合 a l l all all从小到大保存起来每位不为 1 1 1对应的结果,然后分两种情况讨论:
- 若 0 0 0不会被构造出来,显然若有 c n t cnt cnt位为 1 1 1,那么构成的数的个数为 2 c n t − 1 2^{cnt}-1 2cnt−1。构造方法是二进制的思路:从最小的位开始,如果当前的第 i i i位在 k k k中为 1 1 1,那么异或上这一位对应的线性基元素。
- 若 0 0 0可以被构造出来,这样构造出的数的个数为 2 c n t 2^{cnt} 2cnt。我们如果只用 1 1 1的位是无法异或出 0 0 0的,为了方便先将 k − − k-- k−−,这样相当于从 0 0 0开始是第 0 0 0小,然后仍然安装上面的方法构造,显然能成功构造出 0 0 0。
ll queryKth(ll k) {
if (isZero) k--;
int cnt = all.size();
if (k >= (1LL << cnt)) return -1;
ll ans = 0;
for (int i = 0; i < all.size(); i++) {
if (k & (1LL << i)) ans ^= all[i];
}
return ans;
}
模板
struct LB {
ll a[105];
bool isZero;
int maxlen;
vector<ll> all;
void init(int len) {
maxlen = len, isZero = 0;
memset(a, 0, sizeof a);
}
void build(int n, ll *b) {
for (int i = 1; i <= n; i++) {
if (!insert(b[i])) isZero = 1;
}
all.clear();
for (int i = 0; i <= maxlen; i++) {
if (a[i]) all.push_back(a[i]);
}
}
ll queryMax() {
ll ans = 0;
for (int i = 0; i <= maxlen; i++) {
ans ^= a[i];
}
return ans;
}
ll queryMin() {
if (isZero) return 0;
for (int i = 0; i <= maxlen; i++) {
if (a[i]) return a[i];
}
}
bool ask(ll x) {
for (int i = maxlen; i >= 0; i--) {
if (x & (1LL << i)) x ^= a[i];
}
return x == 0;
}
bool insert(ll x) {
for (int i = maxlen; i >= 0; i--) {
if (!(x & (1LL << i))) continue;
if (a[i])
x ^= a[i];
else {
for (int j = 0; j < i; j++) {
if (x & (1LL << j)) x ^= a[j];
}
for (int j = i + 1; j <= maxlen; j++) {
if (a[j] & (1LL << i)) a[j] ^= x;
}
a[i] = x;
return 1;
}
}
return 0;
}
ll queryKth(ll k) {
if (isZero) k--;
int cnt = all.size();
if (k >= (1LL << cnt)) return -1;
ll ans = 0;
for (int i = 0; i < all.size(); i++) {
if (k & (1LL << i)) ans ^= all[i];
}
return ans;
}
};
实现二
就像高斯约旦消元和高斯消元的区别,如果将求线性基的步骤换做消成上三角矩阵,那么得到的线性基仍然满足线性基的基本性质,只是在运用上稍微麻烦一些。设线性基 A = { a i } A=\{a_i\} A={ai}对于 x x x从高到低的每一位 i i i:
- 若 a i ≠ 0 a_i \neq 0 ai=0,则 x = x x o r a i x= x ~~xor~~a_i x=x xor ai消去这一位。
- 否则就直接令 a i = x a_i = x ai=x
void build(int n, ll *b) {
for (int i = 1; i <= n; i++) {
if (!insert(b[i])) isZero = 1;
}
}
bool insert(ll x) {
for (int i = maxlen; i >= 0; i--) {
if (!(x & (1LL << i))) continue;
if (a[i])
x ^= a[i];
else {
a[i] = x;
return 1;
}
}
return 0;
}
子集最大异或和
和上一种不同,因为主元对应的行后面的位有可能为 1 1 1,会对答案产生影响,因此要从最高位开始,如果答案不会变小就异或。
ll queryMax() {
ll ans = 0;
for (int i = maxlen; i >= 0; i--) {
if ((ans ^ a[i]) > ans) ans ^= a[i];
}
return ans;
}
子集最小异或和
和上一种构造一样。
ll queryMin() {
if (isZero) return 0;
for (int i = 0; i <= maxlen; i++) {
if (a[i]) return a[i];
}
}
查询元素是否在线性基表示的值域中
和上一种构造一样。
bool ask(ll x) {
for (int i = maxlen; i >= 0; i--) {
if (x & (1LL << i)) x ^= a[i];
}
return x == 0;
}
查询线性基能表示出的第 k k k小的数
这种方法需要在构建了线性基后消去每行非主元的 1 1 1,不如上一种方法方便。消去之后和上面一样的思路。