线性基是什么
线性基是一个数的集合。
对于数的集合 A ,A的线性基是指,在 A 中选取任意多个数进行异或,得到的结果的值域,和 A 的线性基中数字异或的值域相同。这样对原数组元素的异或运算就可以转化为线性基中元素的异或运算。但线性基中的元素个数比原数组中的元素个数少得多,因此常用来解决一个数组中元素异或值的问题。
线性基的构造
先来看下面的性质:
设集合 A = { a 1 , a 2 , . . . , a n } A = \{a_1, a_2,...,a_n\} A={a1,a2,...,an},将其中一个元素 a i a_i ai 用 a i ⊕ a j a_i \oplus a_j ai⊕aj( i ≠ j i \neq j i=j)替换,得到集合 A = { a 1 , a 2 , . . . , a i ⊕ a j , . . . , a n } A = \{a_1, a_2, ... ,a_i \oplus a_j,...,a_n\} A={a1,a2,...,ai⊕aj,...,an},则从集合 A 中选取一些值异或得到的结果,都能通过在集合 B 中选取一些值异或得到。
设
x
=
a
k
1
⊕
a
k
2
⊕
.
.
.
.
⊕
a
k
m
x = a_{k1} \oplus a_{k2} \oplus .... \oplus a_{km}
x=ak1⊕ak2⊕....⊕akm
如果这些数不包括
a
i
a_i
ai ,那么这些数也在集合
B
B
B 中。
如果这些数包括
a
i
a_i
ai 那么,
a
i
a_i
ai 可以用
(
a
i
⊕
a
j
)
⊕
a
j
(a_i \oplus a_j) \oplus a_j
(ai⊕aj)⊕aj 替换,所以 x 也可以由集合
B
B
B得到。
线性基还有一个重要性质:所有元素不为0。因此如果原数组异或可能得到0的情况需要特判。
我们可以根据这两个性质,将原数组中的某些元素,用其和另一个元素的异或值代替,不会改变原数组异或值的值域,这样来构造线性基。
如果原数组中的两个数字
A
A
A和
B
B
B的二进制最高位的1在同一位,例如
A
=
18
(
10010
)
,
B
=
24
(
11000
)
A=18 \ (10010),B=24 \ (11000)
A=18 (10010),B=24 (11000),最高位的1都在第5位,那么我们可以用
A
=
A
⊕
B
A = A \oplus B
A=A⊕B 来代替
A
A
A,这样
A
=
10
(
1010
)
,
B
=
24
(
11000
)
A=10 \ (1010),B=24 \ (11000)
A=10 (1010),B=24 (11000),将最高位的1放在不同的位上。对原数组的每个元素进行这样的操作,使得每个元素最高位的1分别处于不同的位上,例如:
a
1
=
10010011
a1 = 10010011
a1=10010011
a
2
=
00101000
a2 = 00101000
a2=00101000
a
3
=
00010111
a3 = 00010111
a3=00010111
a
4
=
00000010
a4 = 00000010
a4=00000010
易知这样处理会将原数组的大小变为
log
2
N
\log_2{N}
log2N,因此线性基的复杂度为
O
(
log
N
)
O(\log{N})
O(logN)。那么消失的元素去了哪里呢?之前说过线性基的元素不为0,是由于线性基中 0 元素没有意义,因此那些大部分异或得到0的元素直接被排除在外了。
求线性基的过程是在线的,也就是说,可以随时在线性基中插入一个新数字,因此实际上我们可以直接边输入边插入,不需要数组 A A A来保存。
#include <cstdio>
#define ll long long int
ll b[101], temp;
//b[i]保存线性基中二进制最高位为第i位的数字
int flag, N; //flag标记原数组异或是否能得到0
void insert(ll x) //在线性基中插入x
{
for(int i=62;i>=0;i--) //从高位向低位扫描 ,2^62大概1e18左右,即ai最大可以达到1e18
{
if(x&(1ll<<i)) //如果x的二进制的第i位是1
{
if(b[i])
x ^= b[i]; //根据性质将x进行变换
else
{
b[i] = x;
return;
}
}
}
flag = 1;
return;
}
int main()
{
scanf("%d", &N);
for(int i=1;i<=N;i++)
{
scanf("%lld", &temp);
insert(temp);
}
return 0;
}
线性基求解的常见问题
求一组数能异或得到的最大值
即在一个数组中,取若干个数,使得它们的异或和最大。
从数组线性基的最高位向低位扫描,贪心地选取可以使结果变大的数字即可。
ll get_max()
{
ll res = 0;
for(int i=62;i>=0;i--)
{
if((res^b[i])>res)
res ^= b[i];
}
return res;
}
求一组数能异或得到的最小值
一个数组能异或得到的最小值,就是线性基中最小的元素,还要特判异或为0的情况。
ll get_min()
{
if(flag) return 0;
for(int i=0;i<=62;i++)
if(b[i])
return b[i];
return 0;
}
求异或第K小值
求异或第K小值,在求得的线性基的基础上需要进行重构,使得线性基每个元素最高位的1,所在的那一位,只有这个元素在这一位的数字为1,其余元素在这一位的数字为0。例如原来的线性基:
p
3
=
10010011
p3 = 10010011
p3=10010011
p
2
=
00101000
p2 = 00101000
p2=00101000
p
1
=
00010111
p1 = 00010111
p1=00010111
p
0
=
00000010
p0 = 00000010
p0=00000010
重构成为:
p
3
=
10000100
p3 = 10000100
p3=10000100
p
2
=
00101000
p2 = 00101000
p2=00101000
p
1
=
00010101
p1 = 00010101
p1=00010101
p
0
=
00000010
p0 = 00000010
p0=00000010
也就是说,某一位至多只有一个元素能影响。
重构后得到的
p
p
p 数组,
p
i
p_i
pi对于异或值结果的贡献为
2
i
2^i
2i,如果我要求异或值第10小,相当于求
p
3
⊕
p
1
p_3 \oplus p_1
p3⊕p1。因为
10
=
2
3
+
2
1
10=2^3+2^1
10=23+21
void rebuild() //对b[]重构
{
for(int i=62;i>=1;i--)
{
if(b[i]) //有第i位为1的数字
{
for(int j=i-1;j>=0;j--)
{
if(b[i]&(1ll<<j))
b[i] ^= b[j];
}
}
}
for(int i=0;i<=62;i++) //得到p[]数组
if(b[i])
p[cnt++] = b[i];
}
ll Kth(ll k) //求异或值第K小
{
if(flag)
k--;
if(k==0)
return 0;
ll res = 0;
if(k>=(1ll<<cnt)) //不存在第K小
return -1;
for(int i=0;i<cnt;i++)
{
if(k&(1ll<<i)) //k有这一位,则这一位的p[i]产生了贡献
res ^= p[i];
}
return res;
}
判断一个数是否能被数组元素异或得到
根据线性基的插入性质,如果一个数字可以插入到数组的线性基中,那么这个数字一定不能被原数组异或得到,反之则能。
一个数字无法插入线性基,则其一定是异或若干个数之后变成了0。
那么就能得到:
x
⊕
a
k
1
⊕
a
k
2
.
.
.
.
.
.
.
=
0
x \oplus a_{k1} \oplus a_{k2}.......=0
x⊕ak1⊕ak2.......=0
所以:
a
k
1
⊕
a
k
2
⊕
.
.
.
.
.
.
.
=
x
a_{k1} \oplus a_{k2} \oplus .......=x
ak1⊕ak2⊕.......=x
所以,如果
x
x
x不能成功插入线性基,一定是因为当前线性基里面的一些数异或起来可以等于
x
x
x。
代码将插入代码稍加修改即可。
例题:
模板(求异或最大值): 洛谷P3812 【模板】线性基.
求异或第K小值: 杭电3949 XOR.
一点思维:洛谷P3857 [TJOI2008]彩灯.