线性基总结

说在前头

今天校测遇见线性基,发现我对线性基的了解还非常片面,于是来总结一下。

本篇博客仅总结了线性基的基本性质与应用,不涉及过多证明与习题

概述

一、定义

我们一般说的线性基是一个数集,对于每个数集都会有至少一个线性基,取线性基中的若干数异或起来可以得到原数集的任何一个数,线性基有三个重要性质:

  • 原序列中的任何一个数都可以线性基中的一些数异或得到
  • 线性基没有异或和为\(0\)的子集
  • 线性基是保证前两个性质的基础上数的个数最少的一个

二、构造

我们用\(c\)来表示序列\(a\)的线性基,\(d\)是原序列中的数的最大二进制位位数:

我们先给出构造,再证明其拥有线性基基本性质

inline void insert(ll x){
	for(int j=d;j>=0;--j){
		if((x>>j)&1){
			if(c[j]) x^=c[j];
			else{c[j]=x;break;}
		}
	}
}

由这个构造我们可以得到线性基\(c\)的性质:\(c[i]\not= 0\),则\(c[i]_{(2)}\)的第\(i\)位是\(1\)且是\(c[i]_{(2)}\)的最高位

我们将用它来证明线性基的基本性质:

性质\(1\)

  • 对于任意一个数\(x\),当我们用它插入线性基时,如果它没有被成功插入(即\(c[j]=x\)并没有被执行),说明它被经过的一些\(c\)异或后变成了\(0\),那么这些\(c\)的异或和就是\(x\)
  • 如果它被成功插入了,那么它假设它被插入到了\(c[j]\),那么\(c[j]\)一定是前面一些\(c\)\(x\)异或得到的结果,于是\(c[j]\)与这些\(c\)异或就能得到\(x\)
  • 于是性质\(1\)得证

性质\(2\)

  • 比较显然,因为如果一些\(c\)异或会得到\(0\),那么它们中最后一个被插入的一个不可能被成功插入

性质\(3\)

首先我们证明无论插入线性基的方式是否相同,最终得到的元素个数相同:

  • 如果所有元素都被成功插入,那么无论什么顺序,线性基中元素数量都等于原序列元素个数
  • 否则,设\(x\)未被成功插入,是因为它可以由\(c[i]\otimes c[j]\)得到(数量不影响),那么交换顺序,例如将\(c[j]\)改为最后一个被插入,那么\(c[j]=c[i]\otimes x\)依然无法成功插入,因此最终得到元素个数相同

因此,感性理解一下,我们就得到了结论:线性基中的任何一个数都是必要的,如果删去它,也必须有别的元素替代它的位置,否则一定会有一个数无法被表示,因此满足性质\(3\)

