线性代数 —— 线性基与前缀线性基
【概述】
线性基,是线性代数中的概念,在信息学竞赛中,前缀线性基是线性基的扩展,他们主要用于处理有关异或和的极值问题。
一组线性无关的向量即可作为一组基底,张起一个线性的向量空间,这个基底即称为线性基,利用线性基的基底进行线性运算,可表示向量空间内的所有向量,换句话说,所有向量都可以拆成基底的线性组合。
根据异或的原理,将一个数字拆成他的二进制形式,将二进制形式用向量来表示,由于一组线性无关的向量可以张起一个向量空间,因此可以考虑构造这样一组数字的二进制形式组成的线性基,在这个线性基中,通过基底的线性组合、异或运算,从而可以表达所有的异或结果。
简单来说,若一个数集 T 的值域范围为 ,那么 T 的线性基是 T 的一个子集 A={a1,a2,a3,...,an},A 中元素相互异或而成的集合,等价于原数集 T 的元素相互异或形成异或集合
【线性基】
1.构造
线性基具有以下几个性质:
- 原数列中的任意一个数,均可由线性基中的一些数异或得到
- 线性基中的数的个数唯一,且在保证唯一性的前提下,数的个数是最少的
- 线性基的异或集合中,每个元素的异或方案唯一 ,且异或集合中不存在 0
- 线性基中元素互相异或,异或集合不变
- 线性基二进制最高位互不相同
1)插入
由于 xor 偶数具有抵消性,因此可利用这个性质对加入的向量进行修改。
对于每一个数来说,需要判断其是否能加入到线性基中,将新加入的数字 x 拆为二进制形式,从最高位向最低位扫描其为 1 的二进制位,扫描到第 i 位时,如果线性基向量组 d[i] 中不存在,即 d[i]=0 时,就令 d[i]=x,然后 break;反之,若线性基向量组 d[i] 中存在,即 d[i]=1 时,就令 x=x xor d[i]
这样在扫描完成后,新加入的数字 x,要么被添加进线性基,要么经过一系列操作后变成了 0,如果变成了 0,说明这个数字 x 对应的二进制向量不属于极大线性无关组。
LL d[60+5];//线性基
void add(LL x) {
for(int i=60; i>=0; i--) {
if(x&(1LL<<i)) {
if(d[i])//插入失败,异或
x^=d[i];
else {//插入成功,保存后退出
d[i]=x;
break;
}
}
}
}
2)合并
当两个线性基需要合并时,直接将一个线性基暴力的插入另一个线性基即可。
LL d1[60+5],d2[60+5];//合并前的两个线性基
LL d[60+5];//合并后的线性基
void Union(){
memcpy(d,d1,sizeof(d1));
for(int i=0;i<=60;i++){
if(d2[i])
add(d2[i]);
}
}
2.查询
在线性基构造完毕后,即可对线性基进行查询。
1)最大值
当要查询最大值时,就从高位向低位扫描线性基,如果异或后使得结果变大,就异或进答案中。
LL d[60+5];//线性基
LL queryMax() {
LL res=0;
for(int i=60; i>=0; i--)
if((res^d[i])>res)
res^=d[i];
return res;
}
2)最小值
当要查询最小值时,最低位上的线性基即为答案。
LL d[60+5];//线性基
LL queryMin(){
for(int i=0; i<=60; i++)
if(d[i])
return d[i];
return 0;
}
3)第 k 小值
由于线性基二进制最高位互不相同,因此要将线性基改造为每一位相互独立。
从最高位向最低位枚举,如果 i<j,那么 d[j] 的第 i 位是 1,就将 a[j] 异或上 a[i],经过一系列操作后,对于二进制的某一位 i,只有 a[i] 这一位是 1,其余的都是 0
这样在查询的时候,将 k 进行二进制拆分,对于为 1 的位,就异或上对应的线性基,最终得到的就是第 k 小的值
注:如果要求第 k 大的值,也就是求第 n-k+1 小的值,将 queryKth() 的参数 k 换为 n-k+1 即可
LL d[60+5];//线性基
LL p[60+5];//改造后的线性基
int cnt;
void rebuild() {//改造线性基
for(int i=60; i>=0; i--)
for(int j=i-1; j>=0; j--)
if(d[i]&(1LL<<j))
d[i]^=d[j];
for(int i=0; i<=60; i++)
if(d[i])
p[cnt++]=d[i];
}
LL queryKth(LL k){//查询第k小的值,若要查k大,将k换为n-k+1即可
int res=0;
if(k>=(1LL<<cnt))
return -1;
for(int i=60; i>=0; i--)
if(k&(1LL<<i))
res^=p[i];
return res;
}
【前缀线性基】
线性基用于处理整个区间异或的极值问题,前缀线性基用来维护整个区间中的一段区间异或的极值问题。
1.构造
前缀线性基建立在线性基的基础上,其是对于 i 都建立一个 1~i 数组成的线性基。
假设有 4 个数 1、2、3、4,那么就建立 4 个基底,分别为:
- 第一基底:1
- 第二基底:1、2
- 第三基底:1、2、3
- 第四基底:1、2、3、4
在构造 i 个基底时,先将前 i-1 个 基复制过来,然后再加上第 i 个数,来构造一个新的基底。
如果对于基底的某个位置上的数有能更新的数,就进行替换,以保证对于某个右端点 R 能尽可能的用靠近 R 的数作为基,也就保证了所有 [L,R] 中的数都尽可能的参与构造这个数。
例如:对于第五个基的第 3 个位置,如果第 4 个数和第 2 个数都能放在这个位置,那么就放第 4 个数,因为查询区间 [3,5] 时能用 3、4、5 构造这个数,包含了 4;查询区间 [2,5] 时,能用 2、3、4、5 构造这个数
也就是说对于 [2,5] 来说,第 2 个数还是第 4 个数都可以使用,但是 [3,5] 来说,只能使用第 4 个数,因此当第 4 个数和第 2 个数都能放在第 3 个位置时,优先用第 4 个数
void add(int x) { //向线性基中添加x
num++;
for(int i=0; i<32; i++) { //复制前num-1个线性基
d[num][i]=d[num-1][i];
pos[num][i]=pos[num-1][i];
}
int P=num;
for(int i=31; i>=0; i--) {
if((x>>i)&1) {
if(d[num][i]) { //插入失败
if(pos[num][i]<P) { //交换位置
swap(pos[num][i], P);
swap(d[num][i],x);
}
x^=d[num][i];//异或
} else { //插入成功
d[num][i]=x;
pos[num][i]=P;
break;
}
}
}
}
2.查询
在查询 [L,R] 区间时,就找到第 R 个基,然后用 pos[i] 来记录第 i 个位置是哪个数,如果 pos<l,那么就说明不是区间 [L,R] 的基
1)最大值
int queryMax(int l,int r) { //查询[l,r]中的最大值
int res=0;
for (int i=31; i>=0; i--) {
if(pos[r][i]<l)
continue;
if ((res^d[r][i])>res)
res^=d[r][i];
}
return res;
}
2)最小值
int queryMin(int l,int r) {//查询[l,r]中的最小值
for(int i=0; i<=60; i++) {
if(pos[r][i]<l)
continue;
if(d[r][i])
return d[r][i];
}
return 0;
}
【模版】
1.线性基
struct LinearBasis {
int num;//线性基中元素个数
LL d[60+5];//线性基
LL p[60+5];//查询第k小时的改造的线性基
int cnt;//改造线性基用
LinearBasis() {
memset(d,0,sizeof(d));
memset(p,0,sizeof(p));
num=0;
cnt=0;
}
bool add(LL x){
for(int i=60; i>=0; i--) {
if(x&(1LL<<i)) {
if(d[i])//插入失败,异或
x^=d[i];
else {//插入成功,保存后退出
num++;//统计线性基中元素个数
d[i]=x;
break;
}
}
}
return x>0;//x>0插入成功
}
LL queryMax() {//查询最大值
LL res=0;
for(int i=60; i>=0; i--)
if((res^d[i])>res)
res^=d[i];
return res;
}
LL queryMin() {//查询最小值
for(int i=0; i<=60; i++)
if(d[i])
return d[i];
return 0;
}
void rebuild() {//改造线性基
for(int i=60; i>=0; i--)
for(int j=i-1; j>=0; j--)
if(d[i]&(1LL<<j))
d[i]^=d[j];
for(int i=0; i<=60; i++)
if(d[i])
p[cnt++]=d[i];
}
LL queryKth(LL k) {//查询第k小的值,若要查k大,将k换为n-k+1即可
int res=0;
if (k>=(1LL<<cnt))
return -1;
for (int i=60; i>=0; i--)
if (k&(1LL<<i))
res^=p[i];
return res;
}
};
LinearBasis Union(const LinearBasis &rhs1,const LinearBasis &rhs2) {
LinearBasis res=rhs1;
for(int i=60; i>=0; i--)
if(rhs2.d[i])
res.add(rhs1.d[i]);
return res;
}
2.前缀线性基
struct PrefixLinearBasis{
int d[N][32];//前缀线性基
int pos[N][32];//最后一个修改i这个位置的数
int num;//线性基中元素个数
PrefixLinearBasis(){
memset(d,0,sizeof(d));
memset(pos,0,sizeof(pos));
num=0;
}
void clear(){
memset(d,0,sizeof(d));
memset(pos,0,sizeof(pos));
num=0;
}
void add(int x){//向线性基中添加x
num++;
for(int i=0; i<32; i++){//复制前num-1个线性基
d[num][i]=d[num-1][i];
pos[num][i]=pos[num-1][i];
}
int P=num;
for(int i=31; i>=0; i--){
if((x>>i)&1){
if(d[num][i]){//插入失败
if(pos[num][i]<P){//交换位置
swap(pos[num][i], P);
swap(d[num][i],x);
}
x^=d[num][i];//异或
}
else{//插入成功
d[num][i]=x;
pos[num][i]=P;
break;
}
}
}
}
int queryMax(int l,int r){//查询[l,r]中的最大值
int res=0;
for (int i=31; i>=0; i--){
if(pos[r][i]<l)
continue;
if ((res^d[r][i])>res)
res^=d[r][i];
}
return res;
}
int queryMin(int l,int r) {//查询[l,r]中的最小值
for(int i=0; i<=60; i++){
if(pos[r][i]<l)
continue;
if(d[r][i])
return d[r][i];
}
return 0;
}
}PLB;