线性基

别问我为什么现在才开始学线性基。。。

看到我们机房巨佬去年10月份就已经切了线性基,简直无地自容。

好了现在进入正题。


线性基

定义:线性基是OI中常用来解决子集异或一类题目的算法。

性质:①线性基能相互异或得到原集合的所有相互异或得到的值。

   ②线性基是满足性质1的最小的集合。

   ③线性基没有异或和为0的子集。

证明:反证法:设线性基S={a1,a2...,an};
  若有子集a1^a2^...^at=0,则a1=a2^a3^...^at,则舍弃a1后一定能通过剩余的元素异或出所有需要a1参与异或的值。设Y=a1^X,因为{a1,a2,...,an}是一组线性基,X一定能由a2...an中相互异或得来。
  Y=a1^X=a2^a3^...^at^X,将X中在a2...at中出现的元素删去,在a2...at中未出现的元素加入,则也能异或得到Y,所以a1于线性基无用,与线性基是最小子集的定义矛盾。
  所以:线性基没有异或和为0的子集。
(以上内容摘自百度百科

应用

我们知道,运用线性基中的一些元素进行异或,可以得到原序列中的任意元素(当然针对性质3,序列中的0需要单独讨论),利用这样的性质,我们就可以解决很多OI中的关于元素异或的问题了!(*^▽^*)

  • 线性基的构造

  我们用a数组来存储原序列中的元素,用b数组来存储线性基中的元素。我们可以尝试将a中的元素插入线性基。

 1 for(int i=1;i<=n;i++)
 2     {
 3         ll x=a[i];
 4         for(int j=50;j>=0;j--)
 5         if(x&(1ll<<j)) //注意当j超过31时要用1ll,不然会爆掉(QAQ我就在这调了好久)
 6         {
 7             if(!b[j]) 
 8             {
 9                 b[j]=x;
10                 break;//插入成功后记得退出哦!
11             }
12             else x^=b[j];
13         }
14     }

对于x而言,每次能进行插入的实质上是它的最高位(二进制下),如果插入成功就可以直接退出,否则与b数组该位的元素进行异或(通过手玩推理可以发现这样插入可以保证能异或出序列中的原元素)。

  • 查询最大值

对于查询序列中能异或出的最大值,我们可以采用贪心的思想,把b数组从大往小枚举,如果能够使当前答案变大,就异或上该b数组中的元素。

1 ll qmax()
2 {
3     ll ans=0;
4     for(int i=50;i>=0;i--) if((ans^b[i])>ans) ans^=b[i];
5     return ans;
6 } 

证明:对于每一个非0的bi,其二进制上的i+1位都为1,如果当前答案异或上改元素大于原答案,证明原答案第i+1位上为0,因为是从大往小枚举,因此异或只会对答案i+1位以后产生影响,且就算后i位全从1变成0也比第i+1位从0变成1的贡献小,故该贪心是正确的。

  • 查询最小值

  考虑b数组各元素最高位不同,故每次由小的b异或大的b都会使小的b变大,且b的值随i值的增大而增大(非零状况下),所以可以分两类考虑:

  ①当原序列能异或出0时,直接输出0;

  ②如果原序列不能异或出0,那么将b数组由i值从小到大枚举,第一个非0的b值即为答案。

1 ll qmin()
2 {
3     if(fl) return 0;//fl==1指能异或出0
4     for(int i=0;i<=50;i++) if(b[i]) return b[i];
5 }
  • 查询第k小

首先,我们要简化一下我们的b数组。

 1 void pre()
 2 {
 3     for(int i=1;i<=50;i++)
 4     {
 5         for(int j=i-1;j>=0;j--)
 6             if(b[i]&(1<<j)) b[i]^=b[j];
 7         if(b[i]) c[cnt++]=b[i];
 8         }    
 9     cnt--;
10 }

大家可以发现,简化完了后,对于二进制上的每一位,b数组中该位上为1的元素个数<=1。而现在的这个b数组与原数组实质上是相同的,都可以通过异或得到原序列的每一个元素。

然后,我们用一个c数组记录b数组中对我们的答案有贡献的元素(即不为0的元素)。

  我们对k进行转化,转化为二进制数。对于k的每一个为1的二进制位(假设为i),我们用c[i]异或上我们当前的答案。

 1 ll qk_th(int k)
 2 {
 3     if(fl) k--;
 4     if(!k) return 0;
 5     if(k>=(1<<cnt)) return -1; 
 6     ll ans=0;int t=0;
 7     while(k)
 8     {
 9         if(k%2) ans^=c[t];
10         k/=2;
11         t++;
12      } 
13     return ans;
14  } 

这样做是正确的。因为c数组是递增的,而且每一个数1的分布不同,可以证明,形如11…比形如10…(假设位数相同)的结果大,位数大的比位数小的大,因此异或出的结果一定是第k大的(当然要对0的情况特判)。注意有贡献的状态有cnt个,最多能异或出的非0的状态为(1<<cnt)-1个,若k大于总状态数,则无解。

(未完待续...)

 
posted @ 2020-01-29 14:21  P-Y-Y  阅读(232)  评论(0)    收藏  举报