三、基本应用

  • 求原序列所有元素异或得到的最大值

    例题:洛谷模板题

    首先根据线性基基本性质,我们可以把原问题转化为线性基中所有元素异或得到的最大值。

    上面提到过一个性质:\(c[i]_{(2)}\)的第\(i\)位一定是\(1\),因为高位的\(1\)贡献,比低位的任意多个\(1\)贡献都打,于是我们选择从高到低扫过去,贪心选择,即设最大值答案为\(ans\),遍历到第\(i\)位时,如果\(ans\otimes c[i]>ans\),那么就令\(ans=ans\otimes c[i]\)

    • 证明:当我们遍历到第\(i\)位时,有\(c[i]_{(2)}\)的第\(i\)位是\(1\),于是如果\(ans_{(2)}\)的第\(i\)位是\(0\),那么异或之后一定能增大\(ans\),因为此后所有的\(c[i]\)最高位都不高于第\(i-1\)位,因此只会在第\(0-i-1\)位上做贡献,不可能有给第\(i\)位增加\(1\)带来的贡献更大,当\(ans_{(2)}\)的第\(i\)位是\(1\)时,同理,选择不异或\(ans\)一定更优。

    • 代码:

      #include<bits/stdc++.h>
      using namespace std;
      typedef long long ll;
      const int N=51;
      int n;
      ll d[N];
      inline void insert(ll x){
      	for(int i=50;i>=0;--i){
      		if(x&(1ll<<i)){
      			if(d[i]) x^=d[i];
      			else{
      				d[i]=x;
      				return ;
      			}
      		}
      	}
      }
      inline ll getmx(){
      	ll ans=0;
      	for(int i=50;i>=0;--i)
      		if((ans^d[i])>ans) ans=ans^d[i];
      	return ans;
      }
      int main(){
      	scanf("%d",&n);
      	for(int i=1;i<=n;++i){
      		ll x;scanf("%lld",&x);
      		insert(x);
      	}
      	printf("%lld\n",getmx());
      	return 0;
      }
      
  • 求原序列所有元素异或得到的最小值

    在插入线性基时如果有元素无法插入,那么一定意味着一些元素的异或和为\(0\),因此最小值就是\(0\),如果不考虑\(0\),那么问题转化为线性基上所有元素异或得到的最小值,这个显然答案是最小的一个\(c[i]\),因为如果异或了其他数,那么新数的二进制下的最高位一定是别的数的最高位,会更大。

  • 求原序列所有元素能异或得到的值中第\(k\)小的一个

    首先我们对线性基作处理使线性基满足这样一条性质:对于任意\(i\),如果\(c[i]\not= 0\),那么其他的所有\(c\)的二进制下第\(i\)位均为\(0\)

    处理很容易完成,我们从高到低处理每一位,处理到第\(i\)位时,从高到低枚举所有小于\(i\)\(j\),如果\(c[i]_{(2)}\)\(j\)\(=1\),那么就让\(c[i]=c[i]\otimes c[j]\),这样一定不会对高位产生影响并且在\(c[j]\not=0\)时能将\(c[i]_{(2)}\)\(j\)位变成\(0\)

    处理完之后,我们顺带有数组\(p\)存储所有非零的\(c[i]\)以方便进一步处理。

    代码如下:

    inline void rebuild(){
    	tot=0;
    	for(int j=d;j>=0;--j){
    		if(!c[j]) continue;
    		for(int k=j-1;k>=0;--k)
    			if(c[j]&(1ll<<k)) c[j]^=c[k];
    	}
    	for(int j=0;j<=d;++j)
    		if(c[j]) p[tot++]=c[j];	
    }
    

    回到原问题,处理完之后,对于满足\(c[i]\not=0\)\(i\),能对第\(i\)位作出贡献的只有一个数了,于是因为高位的\(1\)贡献更大,那么我们将\(k\)转化为二进制,对于任意的\(j\)满足\(k_{(2)}\)的第\(j\)位等于\(1\),我们就将\(ans\)异或上\(p[j]\)即可。

    inline ll getkth(ll k){
    	ll ret=0;
          rebuild();
    	if(k==1&&tot<n) return 0;
    	if(tot<n) k--;//特判0,如果tot<n则说明原序列可以异或出0
    	for(int j=tot-1;j>=0;--j)
    		if(k&(1ll<<j)) ret^=p[j];
    	return ret;
    }
    

    如果是第\(k\)大,那么就将\(k=2^{tot}-k\)即可,因为大小为\(tot\)的线性基异或出来的不同数数目是\(2^{tot}\),同时\(rebuild\)的结果依然是线性基,所以多次查询时我们可以在建立完线性基后就\(rebuild\)

  • 求原序列所有元素异或得到的不同值中比给定数大的有多少个

    当给定数能由原序列异或得到时,用这个就能求出这个数的排名

    我们依然考虑\(rebuild\)后得到的\(p\)数组,如果第\(j\)位会对\(x\)作出贡献,那么意味着\(2^j\)个数都比\(x\)小,因为\(x\)的这一个二进制位是\(1\)而这\(2^j\)个数的这个位置一定都是\(0\)

    于是我们得到\(ans\)后再用\(2^{tot}-ans\)就能得到答案了,注意这里求出的是\(\ge\)给定数的数的个数,因此要特判给定数自己是否能被表示出来。

    inline ll bigger(ll x){
    	ll ret=0,cur=0;
    	for(int j=tot-1;j>=0;--j){
    		if((cur^p[j])<=x)
    			cur^=p[j],ret|=1ll<<j;
    	}
    	if(cur<x) ret++;
    	return (1ll<<tot)-ret;
    }
    
  • 线性基求交

    引理:若 \(V_1、V_2\) 是线性空间,\(B_1\)\(B_2\) 分别是他们的一组基,令 \(W=B_2 \cap V_1\),若 \(B_1 \cup (B_2 \setminus W)\)线性无关,则\(W\)\(V_1 \cap V_2\)的一组基。

    • 证明:反证法,考虑任意\(v\in V_1\cap V_2\)不能被\(W\)线性表示,那么\(v\)可以同时被\(B_1\)\(B_2\)表示出。因此\(v\)一定可以被\(S\otimes T\)得到,其中\(S\in W,T\in B_2\setminus W\)。那么此时\(T=S\otimes v\),于是\(T\)一定与\(B_1\)线性相关,与我们的假设不符。所以引理成立
    • 于是问题变为了寻找符合条件的\(W\),我们从低位到高位枚举\(B_2\)的基底\(x\)加入\(W\)中,令\(tmp= B_1 \cup (B_2 \setminus W)\),如果\(x\)不能被\(tmp\)表示出来,那么直接加入\(tmp\)中,否则设\(x=S\otimes T\),其中\(S\in B_1,T\in (B_2 \setminus W)\),因为我们是从低到高枚举的,所有\(x\)不可能单独地被某个\(T\)表示,因此一定存在这样的\(S\)\(T\)能表示出\(x\),那么此时\(S=x\otimes T\),于是\(S\)能被\(B_2\)表示出来也能被\(B_1\)表示出来,直接加入\(W\)
    • 关于实现的问题,在\(rebuild\)之后,我们判断一个数能否被表示出来就可以直接去看它各二进制位对应的线性基了
    LB operator &(LB x,LB y){
    	LB A=x,tmp=x,ret;ret.init();
    	for(int i=0;i<=d;++i){
    		if(y.c[i]){
    			ll cur=0,now=y.c[i];
    			for(int j=i;j>=0;--j){
    				if((now>>j)&1){
    					if(tmp.c[j]){
    						now^=tmp.c[j];cur^=A.c[j];
    						if(now) continue;
    						ret.c[i]=cur;
    					}
    					else tmp.c[j]=now,A.c[j]=cur;
    					break;
    				}
    			}
    		}
    	}
    	return ret;
    }
    
  • 线性基求并:求交后容斥即可

