学习:数学----线性基
线性基主要解决关于一些数的异或等问题,其中包括解决一堆数中任意几个数异或的最大值,最小值,第k大值等等。
线性基介绍及特点
前言
线性基对于萌新来说刚开始学肯定有点难度的,网上很多博客都把线性基讲复杂了(一开始就讲什么线性无关,什么张成),虽然学过线性代数再来理解线性基的确很容易,但是没学过线性代数而来学习线性基却也没有很多很难得地方(至少你知道异或吧)。
所以写这篇博客直接从线性基的特点和作用来讲解,进而来讲其他操作,不会非常涉及到线性代数的某些专业知识。
线性基的组成
当我们有了一组确切的数,我们才能找出这一组数的线性基。故线性基也是一组数,但是注意一点,线性基内可能包含原数组内的数可能也有新的数,这一点在线性基的构造与插入中可以看出。
对于某个确切的数组 $a$ 内的任何一个数 $a[i]$,我们总能在数组 $a$ 的线性基中找到某几个数,使得这某几个数异或起来等于 $a[i]$。你可以把一组数的线性基理解成这一组数的代表数组,也可以理解成这一组数的缩影。
线性基的性质
一组数的线性基内有多少数是固定的,取决于原数组是int类型还是long long类型,如果是int类型,线性基内的数不多余32个,如果是long long类型,线性基内的数不多余64个。
解释:将原数组的每一个数都化为二进制形式,可能一些数的某一个二进制位是1,也有可能是0。而线性基为了表示整个数组,所以线性基中的每一个数都对于原数组数的二进制某个一位值有贡献(如果原数组的数的某一个二进制位都是0,那么线性基内所有的数的这个二进制位也一定是0,这是一种贡献为0的‘贡献’),而int类型(long long类型)二进制位不超过32(64),故线性基内的数的个数不超过32(64)个。
由于线性基内每一个数主要对原数组所有数的二进制的某一位有贡献,故线性基内不存在一些数异或起来等于0
总结:
1:一组数的线性基是不唯一的,但是线性基内数的数量是惟一且最少的。
2:线性基内不存在一些数异或起来等于0。
3:线性基内的元素某几个元素相互异或能异或出原数组内的值已经原数组内相互异或的值(0除外)。
线性基的构造与插入
线性基的构造
前面说的,线性基内的每一个数有且只对原数组所有数的二进制位的某一位有贡献。
首先创建一个大小为60的数组 $base$ 来存原数组 $origin$ 的线性基(我们默认原数组的数都是long long类型),刚开始 $base$ 内的数都为0。
然后我们遍历原数组 $origin$ ,假设我们此时遍历到了$origin[i]$,为了方便,设 $x=origin[i]$
判断x能不能插入线性基了?我们从x的二进制高位往低位遍历(即从第60位往第1位遍历):
如果此时遍历到第 $i+1$ 位,首先判断 x 二进制的第 $i+1$ 位是否为1,即判断 $\text{x&(1ll<<i)}$ 是否等于1($\text{1ll}$ 表示long long类型的数字1)
1.如果不等于1,继续遍历第 $i$ 位;
2.如果等于1,我们判断线性基数组 $base[i]$ 是否等于0,等于0说明线性基此位置之前还没有数插入过,我们就将x插入到 $base[i]$ 中,即令 $base[i]=x$,表示 x 对原数组所有数的二进制第 $i$ 为有贡献;如果 $base[i]$ 处的值不等于0,说明之前已经有数插入到了这个位置,表示有其他说已经对此位做出了贡献,那么x就不需要再做出贡献了,我们就让 $x=x\land base[i]$ 来消除x对二进制第 $i$ 位的贡献,然后对于新的x,我们继续遍历第 $i-1$ 位,看看这个位置能不能插入新的x。
伪代码如下:
//***对于x for(i = 60; i >= 0; i--){ if(x的二进制第i+1位为1成立){ if(base[i]不等于0){ x 异或 base[i] } else{ x 插入到 base[i] 中 跳出循环 } } } //****
对一个长度为 $n$ 的数组 $origin$ 构建线性基 $base$ 的代码:
for(int i = 1; i <= n; i++){ int x = origin[i]; for(int j = 60; j >= 0; j--){ if(x & (1ll << j)){ if(base[j]) x ^= base[j]; else{ base[j] = x; break; } } } }
将原数组的每一个元素尝试插入线性基,就得到原数组的线性基。
线性基的插入
如果你弄懂了线性基的构造,那么你对线性基的插入肯定也是很清楚地。如果你没弄懂线性基的构造,你可以在学习线性基的插入中慢慢领悟线性基的构造。
线性基的构造与线性基的插入是有区别的:如果我们通过线性基的构造来得到了一组数的线性基,那么如果在原数组中插入了一个数,那么此时原数组的线性基是可能会改变了,这就涉及到把一个单个的数尝试插入原有线性基中,也就是线性基的插入。
通过线性基的构造的学习,对于线性基的插入,我们先来讨论一个数能不能插入线性基:
1.如果一个数 $x$ 能插入线性基 $base$:
表示 $x$ 可以有新的贡献,也就是用线性基内的任意多个数都不能异或出 $x$
2.如果一个数 $x$ 能插入线性基 $base$:
表示在线性基内有若干个数,使得他们的异或和等于x,此时的x完全没有贡献,这也体现了为什么线性基内任何几个元素异或都不会为0(任意多个元素异或出x,x再异或x等于0)
以上所述的 $x$ 的贡献:只要 $x$ 对于原数组的数的二进制位任何位有贡献,都可以认为 $x$ 有贡献,从下图可以看出
上图可以看出,要想把x插入到线性基base中,假设线性基此时只有 $base[0],base[2],base[3]$ 不为0,而且x的二进制高位都为0,所以从我们从 $base[3]$来考虑
很明显,x二进制的第 1<<3 的位置是1,当 $x$ 尝试插入 $base[3]$ 的位置时,发现 $x$ 已经有了一个值13,那么x肯定不能插入到 $base[3]$ 的位置,那么就让 $x$ 异或
$base[3]$,使得 $x=15\land 9=(00110)_{(2)}=6$。
同理在插入 $base[2]$ 的时候,需要异或 $base[2]$ ,此时 $x=6\land 5=(011)_{(2)}=3$
当 $x$ 在尝试插入 $base[1]$ 的时候,成功插入这个位置,于是 $base[1]=x=3$
于是,线性基的构造可以看做任意个线性基的插入,代码如下:
void insert(int x){//线性基插入 for(int i = 60; i >= 0; i--){ if(x & (1ll << i)){ if(base[i]) x ^= base[i]; else{ base[i] = x; break; } } } return; }
for(int i = 1; i <= n; i++){//线性基构造 insert(origin[i]); }
甚至可以知道 $origin[i]$ 是否成功的插入了线性基,代码如下:
bool insert(int x){//线性基插入 for(int i = 60; i >= 0; i--){ if(x & (1ll << i)){ if(base[i]) x ^= base[i]; else{ base[i] = x; return true; break; } } } return false; }
求原数组异或和最大值
由于线性基是原数组的代表,求原数组的异或的最大值,相当于求线性基异或的最大值。
用线性基求异或的最大值的优点:原数组的数的个数可能有很多个,如果暴力求的话在实际情况下可能会超时;而线性基内数的个数最多不超过60个,就算是暴力求也可以在很短的时间内求出。
那么如何个暴力法:当然是遍历线性基内每一个数,假设当前算得的异或最大值为 $ans$ ,此时遍历到 $base[i]$ ,如果 $ans\land base[i]>ans$,说明异或上 $base[i]$ 之后,ans可以变大,那么ans的值就可以更新 $ans\land base[i]$
下面逻辑代码:
初始ans = 0; for(遍历线性基内每一个元素base[i]){ if(ans异或上base[i]后变大){ ans异或上base[i]; } } return ans;
代码:
typedef long long ll; ll getMaxXor(){ ll ans = 0; for(int i = 0; i <= 60; i++){ if((ans ^ base[i]) > ans){ ans ^= base[i]; } } return ans; }
求原数组异或和最小值
结论:线性基中最小且不大于0的那个数,就是原数组异或和的最小值;
证明:线性基最小的那个数异或上其他数之后,一定会使得异或和变大,所以线性基最小值就是异或和最小值。
方法:只需从base[0]遍历到base[60],一旦有某一个数大于0,就是异或和最小值。
代码:
ll getMinXor(){ for(int i = 0; i <= 60; i++){ if(base[i] > 0){ return base[i]; } } }
求原数组异或和第k小
由于线性基不唯一,所以我们直接用当前线性基来求第k小时不太可能的(也是没有方法的),为了让线性基变得唯一,我们需要将线性基改造,如下图所示(下图是二进制表示形式)
如上图,先把每一个数拆成二进制,写成一行,就成了一个矩阵。每一行的首非0元素对应的一列中还有其他非0元素,则将两行对应十进制中较大那个数异或较小那个数,使得较大那个数的二进制表示在这一列为0。如此,使得这一列的其他元素都为0为止,为了方便,可以创建一个变量 $cnt$ 来存线性基中非0元素的个数(比如上图中非0元素个数为5,第二行为0)
int build(){ int cnt = 0; for(int i = 0; i < 60; i++){ for(int j = 0; j < i; j++){ if(base[i] & (1ll << j)){ base[i] ^= base[j]; } } if(base[i]) cnt++; } return cnt; }
用上面的操作,让线性基内的元素互相异或,使得线性基内的元素相对最小,可以证明这样的线性基是唯一的。这样的线性基中任意两个数异或,都会增大。
为了得到异或和第k小,如下图所示,给每一行编一个号:
通过上面图,我们知道,用这个线性基异或的最小值一定是第(1)行,第2小就是第(3)行(跳过全0行),但是注意第三小是第(1)行异或第(3)行,第四小是第(4)行,第五小是第(4)行异或第(1)行,依次内推;
于是就有一个规律,我们想要知道第k小,就看k的二进制表示,其中二进制表示中哪几位为1,就哪几行一起异或。比如求第5小 ,其中5的二进制表示为101,本来是第(1)行和第(3)行异或,但第(2)行为0,所以是第(4)行和第(1)行异或。
除此之外,还要注意一点:线性基内的元素不可能异或出0,但是原数组中可能存在异或和等于0的情况。如果原数组中的数都成功插入到线性基中,那么原数组一定不能异或出0,否则一定能异或出0。可以在线性基构造中做一个标记
bool zero = false;//标记 bool insert(int x){//线性基插入 for(int i = 60; i >= 0; i--){ if(x & (1ll << i)){ if(base[i]) x ^= base[i]; else{ base[i] = x; return true; break; } } } zero = true; return false; }
那么,如果线性基构造完毕之后,zero标记为true,说明原数组能异或出0
代码如下:
int build(){ int cnt = 0; for(int i = 0; i < 60; i++){ for(int j = 0; j < i; j++){ if(base[i] & (1ll << j)){ base[i] ^= base[j]; } } if(base[i]) cnt++; } return cnt; } ll getKXor(int k){ if(zero && !--k) return 0;//如果k=1且zero为true,那么异或和第一小就为0 int cnt = build();//改造线性基 if(k >= (1ll << cnt)) return -1;//k超过一定范围就不存在 ll ans = 0; for(int i = 0; i <= cnt; i++){ if(base[i]){//跳过线性基内全0数 if(k & 1){ ans ^= base[i]; } k >>= 1; } } return ans; }
例题
1.2019牛客暑期多校训练营(第一场)----H-XOR:https://blog.csdn.net/weixin_43702895/article/details/97683338