【学习笔记】 线性基

引入

今天在刷题的时候看到这样一个题:
在n个数中求出异或和的最大值
发现并不是很会
然后学了线性基

算法介绍

若干数的线性基是一组数\(a_1,a_2,...a_n\),其中\(a_x\)的最高位的\(1\)在第\(x\)位。
通过线性基中元素\(xor\)出的数的值域与原来的数\(xor\)出数的值域相同。
设线性基中的元素为\(p_1,p_2,···p_n\)(\(p_i的数在二进制表示下 第i位是该数字的最高位1\))则 \(a_1,a_2,...a_n\) 可用 \(p_1,p_2,···p_n\) 通过\(xor\)运算得出

求线性基 我们考虑把每一个数对线性基的影响分次考虑
有如下代码

插入一个数到线性基中

void ins(ll num){
     for(ll i = 63;i >= 0;i--)//枚举位数
	 if(num & (1ll << i)){ //注意这里的1ll
	    if(!p[i]){p[i] = num;break;}
	    num ^= p[i];
	 }
}

解释一下 我们根据我们的定义 我们从大到小来枚举当前\(num_{(2)}\)\(第i位\) 如果发现\(p_i\)已经有元素了 就让\(num = num xor p_i\) 否则插入当前的\(num\)
我们考虑有这么一个数\(z\) 如果他无法通过我们的方式对线性基产生变化 那么必有\(p_x\ xor\ p_y\ xor\ ······ xor\ z = 0\) 此时\(p_x\ xor\ p_y\ xor\ ······ \ =\ z\)
所以此时\(z\)已经能被线性基中的元素表示了 自然对线性基不产生变化
其实关于线性基的求法 我们还有高斯消元的做法 但笔者认为这里的方法比较简单(明明是你不会高斯消元法

用法

线性基通常有如下三个用法

  1. 查询一个数是否能被集合中其他数异或表示
  2. 求一个集合异或最大/最小值
  3. 求一个集合异或的第k大值

下列给定三种方法的代码

查询一个元素是否可以被异或出来

从高到低,如果这一位为1就异或上这一位的线性基,把1消去,如果最后得到了0,那这个数就可以表示出来。

inline int ask(LL x) {
	for(R int i=62;i>=0;i--) 
		if(x&(1LL<<i)) x^=p[i];
	return x==0;
}

查询一个集合异或最大值

从高到低 贪心异或即可

void get(){//从高到低 贪心选择 
	for(ll i = 63;i >= 0;i--)
	ans = max(ans,ans ^ p[i]); 
}

查询一个集合异或最小值

在集合中有0的情况下 集合异或最小值答案为零
否则为线性基中最小的元素

inline LL askmn() {
	if(zero) return 0;
	for(R int i=0;i<=62;i++)
		if(p[i]) return p[i];
}

求一个集合异或的第k大值

首先考虑,要是每一位的选择都不会影响下一位的话,那就可以直接从高到低按位去选择就行了,就类似于二叉树求rank的玩法。但是我们之前建出来的线性基是没有这个性质的所以我们考虑重构一个数组d来解决这个问题。先上代码:

inline void rebuild() {
	cnt=0;top=0;
	for(R int i=MB;i>=0;i--)
		for(R int j=i-1;j>=0;j--)
			if(p[i]&(1LL<<j)) p[i]^=p[j];
	for(R int i=0;i<=MB;i++) if(p[i]) d[cnt++]=p[i];
}

然后就是按从大到小了

inline LL kth(int k) {
	if(k>=(1LL<<cnt)) return -1;
	LL ans=0;
	for(R int i=MB;i>=0;i--)
		if(k&(1LL<<i)) ans^=d[i];
	return ans; 
}
//考虑一手出现0的情况
printf("%lld\n",k-zero?kth(k-zero):0LL);
posted @ 2020-10-14 20:44  fhq_treap  阅读(1306)  评论(1编辑  收藏  举报