线性基

本文介绍了线性基的定义,即数集值域范围为[1,2n−1]时,其线性基是子集且元素异或集合等价于原数集。阐述了线性基的性质,如异或集合无0等。还说明了线性基的维护方法,包括插入、合并、查询等,并给出多个例题及分析,如BZOJ2460、hdu3949等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

感谢大佬博客:https://blog.youkuaiyun.com/qaq__qaq/article/details/53812883

定义
设数集TT的值域范围为[1,2n−1][1,2n−1]。 
TT的线性基是TT的一个子集A={a1,a2,a3,...,an}A={a1,a2,a3,...,an}。 
AA中元素互相xor所形成的异或集合,等价于原数集TT的元素互相xor形成的异或集合。 
可以理解为将原数集进行了压缩。


性质
1.设线性基的异或集合中不存在00。 
2.线性基的异或集合中每个元素的异或方案唯一,其实这个跟性质1是等价的。 
3.线性基二进制最高位互不相同。 
4.如果线性基是满的,它的异或集合为[1,2n−1][1,2n−1]。 
5.线性基中元素互相异或,异或集合不变。

维护

插入

如果向线性基中插入数x,从高位到低位扫描它为1的二进制位。 
扫描到第i时,如果ai不存在,就令ai=x,否则x=x⊗ai。 
x的结局是,要么被扔进线性基,要么经过一系列操作过后,变成了0。

bool insert(ll val){
        for(int i=63;i>=0;--i){///最多搞63,不能搞64,1ll<<64爆longlong了
            if(val&(1ll<<i)){
                if(!d[i]){
                    d[i]=val;///如果第i位的线性基不存在则插入
                    break;
                }
                val^=d[i];///如果第i位已经被占则寻找该值插入的下一个位置
            }
        }
        return val>0;///如果val==0则表示未插入线性基中
    }

合并

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

Linner_base merge(const Linner_base& a,const Linner_base& b)///合并线性基
    {
        Linner_base res=a;
        for(int i=0;i<=63;i++){
            if(b.d[i]){
                res.insert(b.d[i]);
            }
        }
        return res;
    }

查询 

存在性

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

最大值

从高位到低位扫描线性基。 
如果异或后可以使得答案变大,就异或到答案中去。

ll query_max(ll res=0)///如果不含初值res=0,否则res=初值
    {
        for(int i=63;i>=0;--i){
            res=max(res^d[i],res);
        }
        return res;
    }

最小值

最小值即为最低位上的线性基。如果含有初值,则与最大值类似

ll query_min(ll res=0)
    {
        if(!res){///无初值
            for(int i=0;i<=63;++i)
                if(d[i]){
                    res=d[i];
                    break;
                }
        }
        else {///有初值
            for (int i = 0; i <=63; ++i) {
                res = min(res ^ d[i], res);
            }
        }
        return res;
    }

第K小

根据性质3。 
我们要将线性基改造成每一位相互独立。 
具体操作就是如果i<j,aj的第i位是1,就将aj异或上ai。 
经过一系列操作之后,对于二进制的某一位i。只有ai的这一位是1,其他都是0。 
所以查询的时候将k二进制拆分,对于1的位,就异或上对应的线性基。 
最终得出的答案就是k小值。

 void rebuild()///使每个基底a[i]都只是第i位为1,其余均为0
    {
        for(int i=63;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<=63;i++){
            if(d[i]){
                nd[tot++]=d[i];
            }
        }
    }
ll query_kthmin(ll k)///求第k小
        {
            ll ans=0;
            if(k>=(1ll<<tot)) return -1;
            for(int i=0;i<=63;++i){
                if(k&(1ll<<i)){
                    ans^=nd[i];
                }
            }
            return ans;
    }

例题1:BZOJ2460

题意:有N个元素,每个元素有a,b两种属性,要求N个中选任意多个元素,使a异或起来不为0,b加起来最大。

分析:首先考虑b加起来最大,对N个元素按b进行从大到小排序,依次插入线性基,由于线性基的所以基底异或不会为0,刚好满足a的限制,最后把所有能插入线性基的元素的b加起来即可。

Ac code:

#include<cstdio>
#include <iostream>
#include <cmath>
#include <stack>
#include <vector>
#include <algorithm>
#include <set>
#include <queue>
#include <cstring>
 
using namespace std;
const int maxn = 1e3 + 5;
typedef long long ll;
struct Node{
    ll id;
    int val;
    bool operator<(const Node& a)const{
        return val>a.val;
    }
}a[maxn];
struct Linner_base{
   ll d[65];
   void init()
   {
       memset(d,0,sizeof d);
   }
   bool insert(ll id)
   {
       for(int i=63;i>=0;--i){
           if(id&(1ll<<i)){
               if(!d[i]){
                   d[i]=id;
                   break;
               }
               id^=d[i];
           }
       }
       return id>0;
   }
}LB;
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i].id>>a[i].val;
    sort(a+1,a+n+1);
    ll ans=0;
    for(int i=1;i<=n;i++){
        if(LB.insert(a[i].id)){
            ans+=a[i].val;
        }
    }
    cout<<ans<<endl;
    return 0;
}


 

