[学习笔记]线性基

学长讲的,之前也在线性代数上看到过

zero4338大佬指正,这两个基本可以看做等价的

所以我自然结合了线性代数来乱搞(

约定用 表示异或

1. 什么是线性基?

线性基是一个数的集合,每一个序列都至少有一个线性基,取线性基中的若干个数异或起来可以得到序列中的任意数字

为方便理解,可以将一个数字拆成二进制

考虑一个数字 13 ,把它拆成二进制就是 (1101)2 ,设有 m

那么考虑建系,构建一个 m 维直角坐标系 m13 可以表示为 a=(1,1,0,1)

能够把这个向量唯一表示的一组基底(比如 12=(1100)21=(1)2 )就叫做线性基

 

2. 线性基的性质

1. 原序列里的任何一个数都可以由线性基中的几个数异或得到

2. 线性基中的任意一些数异或起来都不能得到 0

3. 线性基中的数字个数是唯一的且尽可能少

对于性质 2 的证明:首先假设 abc=0 ,那么 ab=c ,根据定义得不成立

对于性质 3 的证明:依旧考虑 m 空间中的一些基底,显然地,少了任意一个都会导致某些向量无法表示

同时另外的任何一个数都可以用这一组基底表示,因此多出一个是完全没有必要的

3. 构建一个线性基

不妨设 base 数组为 a 数组的一个线性基,那么我们考虑如何构建这个玩意?

显然的是可以对于 a 中的每一个数从二进制的最高位向最低位扫,如果这一位还没有对应的线性基那么就把当前的 a[i] 作为线性基中的一员

在插入一个数字之后立刻 break 是一个比较重要的点

 

复制代码
#define int long long
void insrt(int x){
    for(int i=63;i>=0;i--){
        if((x>>i)&1){
            if(!base[i]){
                base[i]=x;
                return;
            }else{
                x^=base[i];
            }
        }
    }
}
复制代码

 

4. 线性基的运算

0.判断一个数能否用线性基表示

从高位到低位减去对应的 base[i] 就好了,代码就不放了

1. 查询异或最大值

放一道例题

这道题目就是板子,题解中大多在构造线性基的时候判断 x(1<<i) 的关系,但是当 i=63long long 有溢出的风险

所以我采用了 (x>>i) 的方式避免溢出

 

复制代码
#include<cstdio>
#include<cstring>
#include<string>
#define int long long
#define WR WinterRain
using namespace std;
const int WR=1010;
int n,ans;
int base[WR],a[WR];
int read(){
    int s=0,w=1;
    char ch=getchar();
    while(ch>'9'||ch<'0'){
        if(ch=='-') w=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9'){
        s=s*10+ch-'0';
        ch=getchar();
    }
    return s*w;
}
void insrt(int x){
    for(int i=63;i>=0;i--){
        if((x>>i)&1){
            if(!base[i]){
                base[i]=x;
                return;
            }else{
                x^=base[i];
            }
        }
    }
}
signed main(){
    n=read();
    for(int i=1;i<=n;i++){
        a[i]=read();
        insrt(a[i]);
    }
    for(int i=63;i>=0;i--){
        if((ans^base[i])>ans) ans^=base[i];
    }
    printf("%lld\n",ans);
    return 0;
}
View Code
复制代码

 

2.查询异或最小值

一般而言,异或的最小值就是线性基中的最小元素,这是因为我们总是尽可能地让这一位以上的各位是 0 再去插入

但是要注意特殊情况也就是存在 0 ,此时直接输出 0 就行了

 

复制代码
#include<cstdio>
#include<cstring>
#include<string>
#define int long long
#define WR WinterRain
using namespace std;
const int WR=1010;
int n,ans;
int base[WR],a[WR];
int read(){
    int s=0,w=1;
    char ch=getchar();
    while(ch>'9'||ch<'0'){
        if(ch=='-') w=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9'){
        s=s*10+ch-'0';
        ch=getchar();
    }
    return s*w;
}
void insrt(int x){
    for(int i=63;i>=0;i--){
        if((x>>i)&1){
            if(!base[i]){
                base[i]=x;
                return;
            }else{
                x^=base[i];
            }
        }
    }
}
signed main(){
    n=read();
    for(int i=1;i<=n;i++){
        a[i]=read();
        insrt(a[i]);
        if(a[i]==0){
            printf("0\n");
            return 0;
        }
    }
    for(int i=0;i<=63;i++){
        if(base[i]){
            printf("%lld\n",base[i]);
            return 0;
        }
    }
    return 0;
}
View Code
复制代码

 

3. 查询异或排名

有一道例题 albus就要第一个出场

这里给出正确性的证明:

首先,n 个数任意排列组合有 i=0nCni=2n 种不同的异或方法

但是有些数异或的结果是 0 ,这样就导致了重复结果的出现

由于线性基里异或不可能为 0 ,设线性基大小为 V 那么有 2V 种不同的异或值

接着,对于一个异或和,在外面 nV 个数中任意取一个数加入可以异或出这个异或和的数的集合,一定可以通过调整原本的线性基内的数的组成来使异或和不变。

还是分类讨论:

1. 如果从外面加入的这个数可以用除了表示出这个异或和的那些线性基内的数表示出来,那就把那些数也异或进来,那么异或和不变。

2. 如果外面加入的数必须用完全属于表示出这个异或和的那些线性基内的数表示出来,那就把这些线性基中的数弹掉,那异或和还是不变。

3. 如果部分属于表示这个集合的数,部分不属于,那就把两个部分拆开,属于的部分不选,不属于的部分选,就把这两个部分都变成了 0 ,异或和照样不变。

假如从 nV 个数中选了多于一个数,对于每一个数都按照上面的操作方法就可以保证异或和不变。

所以对于外面的点的任意一个集合都是可以通过调整线性基内的数使得异或和不变。所以同一个异或和出现了 2nV

那么如何计算就比较简单了,我们找出唯一构成 q 的方案,用一个数组存储

考虑到小于 q 的子集至少都要不选取这些元素中的其中一些,我们枚举这些元素中不选取的位数最高的元素(假设为从低到高为第 i 位存在的元素)

那么位数更低的元素显然可以随便选,也就是说对 rank 的贡献是 2i×2nV ,求个和就好了

 

复制代码
#include<cstdio>
#include<cstring>
#include<string>
#define int long long
#define WR WinterRain
using namespace std;
const int WR=100100,mod=10086;
int n,ans,q;
int stk[WR],cnt;
int rnk;
int base[WR],val[WR];
int read(){
    int s=0,w=1;
    char ch=getchar();
    while(ch>'9'||ch<'0'){
        if(ch=='-') w=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9'){
        s=s*10+ch-'0';
        ch=getchar();
    }
    return s*w;
}
int quick_pow(int a,int b){
    int bse=a,res=1;
    while(b){
        if(b&1) res=res*bse%mod;
        bse=bse*bse%mod;
        b>>=1;
    }
    return res;
}
void insrt(int x){
    for(int i=31;i>=0;i--){
        if((x>>i)&1){
            if(!base[i]){
                base[i]=x;
                return;
            }else{
                x^=base[i];
            }
        }
    }
}
signed main(){
    n=read();
    for(int i=1;i<=n;i++){
        int x=read();
        insrt(x);
    }
    q=read();
    for(int i=0;i<=31;i++){
        if(base[i]) stk[++cnt]=i;
    }
    for(int i=1;i<=cnt;i++){
        //printf("%lld ",stk[i]);
        if((q>>stk[i])&1) rnk=(rnk+quick_pow(2,i))%mod;
    }
    printf("%lld\n",(rnk*quick_pow(2,n-cnt-1)%mod+1)%mod);
    return 0;
}
View Code
复制代码

 

4. 线性基的合并

想不到吧这玩意还可以合并

还是放一道题目幸运数字

线性基合并,只需要将第二个线性基中的数字作为 val 依次尝试插入第一个线性基就好了

对于这道题目,我们考虑对于每一个节点构建 log(n) 个线性基,用倍增的手段表示从 ii 的祖先这个区间的线性基

在查询路径的时候倍增寻找 lca ,同时合并路径上点的线性基即可

这样的时间复杂度大约是 Θ(qlog3n) 可以过

 

复制代码
#include<cstdio>
#include<cstring>
#include<string>
#define int long long
#define WR WinterRain
using namespace std;
const int WR=50100;
struct Edge{
    int pre,to;
}edge[WR<<1];
int n,q;
int head[WR],tot;
int dpt[WR],fa[WR][20];
int base[WR][20][65];
int ans[WR];
int read(){
    int s=0,w=1;
    char ch=getchar();
    while(ch>'9'||ch<'0'){
        if(ch=='-') w=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9'){
        s=s*10+ch-'0';
        ch=getchar();
    }
    return s*w;
}
void get_base(int a[],int val){
    for(int i=63;i>=0;i--){
        if((val>>i)&1){
            if(!a[i]){
                a[i]=val;
                return;
            }else{
                val^=a[i];
            }
        }
    }
}
void merge_base(int a[],int b[]){
    for(int i=0;i<=63;i++) if(b[i]) get_base(a,b[i]);
}
void add(int u,int v){
    edge[++tot].pre=head[u];
    edge[tot].to=v;
    head[u]=tot;
}
void dfs(int u,int root){
    fa[u][0]=root;
    dpt[u]=dpt[root]+1;
    for(int i=1;i<=19;i++){
        fa[u][i]=fa[fa[u][i-1]][i-1];
        memcpy(base[u][i],base[u][i-1],sizeof(base[u][i-1]));
        merge_base(base[u][i],base[fa[u][i-1]][i-1]);
    }
    for(int i=head[u];i;i=edge[i].pre){
        int v=edge[i].to;
        if(v==root) continue;
        dfs(v,u);
    }
}
void LCA(int x,int y){
    if(dpt[x]<dpt[y]) swap(x,y);
    for(int i=19;i>=0;i--){
        if(dpt[fa[x][i]]>=dpt[y]){
            merge_base(ans,base[x][i]);
            x=fa[x][i];
        }
    }
    if(x==y){
        merge_base(ans,base[x][0]);
        return;
    }
    for(int i=19;i>=0;i--){
        if(fa[x][i]!=fa[y][i]){
            merge_base(ans,base[x][i]);
            merge_base(ans,base[y][i]);
            x=fa[x][i],y=fa[y][i];
        }
    }
    merge_base(ans,base[x][0]);
    merge_base(ans,base[y][0]);
    merge_base(ans,base[fa[x][0]][0]);
    //printf("%lld and %lld's LCA = %lld\n",x,y,fa[x][0]);
}
signed main(){
    n=read(),q=read();
    for(int i=1;i<=n;i++){
        int t=read();
        get_base(base[i][0],t);
    }
    for(int i=1;i<n;i++){
        int u=read(),v=read();
        add(u,v);add(v,u);
    }
    dfs(1,0);
    while(q--){
        int x=read(),y=read();
        memset(ans,0,sizeof(ans));
        LCA(x,y);
        int res=0;
        for(int i=63;i>=0;i--){
            if((res^ans[i])>res) res^=ans[i];
        }
        printf("%lld\n",res);
    }
    return 0;
}
View Code
复制代码
posted @   冬天的雨WR  阅读(486)  评论(11编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
Live2D
点击右上角即可分享
微信分享提示