【9*】线性基学习笔记
前言
线性基是一个好用的东西,多用于维护异或类操作。以后碰到异或类题目,可以考虑线性基,好写好调。
此类知识点大纲中并未涉及,所以【9】是我自己的估计,后带星号表示估计,仅供参考。
线性基
定义
向量:向量是一种既有大小又有方向的量。
线性组合:设 是域 上线性空间 中的有限个向量。若 中向量 可以表示为 ,则 是向量组 的一个线性组合。也就是说, 可由向量组 线性表示。
例如,在三维线性空间 中,向量 可由向量组 线性表示成 , 是向量组 的一个线性组合。
线性相关:在线性代数里,向量空间的一组元素中,若没有向量可以用组内有限个其他向量的线性组合所表示,则称为线性无关,否则称为线性相关。
例如: 这一组向量线性相关,因为 。而 这一组向量线性无关,因为没有向量可以用组内有限个其他向量的线性组合所表示。
线性基:线性空间 的一个极大线性无关组为 的一组线性基,简称基。
通俗来讲,就是在线性空间 中找到一组包含元素最多的线性无关组。
性质
性质 : 维的线性空间 中任意 个向量线性相关。
性质 : 维的线性空间 中任意 个线性无关的向量构成一组基。
性质 :若 中的任意向量均可被向量组 线性表示,则其是 的一个基。
性质 : 中任意线性无关向量组 均可通过插入一些向量使得其变为 的一个基。
实现方式
贪心构造法
由性质 ,我们得到线性基中有且仅有 个元素。
我们考虑对于每个维度进行维护。由性质 ,线性空间内任何元素都可以由线性基表示出。因此,线性基中包含的元素中,在每一个维度上,至少存在一个元素在该维度的向量不为 。
我们把线性基中不为 的最高维为 的元素记录在 中。如果 ,那么就至少存在一个元素在维度 的向量不为 。
由性质 ,我们把每个依次元素插入线性基。
对于插入的元素,我们从高维度往低维度遍历。假设现在遍历到维度 ,即当前向量不为 的最高维为 。
如果 ,那么我们直接将这个向量存入 中。当前向量不为 的最高维为 ,符合 的条件。
如果 ,那么我们利用 消去当前元素的第 维。已经存在一个符合条件的向量了,没有必要再存储一个。但是,这个元素可能在更低维存在不为 的向量,所以我们消去最高维,使较低维称为最高维,继续重复这个过程,直到能被插入或者遍历完所有维。这样做不会导致错误,因为当我们使用消去过的结果时,只需要把它还原成原始数据加上一些用于消去的数据即可,这两个数据都在线性空间中。
void insert(long long x)
{
for(int i=1;i<=m;i++)
if(fabs(a[x].v[i])>=eps)
{
if(fabs(bas[i][i])>=eps)
{
double div=a[x].v[i]/bas[i][i];
for(int j=1;j<=m;j++)a[x].v[j]-=bas[i][j]*div;
}
else
{
for(int j=1;j<=m;j++)
bas[i][j]=a[x].v[j];
q[i]=a[x].c;
break;
}
}
}
时间复杂度为 。
应用
维护选取任意个数的最大异或和:例题 。
维护线性空间大小:例题 。
维护向量:例题 。
维护异或和之和:例题 。
例题
例题 :
我们把一个整数的 为二进制看作一个 维的向量,我们维护这些向量的异或线性基。唯一不同的是这里消去可以直接使用异或,减少了不少码量。
这是一个非常常用的 trick,几乎所有的线性基维护异或类题目都使用了这个 trick。
选取任意个数的最大异或和,相当于在线性基中选取一些数,使它们异或和最大。我们贪心地从高位往低位,如果结果第 位上为 ,则异或 。这个贪心是对的,因为根据线性基的构造过程, 的第 位均为 ,不会影响高位的选择。
#include <bits/stdc++.h>
using namespace std;
long long n,a,bas[100],ans=0;
void insert(long long x)
{
for(int i=50;i>=0;i--)
if((x>>i)&1)
{
if(bas[i]!=0)x^=bas[i];
else
{
bas[i]=x;
break;
}
}
}
int main()
{
scanf("%lld",&n);
for(int i=1;i<=n;i++)scanf("%lld",&a),insert(a);
for(int i=50;i>=0;i--)
if(!((ans>>i)&1))ans^=bas[i];
printf("%lld",ans);
return 0;
}
例题 :
依旧使用例题 的 trick,维护异或线性基。
我们发现,如果 ,那么我们一定可以通过控制 的选取或者不选取来控制第 位为 或 。只要 ,一定存在一种 的选法使第 位为 ,也一定存在一种 的选法使第 位为 。
根据线性基的构造过程, 的第 位均为 ,不会影响高位的选择,所以从高位遍历到低位,每一位的选法彼此独立,使用乘法原理即可。如果存在 个 ,则答案为 。
#include <bits/stdc++.h>
using namespace std;
long long n,m,bas[100],ans=1,mod=2008;
char s[100];
void insert(long long x)
{
for(int i=50;i>=0;i--)
if((x>>i)&1)
{
if(bas[i]!=0)x^=bas[i];
else
{
bas[i]=x;
break;
}
}
}
int main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=m;i++)
{
long long a=0;
scanf("%s",s+1);
for(int i=1;i<=n;i++)
if(s[i]=='O')a=(a<<1)|1;
else if(s[i]=='X')a=(a<<1);
insert(a);
}
for(int i=50;i>=0;i--)
if(bas[i])ans=ans*2%mod;
printf("%lld",ans);
return 0;
}
例题 :
线性基模板题,不多赘述。
#include <bits/stdc++.h>
using namespace std;
struct val
{
double v[600];
long long c;
}a[600];
long long n,m,ans1=0,ans2=0,q[600];
double bas[600][600],eps=1e-5;
char s[600];
bool cmp(struct val a,struct val b)
{
return a.c<b.c;
}
void insert(long long x)
{
for(int i=1;i<=m;i++)
if(fabs(a[x].v[i])>=eps)
{
if(fabs(bas[i][i])>=eps)
{
double div=a[x].v[i]/bas[i][i];
for(int j=1;j<=m;j++)a[x].v[j]-=bas[i][j]*div;
}
else
{
for(int j=1;j<=m;j++)
bas[i][j]=a[x].v[j];
q[i]=a[x].c;
break;
}
}
}
int main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%lf",&a[i].v[j]);
for(int i=1;i<=n;i++)scanf("%lld",&a[i].c);
sort(a+1,a+n+1,cmp);
for(int i=1;i<=n;i++)insert(i);
for(int i=1;i<=m;i++)
if(fabs(bas[i][i])>=eps)ans1++,ans2+=q[i];
printf("%lld %lld",ans1,ans2);
return 0;
}
例题 :
题目要求使异或和尽可能大,自然想到线性基。由于题目中有单点修改,区间操作,自然考虑线段树或树状数组。
为了便于理解,我使用了线段树,对于 P 哥的每一个桶,开一个线性基。在桶里添加一个球,相当于在线性基中插入一个元素。区间查询只需要合并出最后的线性基,即可查询。
对于线段树的每一个节点,我们维护 的桶合并后的线性基,相当于合并两个子区间的线性基。
接下来,就是一个重要且常用的知识点:线性基合并。合并线性基 到线性基 ,我们只需要把 中的每个元素插入到 中。这个过程的时间复杂度是 ,其中 为值域。
总的时间复杂度为 。
#include <bits/stdc++.h>
using namespace std;
struct node
{
long long bas[50];
}tr[400000];
long long n,m,op,l,r,lc[400000],rc[400000],root=0,cnt=0;
void insert(long long x,struct node &p)
{
for(int i=32;i>=0;i--)
if((x>>i)&1)
{
if(p.bas[i])x^=p.bas[i];
else
{
p.bas[i]=x;
break;
}
}
}
void pushup(long long x)
{
tr[x]=tr[lc[x]];
for(int i=32;i>=0;i--)
if(tr[rc[x]].bas[i])insert(tr[rc[x]].bas[i],tr[x]);
}
struct node merge(struct node a,struct node b)
{
struct node res=a;
for(int i=32;i>=0;i--)
if(b.bas[i])insert(b.bas[i],res);
return res;
}
void build(long long &now,long long l,long long r)
{
now=++cnt;
if(l==r)return;
long long mid=(l+r)>>1;
build(lc[now],l,mid),build(rc[now],mid+1,r);
pushup(now);
}
void update(long long now,long long l,long long r,long long x,long long p)
{
if(l==r)
{
insert(x,tr[now]);
return;
}
long long mid=(l+r)>>1;
if(p<=mid)update(lc[now],l,mid,x,p);
else if(p>=mid+1)update(rc[now],mid+1,r,x,p);
pushup(now);
}
struct node query(long long now,long long l,long long r,long long lx,long long rx)
{
if(l>=lx&&r<=rx)return tr[now];
long long mid=(l+r)>>1;
if(lx>=mid+1)return query(rc[now],mid+1,r,lx,rx);
if(rx<=mid)return query(lc[now],l,mid,lx,rx);
return merge(query(lc[now],l,mid,lx,rx),query(rc[now],mid+1,r,lx,rx));
}
long long getmx(long long l,long long r)
{
long long ans=0;
struct node res=query(root,1,n,l,r);
for(int i=32;i>=0;i--)
if(!((ans>>i)&1))ans=ans^res.bas[i];
return ans;
}
int main()
{
scanf("%lld%lld",&m,&n);
build(root,1,n);
for(int i=1;i<=m;i++)
{
scanf("%lld%lld%lld",&op,&l,&r);
if(op==1)update(root,1,n,r,l);
else if(op==2)printf("%lld\n",getmx(l,r));
}
return 0;
}
例题 :
同样的,需要维护最大异或值,考虑线性基。又由于静态树上查询路径,考虑倍增。
我们用倍增求出查询点 的 LCA, 并记录 表示从点 开始,它以及它的 级祖先的线性基。我们很容易得到下面转移方程:
其中 操作表示使用线性基合并操作合并两个线性基。
在查询时,我们只需要将路径上倍增查询到的的所有线性基合并起来,直接在合并后的线性基求答案即可。注意容易被卡空间。时间复杂度为 。
这不是最优的做法,可以参考这篇 题解 学习更优做法。
#include <bits/stdc++.h>
using namespace std;
struct edge
{
int v,nxt;
}e[60000];
struct lb
{
long long bas[61];
}b[20001][21];
int n,m,x,y,dep[30000],f[30000][30],h[30000],cnt=0;
long long a[30000];
void add_edge(int u,int v)
{
e[++cnt].nxt=h[u];
e[cnt].v=v;
h[u]=cnt;
}
void insert(long long x,struct lb &p)
{
for(int i=60;i>=0;i--)
if((x>>i)&1)
{
if(p.bas[i])x^=p.bas[i];
else
{
p.bas[i]=x;
break;
}
}
}
struct lb merge(struct lb a,struct lb b)
{
struct lb res=a;
for(int i=60;i>=0;i--)
if(b.bas[i])insert(b.bas[i],res);
return res;
}
void dfs(int x,int fa)
{
f[x][0]=fa,insert(a[x],b[x][0]);
for(int i=1;i<=15;i++)
if(f[x][i-1])f[x][i]=f[f[x][i-1]][i-1],b[x][i]=merge(b[x][i-1],b[f[x][i-1]][i-1]);
else break;
for(int i=h[x];i;i=e[i].nxt)
{
if(e[i].v==fa)continue;
dep[e[i].v]=dep[x]+1;
dfs(e[i].v,x);
}
}
int lca(int x,int y)
{
if(dep[x]>dep[y])swap(x,y);
long long c=dep[y]-dep[x];
for(int i=15;i>=0;i--)
if((c>>i)&1)y=f[y][i];
if(x==y)return x;
for(int i=15;i>=0;i--)
if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];
return f[x][0];
}
struct lb query(int x,int y)
{
struct lb ans=b[x][0];
long long c=dep[y]-dep[x];
for(int i=15;i>=0;i--)
if((c>>i)&1)ans=merge(ans,b[y][i]),y=f[y][i];
return ans;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
for(int i=1;i<=n-1;i++)
{
scanf("%d%d",&x,&y);
add_edge(x,y),add_edge(y,x);
}
dfs(1,0);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&x,&y);
long long l=lca(x,y),ans=0;
struct lb res=merge(query(l,x),query(l,y));
for(int i=60;i>=0;i--)
if(!((ans>>i)&1))ans^=res.bas[i];
printf("%lld\n",ans);
}
return 0;
}
例题 :
线性基在图中的运用。
我们发现,从一条路径上走到一个环,再原路返回,经过的非环路径不会影响结果。因为过去经过一次,回来经过一次,刚好抵消。
所以,最终的路径为一条 的路径和若干个环组成。我们利用 DFS 求出每一个环的边权异或和,丢进线性基,再以任意一条 的路径作为查询初始值(具体见代码,证明见例题 ),查询最大异或和。
这条 的路径可以随便选。假设路径 比路径 优秀一些,而我们最开始选择了路径 。显然, 与 共同构成了一个环()。如果我们发现路径 要优秀一些,那么我们用 异或上这个大环,就会得到我们想要的 。
#include <bits/stdc++.h>
using namespace std;
struct edge
{
long long v,nxt,d;
}e[300000];
long long n,m,u,v,d,h[300000],bas[300000],book[300000],dis[300000],cnt=1,ans=0;
void add_edge(long long u,long long v,long long d)
{
e[++cnt].nxt=h[u];
e[cnt].v=v;
e[cnt].d=d;
h[u]=cnt;
}
void insert(long long x)
{
for(int i=64;i>=0;i--)
if((x>>i)&1)
{
if(bas[i]!=0)x^=bas[i];
else
{
bas[i]=x;
break;
}
}
}
void dfs(long long now,long long pre,long long dn)
{
book[now]=1,dis[now]=dn;
for(int i=h[now];i;i=e[i].nxt)
if(i!=(pre^1))
{
if(book[e[i].v])insert(dis[now]^dis[e[i].v]^e[i].d);
else dfs(e[i].v,i,dn^e[i].d);
}
}
int main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%lld%lld%lld",&u,&v,&d);
add_edge(u,v,d),add_edge(v,u,d);
}
dfs(1,0,0);
ans=dis[n];
for(int i=64;i>=0;i--)
if(!((ans>>i)&1))ans^=bas[i];
printf("%lld",ans);
return 0;
}
例题 :
CF724G Xor-matic Number of the Graph
最难的一题。
直接维护异或和之和,显然不可能。由于二进制位相互独立,我们考虑维护每一个二进制位的出现次数,再乘以位权加入结果。
对于路径的维护,我们参考例题 ,分成主路径和若干个环。记路径 的边的异或和为 ,任意两点 之间的路径的边的异或和可以由 得到。如果 和 在点 前有重复部分,异或后重复部分消去,留下 。否则,如果没有重复部分,则异或后变为 。结论成立。
如果有环的二进制第 位为 ,那么我们肯定可以使第 位为 。如果目前答案第 位为 ,直接异或,否则不异或。因此,任意一条 的简单路径都第 位都可以为 ,有 中选法。除了第 位,其他位可以任意选择。这样就转化为了例题 ,方案数为 ,其中 为线性基大小,即 的数的数量。运用乘法原理,再乘上权值,这一位的贡献为:
如果没有环的二进制第 位为 ,那么走环对于这一位没有影响。此时线性基可以随便选,方案数为 ,其中 为线性基大小。我们只需要统计有多少条简单路径的异或和的第 位为 即可。
结合路径的计算式,,我们继续进行拆位。对每一个 ,如果第 位为 ,则将 加 ;如果第 位为 ,则将 加 。 表示第 位为 的 的个数, 表示第 位为 的 的个数。如果要让一条路径的权值 第 位为 ,则必须在 中和 中各选一个。根据乘法原理,方案数为 。运用乘法原理,再乘上权值,这一位的贡献为:
代码中这一部分实现方式略微不同,其中 表示 , 表示 。
注意图不一定联通,对于每个联通块分别计算即可。
#include <bits/stdc++.h>
using namespace std;
struct edge
{
long long v,nxt,d;
}e[800000];
long long n,m,u,v,d,h[800000],bas[100],book[800000],dis[800000],ct[100],p[800000],cnt=1,ans=0,siz=0,pos=0;
const long long mod=1e9+7;
void add_edge(long long u,long long v,long long d)
{
e[++cnt].nxt=h[u];
e[cnt].v=v;
e[cnt].d=d;
h[u]=cnt;
}
void insert(long long x)
{
for(int i=63;i>=0;i--)
if((x>>i)&1)
{
if(bas[i]!=0)x^=bas[i];
else
{
siz++,bas[i]=x;
break;
}
}
}
void dfs(long long now,long long pre,long long dn)
{
book[now]=1,dis[now]=dn,pos++;
for(int i=63;i>=0;i--)
if((dn>>i)&1)ct[i]++;
for(int i=h[now];i;i=e[i].nxt)
if(i!=(pre^1))
{
if(book[e[i].v])insert(dis[now]^dis[e[i].v]^e[i].d);
else dfs(e[i].v,i,dn^e[i].d);
}
}
int main()
{
scanf("%lld%lld",&n,&m);
p[0]=1;
for(int i=1;i<=63;i++)p[i]=p[i-1]*2%mod;
for(int i=1;i<=m;i++)
{
scanf("%lld%lld%lld",&u,&v,&d);
add_edge(u,v,d),add_edge(v,u,d);
}
for(int i=1;i<=n;i++)
if(!book[i])
{
memset(bas,0,sizeof(bas)),memset(ct,0,sizeof(ct));
pos=siz=0;
dfs(i,0,0);
long long flag=0;
for(int j=63;j>=0;j--)flag|=bas[j];
for(int j=63;j>=0;j--)
if((flag>>j)&1)ans=(ans+pos*(pos-1)%mod*500000004%mod*p[siz-1]%mod*p[j]%mod)%mod;
else ans=(ans+ct[j]*(pos-ct[j])%mod*p[siz]%mod*p[j]%mod)%mod;
}
printf("%lld",ans);
return 0;
}
后记
我们发现,线性基什么都可以套。把线性基当做元素,就可以套进很多题目。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探