例题2:hdu3949

题意:给你n个数,从中选任意多个数异或起来,问所有能组成的数中的第k小的数是多少

分析:线性基求第K小板子题,关键在与是否可以产生0的判断。

Ac code:

#include<bits/stdc++.h>

using namespace std;
typedef long long ll;
const int maxn=1e4+5;
struct Linner_base{
    ll d[65],nd[65];
    int tot;
    void init(){
        tot=0;
        memset(d,0,sizeof d);
        memset(nd,0,sizeof nd);
    }
    bool insert(ll val){
        for(int i=63;i>=0;--i){///最多搞63,不能搞64,1ll<<64爆longlong了
            if(val&(1ll<<i)){
                if(!d[i]){
                    d[i]=val;
                    break;
                }
                val^=d[i];
            }
        }
        return val>0;
    }
    void rebuild()///使每个基底a[i]都只是第i位为1,其余均为0
    {
        for(int i=63;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<=63;i++){
            if(d[i]){
                nd[tot++]=d[i];
            }
        }
    }
    ll query_kthmin(ll k)///求第k小
        {
            ll ans=0;
            if(k>=(1ll<<tot)) return -1;
            for(int i=0;i<=63;++i){
                if(k&(1ll<<i)){
                    ans^=nd[i];
                }
            }
            return ans;
    }
}LB;
int main()
{
    ios::sync_with_stdio(0),cin.tie(0);
    int t,n,q;
    cin>>t;
    ll x;
    int cas=0;
    while(t--){
        cout<< "Case #" << ++cas<< ":" <<endl;
        cin>>n;
        LB.init();
        bool flag=0;
        for(int i=1;i<=n;i++){
            cin>>x;
            if(!LB.insert(x))///判断是否可生成0
                flag=1;
        }
        LB.rebuild();
        ll k;
        cin>>q;
        while(q--){
            cin>>k;
            if(!flag) cout<<LB.query_kthmin(k)<<endl;
            else{
                cout<<LB.query_kthmin(k-1)<<endl;
            }
        }
    }
    return 0;
}

总板子:

struct Linner_base{
    ll d[65],nd[65];
    int tot;
    void init(){
        tot=0;
        memset(d,0,sizeof d);
        memset(nd,0,sizeof nd);
    }
    bool insert(ll val){
        for(int i=63;i>=0;--i){///最多搞63,不能搞64,1ll<<64爆longlong了
            if(val&(1ll<<i)){
                if(!d[i]){
                    d[i]=val;
                    break;
                }
                val^=d[i];
            }
        }
        return val>0;
    }
    void rebuild()///使每个基底a[i]都只是第i位为1,其余均为0
    {
        for(int i=63;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<=63;i++){
            if(d[i]){
                nd[tot++]=d[i];
            }
        }
    }
    Linner_base merge(const Linner_base& a,const Linner_base& b)///合并线性基
    {
        Linner_base res=a;
        for(int i=0;i<=63;i++){
            if(b.d[i]){
                res.insert(b.d[i]);
            }
        }
        return res;
    }
    ll query_max(ll res=0)
    {
        for(int i=63;i>=0;--i){
            res=max(res^d[i],res);
        }
        return res;
    }
    ll query_min(ll res=0)
    {
        if(!res){///无初值
            for(int i=0;i<=63;++i)
                if(d[i]){
                    res=d[i];
                    break;
                }
        }
        else {///有初值
            for (int i = 0; i <=63; ++i) {
                res = min(res ^ d[i], res);
            }
        }
        return res;
    }
    ll query_kthmin(ll k)///求第k小
    {
        ll ans=0;
        if(k>=(1ll<<tot)) return -1;
        for(int i=0;i<=63;++i){
            if(k&(1ll<<i)){
                ans^=nd[i];
            }
        }
        return ans;
    }
}LB;

 例题:https://ac.nowcoder.com/acm/contest/881/H

参考博客:https://www.cnblogs.com/Yinku/p/11212303.html

题意:给定n个整数,求其中异或和为 0 的子集的大小的和。

关键解题思路:首先转化为每个可以通过异或表示 00 的数贡献它参与的子集数。而不是直接求每个子集的大小,这样很不好求。

分析:

首先转化为每个可以通过异或表示0的数贡献它参与的子集数。

对n个数进行一次消元,得到一组大小为r的线性基B1。

那么剩下的n−r个满足以下的规律:

设现在要计算的元素为X1,其他n−r−1个元素任意组合,总能得到一个整数,再加入X1也是一个整数,这个整数能被线性基B1表示,则这个X1可以对他们贡献总计2^{n-r-1}个次。

接下来算线性基B1里的数的贡献。

