【数论】线性基

12 篇文章 0 订阅
1 篇文章 0 订阅

    所谓线性基,就是线性代数里面的概念。一组线性无关的向量便可以作为一组基底,张起一个线性的向量空间,这个基地又称之为线性基。这个线性基的基底进行线性运算,可以表示向量空间内的所有向量,也即所有向量可以拆成基底的线性组合。
       在ACM领域,线性基主要用来处理有关异或和的极值问题。根据异或按照二进制数位进行的方式,我们可以把一个数字拆成它的二进制表示形式,而这个二进制形式拆成一位一位的,可以用向量来表示。显然,一组线性无关的向量可以张起一个向量空间,我们同样可以考虑构造这样一组数字的二进制形式组成的线性基。在这个线性基里面,我可以通过基底的线性组合、异或运算,表示所有的异或结果。
       要构造这样的线性基,首先要满足一个性质:任取向量组中两个向量a、b,把其中一个替换成a xor b,这组向量线性组合得到的线性空间相同。这个性质对于xor来说是显然的,因为xor偶数具有抵消性。有了这个性质,就可以利用它对加入的向量进行修改。基本思路是:对于一个新加入的数字,从最高为考试往后扫,如果某一位该向量是1,但是之前的向量组中没有一个这一位是1,那么这一位就放如这个数字并且break;如果之前已经有该位为1的,那么该数字异或该位之前对应的数字。最后的结果是,新加入的数字x,要么被添加到某一个二进制位,要么变成0,说明这个数字对应的二进制向量不属于极大线性无关组。
      线性基构造完毕,我们就要求其极值。很多时候我们需要求一堆数字最大的异或和,这个时候就要用到线性基。线性基构造完毕之后,根据它的定义,任何线性基所张的向量空间的所有向量都能够表示为其线性组合。所以我们只需要找,这个线性基中最大的向量即可。做法就是,从高位开始取线性基的每一个基底,如果异或能够使得结果变大,那么就异或。同时,这个还支持有初始值的最大值,因为线性基可以表示所有的向量,所以即使有一个初始值也是能够一起计算最大值的。
 ———————————————— 
版权声明:本文为CSDN博主「alpc_qleonardo」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u013534123/article/details/79875825    

线性基是啥?
       你可以理解为将一个序列处理完之后得到的产物,并且有如下性质(后面有证明):

  1. 原序列里面的任意一个数都可以由线性基里面的一些数异或得到。
  2. 线性基里面的任意一些数异或起来都不能得到0
  3. 线性基里面的数的个数唯一,并且在保持性质一的前提下,数的个数是最少的
  4. 线性基二进制最高位互不相同
  5. 线性基中元素互相异或,异或集合不变
  6. 如果线性基是满的,它的异或集合为[1,2^n−1]


       那么它是怎么构造的呢?


       我们设有一个数组d,表示序列a的线性基,下标从0开始算。对于序列里面的每一个数,我们尝试将它插入到线性基里面去,具体如何插入这里给出伪代码(伪代码好理解呀qwq,为了方便理解,我们设x(2)为x的二进制数):
for i=60 to 0
       if(x(2)的第i+1位为1)
       {
              if(d[i]为0)
              {
                     d[i]=x;
                     break;
              }
              else x=x^d[i];
       }
       据此,我们可以得到一个关于d数组的性质:若d[i]不为0,则d[i](2)的第i+1位为1,并且d[i](2)的最高位就是第i+1位。
       为了更好地进行线性基的讲解,我们要先知道关于异或的一个小性质(巨佬们可以跳过qwq):
       如果满足a^ b^c =0,那么a^ b=c,所以如果a^ b=c,那么a^ c=b
       证明虽然简单但是这里就不给出了,不明白的读者手动模拟一下就明白了


证明性质1


       我们知道了线性基的构造方法后,其实就可以很容易想到如何证明性质1了,我们设原序列里面有一个数x,我们尝试用它来构造线性基,那么会有两种结果——1、不能成功插入线性基;2、成功插入线性基。

分类讨论一下


1、不能成功插入线性基**
       什么时候不能插入进去呢?
       显然就是它在尝试插入时异或若干个数之后变成了0。
       那么就有如下式子:

       x ^ d[a] ^ d[b] ^ d[c] ^...=0

       根据上面的那个小性质,则有:
       d[a]^ d[b] ^ d[c] ^...=x

      所以,如果x不能成功插入线性基,一定是因为当前线性基里面的一些数异或起来可以等于x。