四、线性基模板

以上东西的整合:

//-----------LinearBasis----------
struct LB{
	ll c[N],p[N],tot;
	inline void init(){memset(c,0,sizeof(c));}
	inline void insert(ll x){
		for(int j=d;j>=0;--j){
			if((x>>j)&1){
				if(c[j]) x^=c[j];
				else{c[j]=x;break;}
			}
		}
	}
	inline void rebuild(){
		tot=0;
		for(int j=d;j>=0;--j){
			if(!c[j]) continue;
			for(int k=j-1;k>=0;--k)
				if(c[j]&(1ll<<k)) c[j]^=c[k];
		}
		for(int j=0;j<=d;++j)
			if(c[j]) p[tot++]=c[j];	
	}
	inline ll getkth(ll k){
		ll ret=0;
		if(k==1&&tot<n) return 0;
		if(tot<n) k--;
		rebuild();
		for(int j=tot-1;j>=0;--j)
			if(k&(1ll<<j)) ret^=p[j];
		return ret;
	}
	inline ll bigger(ll x){
		ll ret=0,cur=0;
		for(int j=tot-1;j>=0;--j){
			if((cur^p[j])<=x)
				cur^=p[j],ret|=1ll<<j;
		}
		if(cur<x) ret++;
		return (1ll<<tot)-ret;
	}
}a[N],f[(1<<12)+20];
LB operator &(LB x,LB y){
	LB A=x,tmp=x,ret;ret.init();
	for(int i=0;i<=d;++i){
		if(y.c[i]){
			ll cur=0,now=y.c[i];
			for(int j=i;j>=0;--j){
				if((now>>j)&1){
					if(tmp.c[j]){
						now^=tmp.c[j];cur^=A.c[j];
						if(now) continue;
						ret.c[i]=cur;
					}
					else tmp.c[j]=now,A.c[j]=cur;
					break;
				}
			}
		}
	}
	return ret;
}
posted @ 2021-01-21 19:59  cjTQX  阅读(185)  评论(0)    收藏  举报