再对这n−r个数进行一次消元, 得到另一组线性基B2。枚举B1的一个线性基X2,对其他的r-1+B2个数消元,得到某个线性基B3。

Ac code:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e5+10;
const ll mod=1e9+7;
struct Linner_base{
    ll d[64];
    int r;
    void init(){
        r=0;
        memset(d,0,sizeof d);
    }
    void Copy(Linner_base &b){
        r=b.r;
        memcpy(d,b.d,sizeof b.d);
    }
    void insert(ll val){
        for(int i=63;i>=0;--i){
            if(val&(1ll<<i)){
                if(!d[i]){
                    d[i]=val;
                    ++r;
                    break;
                }
                val^=d[i];
            }
        }
    }
    bool check(ll val)
    {
        for(int i=63;i>=0;--i){
            if(val&(1ll<<i)){
                if(!d[i]){
                    return 0;
                }
                val^=d[i];
            }
        }
        return 1;
    }
}B1,B2,B3;
ll Pow(ll a,ll b)
{
    ll ans=1,base=a;
    while(b){
        if(b&1)
            ans=(ans*base)%mod;
        base=(base*base)%mod;
        b>>=1;
    }
    return ans;
}
ll a[maxn];
vector<int>id;
int main()
{
    int n;
    while(~scanf("%d",&n)){
        B1.init(),B2.init(),B3.init();
        id.clear();
        for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
        for(int i=1;i<=n;i++){
            if(B1.check(a[i])){///插不进B1就插入B2中
                B2.insert(a[i]);
            }
            else{///先把能插入B1的插入B1
                B1.insert(a[i]);
                id.push_back(i);
            }
        }
        ll ans=0,p;
        if(n>B1.r){///第一步:求线性基B1外的每个数在出现在异或为0的子集中的次数
            p=Pow(2,n-B1.r-1);
            ans=(ans+p*(n-B1.r))%mod;
        }
        for(auto &i:id){///方法:考虑B1中的每个数x,若去掉x后的B1和B2合起来组成B3,判断x是否在B3中,如果在就加上x的贡献
            B3.Copy(B2);
            for(auto &j:id){
                if(i!=j) B3.insert(a[j]);
            }
            if(B3.check(a[i])){///第二步:求线性基内部每个数出现在异或为0的子集中的次数
                ans=(ans+p)%mod;
            }
        }
        printf("%lld\n",ans);
        //fflush(stdout);
    }
    return 0;
}

例题:http://acm.hdu.edu.cn/showproblem.php?pid=5544

题意:给你一张图,N个点,M条边,每条边都有一条权值,1号结点是起点,从起点开始,最后回到起点,求经过的边的权值异或最大和。

注意:一条边被走多次也会被异或多次

分析:由于从1出发又回到1,不在环上的边相当于没走,故问题就转化为了从多个环中选任意个环进行异或,使得值最大,显然直接用线性基即可。

Ac code:

#include <bits/stdc++.h>
using namespace std;
const int N=5e4+10;
const int M=1e5+10;
typedef long long ll;
int head[N];
struct Edge{
    int v,nxt;
    ll w;
}edge[M<<1];
int tot,n;
ll ans,val[N];
bool vis[N];
struct Linner_base{
    ll d[64];
    void init(){
        memset(d,0,sizeof d);
    }
    bool insert(ll val)
    {
        for(int i=63;i>=0;--i){
            if(val&(1ll<<i)){
                if(!d[i]){
                    d[i]=val;
                    break;
                }
                val^=d[i];
            }
        }
        return val>0;
    }
    ll query_max(ll res=0)
    {
        for(int i=63;i>=0;--i)
            res=max(res,res^d[i]);
        return res;
    }
}LB;
void addedge(int u,int v,ll w)
{
    edge[tot].v=v;
    edge[tot].w=w;
    edge[tot].nxt=head[u];
    head[u]=tot++;
}
void init()
{
    tot=0;
    memset(head,-1,sizeof head);
    memset(vis,0,sizeof vis);
    memset(val,0,sizeof val);
}
void dfs(int u,ll sum,int pre)
{
    vis[u]=1;
    val[u]=sum;///存第一次走到u点的sum值
    for(int i=head[u];~i;i=edge[i].nxt){
        int v=edge[i].v;
        ll w=edge[i].w;
        if(v==pre) continue;
        if(!vis[v]) dfs(v,sum^w,u);
        else{
            LB.insert(sum^w^val[v]);
        }
    }
}
int main()
{
    int t,m;
    scanf("%d",&t);
    int cas=0;
    while(t--){
        init();
        LB.init();
        scanf("%d%d",&n,&m);
        int u,v;
        ll w;
        while(m--){
            scanf("%d%d%lld",&u,&v,&w);
            addedge(u,v,w);
            addedge(v,u,w);
        }
        dfs(1,0,0);
        printf("Case #%d: %lld\n",++cas,LB.query_max());
    }
    return 0;
}


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值