2、可以成功插入线性基
       我们假设x插入到了线性基的第i个位置,显然,它在插入前可能异或若干个数,那么就有:
       x^ d[a] ^ d[b] ^ d[c] ^ …= d[i]
       d[i] ^ d[a] ^ d[b] ^ d[c] ^ …= x
       所以显然,x此时也可以由线性基里面的若干个数异或得到。

综上,性质1得证


再看性质2


       各位大佬肯定认为这是一条显然的性质啊,但为了严谨一点,还是给出证明吧:
       我们使用反证法
       设d[a] ^ d[b] ^ d[c]=0(其中d[c] 比 d[a]  和 d[b] 要更晚被插入线性基)
       那么有d[a] ^ d[b]=d[c]
       ∵ d[c]可以由d[a] ^ d[b] 得到
       ∴ d[c]不可能插入线性基

       故假设不成立,所以线性基中不存在有任何数异或起来可以得到0


最后看性质3


       这个性质被BJWC拿来出过一道题,那题网上几乎找不到证明(都是草草的给出做法然后贴代码),然而如果你熟记这个性质3,那么很快就能想明白那题。(这题在文章末尾会给出)
       那么我来尝试证明性质3(毕竟网上几乎找不到证明作为参考,这里笔者我只能乱推了):

还是没什么卵用地分类讨论一下


1、假如序列里面的所有元素都可以插入到线性基里面
       显然如果是这种情况的话,不管是用什么顺序将序列里的数插入到线性基里,线性基中的元素一定与原序列元素数量相同。所以性质3成立。

2、假如序列里面的一些元素不能插入到线性基里面

我们设x不能插入到线性基里面,那么一定满足形如d[a] ^ d[b] ^ d[c]=x的式子,那我们尝试将插入顺序改变,变成:d[a] 、d[b] 、x、d[c]。那么显然,d[c]是不可能插入成功的,简单的证明:
       ∵ d[a] ^ d[b] ^ d[c]=x 
       ∴ d[a] ^ d[b] ^ x=d[c] (根据上面那条并没有什么卵用的异或性质)
       原来是x 插入不进去,改变顺序后,d[c] 插入不进去,也就是说,对于插入不进去的元素,改变插入顺序后,要么还是插入不进去,要么就是插入进去了,同时另一个原来插入的进去的元素插入不进去了,所以,可以插入进去的元素数量一定是固定的。
       显然,如果你去掉线性基里面的任意一个数,都会使得原序列里的一些(或一个)数无法通过用线性基里的元素异或得到,所以,每一个元素都是必要的,换句话说,这里面没有多余的元素,所以,这个线性基的元素个数在保持性质1的前提下,一定是最少的。

维护

插入

       顺便贴上插入的代码(代码里的ll 是指longlong):

void add(ll x)
{
    for(int i=50;i>=0;i--)
    {
        if(x&(1ll<<i))//注意,如果i大于31,前面的1的后面一定要加ll
        {
            if(d[i])x^=d[i];
            else
            {
                d[i]=x;
                break;//记得如果插入成功一定要退出
            }
        }
    }
}

合并

将一个线性基暴力插入另一个线性基即可。

L_B merge(const L_B &n1,const L_B &n2)
{
    L_B ret=n1;
    for (int i=0;i<=60;i++)
        if (n2.d[i])
            ret.insert(n2.d[i]);
    return ret;
}

查询

存在性

如果要查询x是否存于异或集合中。 
从高位到低位扫描x的为1的二进制位。 
扫描到第i位的时候x=x⊗ai
如果中途x变为了0,那么表示x存于线性基的异或集合中。

 

如何求最大值

   完整的说,是如何求在一个序列中,取若干个数,使得它们的异或和最大。
       首先构造出这个序列的线性基,然后从线性基的最高位开始,假如当前的答案异或线性基的这个元素可以变得更大,那么就异或它,答案的初值为0 00。
       代码如下:

ll ans()
{
    ll anss=0;
    for(int i=50;i>=0;i--)//记得从线性基的最高位开始
    if((anss^d[i])>anss)anss^=d[i];
    return anss;
 }   

  为啥求解是个贪心的过程?
       前面说过,d[i](2)的第i+1位一定为1,联想一下这里,无非就是两种情况:

  1. ans(2)的第i+1位为0                                                                                                                                                                         对于这种情况,ans异或了d[i]之后一定会变大,那么我们就选择异或它。
           可能有人会问,ans异或完d[i]后虽然i+1位变成了1,但是后面的1~i位可能会受到影响啊。
           管它呢~
           无论后面怎么变,就算后面的i位都能变成1,贡献也没有第i+1位变成1的贡献大呀。
           所以我们要优先使最高位尽可能大。
  2. ans(2)的第i+1位为1                                                                                                                                                                         如果ans异或了d[i],那么第i+1位就会变成0,后面怎么异或也补救不了第i+1位变成0 的损失了,按照上面的思想,要优先使最高位尽可能大,所以此时,ans不能异或d[i]。

如何求最小值

       显然的,最小值一定是最小的d[i]。(如果让最小的d[i]去异或其它的d[i]一定会让它变得更大,所以它自己就是最小的)
       代码就懒得贴了不贴了。(别告诉我你不会求最小值!)

long long query_min()
{
    for (int i=0;i<=60;i++)
        if (d[i])
            return d[i];
    return 0;
}

如何求第k小的值

       完整的说,应该是——从一个序列中取任意个元素进行异或,求能异或出的所有数字中第k小的那个。
       首先,要对这个序列的线性基处理一下,对于每一个d[i],枚举j=i  to 1,如果d[ i ](2)的第j位为1,那么d[ i ]异或d[ j−1 ]。
       那么处理完一个线性基之后,应该大致是长这个样子的(x表示0或1):
       1xxxx0xxx0x
                1xxx0x
                       1x
       求解过程:将k先转成二进制,假如k的第i位为1,ans就异或上线性基中第i个元素(注意不是直接异或d[i-1])。
       代码如下:

void work()//处理线性基
{
	for(int i=1;i<=60;i++)
	for(int j=1;j<=i;j++)
	if(d[i]&(1<<(j-1)))d[i]^=d[j-1];
}
ll k_th(ll k)
{
	if(k==1&&tot<n)return 0;//特判一下,假如k=1,并且原来的序列可以异或出0,就要返回0,tot表示线性基中的元素个数,n表示序列长度
	if(tot<n)k--;//类似上面,去掉0的情况,因为线性基中只能异或出不为0的解
	work();
	ll ans=0;
	for(int i=0;i<=60;i++)
	if(d[i]!=0)
	{
		if(k%2==1)ans^=d[i];
		k/=2;
	}
}

 回想上面的线性基处理过程,可以发现,处理完之后,线性基中的元素,作用其实都是提供自己最高位上的1,那么只要使提供出来的1可以和k(2)的每一位上的1对应,那么求出来的ans一定就是第k小的。

       补充:想想就能知道,其实处理完之后的线性基其实也还是原序列的一个线性基,因为依然拥有上面的三个性质,要知道,一个序列的线性基不唯一,只是元素数量唯一而已。

如何判断一个数是否能被当前线性基中的元素异或得到


       把它尝试插入进线性基里面去,假如可以插入,说明不能异或得到,假如插不进去,则说明可以异或得到 (原理然而上面已经讲了)

 

模板

struct L_B{
    long long d[61],p[61];
    int cnt;
    L_B()
    {
        memset(d,0,sizeof(d));
        memset(p,0,sizeof(p));
        cnt=0;
    }
    bool insert(long long val)
    {
        for (int i=60;i>=0;i--)
            if (val&(1LL<<i))
            {
                if (!d[i])
                {
                    d[i]=val;
                    break;
                }
                val^=d[i];
            }
        return val>0;
    }
    long long query_max()
    {
        long long ret=0;
        for (int i=60;i>=0;i--)
            if ((ret^d[i])>ret)
                ret^=d[i];
        return ret;
    }
    long long query_min()
    {
        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];
    }
    long long kthquery(long long k)
    {
        int ret=0;
        if (k>=(1LL<<cnt))
            return -1;
        for (int i=60;i>=0;i--)
            if (k&(1LL<<i))
                ret^=p[i];
        return ret;
    }
}
L_B merge(const L_B &n1,const L_B &n2)
{
    L_B ret=n1;
    for (int i=60;i>=0;i--)
        if (n2.d[i])
            ret.insert(n1.d[i]);
    return ret;
}

模板题
更多好题(难度非递增):
BJWC 2011 元素 | 题解
SCOI 2016 幸运数字 | 题解
TJOI 2008 彩灯 | 题解

 

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值