【9*】线性基学习笔记

前言

线性基是一个好用的东西,多用于维护异或类操作。以后碰到异或类题目,可以考虑线性基,好写好调。

此类知识点大纲中并未涉及,所以【9】是我自己的估计,后带星号表示估计,仅供参考。

线性基

定义

向量向量是一种既有大小又有方向的量。

线性组合:设 a1,a2,ae(e1) 是域 P 上线性空间 V 中的有限个向量。若 V 中向量 a 可以表示为 a=k1a1+k2a2++keae(kP),则 a 是向量组 a1,a2,ae 的一个线性组合。也就是说,a 可由向量组 a1,a2,ae 线性表示

例如,在三维线性空间 P3 中,向量 a=(x1,x2,x3) 可由向量组 a1=(1,0,0),a2=(0,1,0),a3=(0,0,1) 线性表示成 a=x1a1+x2a2+x3a3a 是向量组 a1=(1,0,0),a2=(0,1,0),a3=(0,0,1) 的一个线性组合。

线性相关:在线性代数里,向量空间的一组元素中,若没有向量可以用组内有限个其他向量的线性组合所表示,则称为线性无关,否则称为线性相关

例如:(4,1,9),(1,4,2),(5,5,11) 这一组向量线性相关,因为 (4,1,9)+(1,4,2)=(5,5,11)。而 (0,1,0),(2,1,0),(0,1,1) 这一组向量线性无关,因为没有向量可以用组内有限个其他向量的线性组合所表示。

线性基:线性空间 V 的一个极大线性无关组V 的一组线性基,简称

通俗来讲,就是在线性空间 V 中找到一组包含元素最多的线性无关组。

性质

性质 1n 维的线性空间 V任意 n+1 个向量线性相关

性质 2n 维的线性空间 V任意 n线性无关的向量构成一组

性质 3:若 V 中的任意向量均可被向量组 a1,a2,,an 线性表示,则其是 V 的一个

性质 4V任意线性无关向量组 a1,a2,,am 均可通过插入一些向量使得其变为 V 的一个

实现方式

贪心构造法

由性质 1,2,我们得到线性基中有且仅有 n 个元素。

我们考虑对于每个维度进行维护。由性质 3,线性空间内任何元素都可以由线性基表示出。因此,线性基中包含的元素中,在每一个维度上,至少存在一个元素在该维度的向量不为 0

我们把线性基中不为 0最高维i 的元素记录在 bas[i] 中。如果 bas[i]0,那么就至少存在一个元素在维度 i 的向量不为 0

由性质 4,我们把每个依次元素插入线性基。

对于插入的元素,我们从高维度低维度遍历。假设现在遍历到维度 i,即当前向量不为 0最高维i

如果 bas[i]=0,那么我们直接将这个向量存入 bas[i] 中。当前向量不为 0 的最高维为 i,符合 bas[i] 的条件。

如果 bas[i]0,那么我们利用 bas[i] 消去当前元素的第 i 维。已经存在一个符合条件的向量了,没有必要再存储一个。但是,这个元素可能在更低维存在不为 0 的向量,所以我们消去最高维,使较低维称为最高维,继续重复这个过程,直到能被插入或者遍历完所有维。这样做不会导致错误,因为当我们使用消去过的结果时,只需要把它还原成原始数据加上一些用于消去的数据即可,这两个数据都在线性空间中。

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;
		       }
		   }
}

时间复杂度为 O(n)

应用

维护选取任意个数的最大异或和:例题 1,4,5,6

维护线性空间大小:例题 2,7

维护向量:例题 3

维护异或和之和:例题 7

例题

例题 1

P3812 【模板】线性基

我们把一个整数的 n 为二进制看作一个 n 维的向量,我们维护这些向量的异或线性基。唯一不同的是这里消去可以直接使用异或,减少了不少码量。

这是一个非常常用的 trick,几乎所有的线性基维护异或类题目都使用了这个 trick。

选取任意个数的最大异或和,相当于在线性基中选取一些数,使它们异或和最大。我们贪心地从高位往低位,如果结果第 i 位上为 0,则异或 bas[i]。这个贪心是对的,因为根据线性基的构造过程,bas[i] 的第 i+1n 位均为 0,不会影响高位的选择。

#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;
}

例题 2

P3857 [TJOI2008] 彩灯

依旧使用例题 1 的 trick,维护异或线性基。

我们发现,如果 bas[i]0,那么我们一定可以通过控制 bas[i] 的选取或者不选取来控制第 i 位为 01。只要 bas[i]0,一定存在一种 bas[i] 的选法使第 i 位为 0,也一定存在一种 bas[i] 的选法使第 i 位为 1

根据线性基的构造过程,bas[i] 的第 i+1n 位均为 0,不会影响高位的选择,所以从高位遍历到低位,每一位的选法彼此独立,使用乘法原理即可。如果存在 kbas[i]0,则答案为 2k

#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;
}

例题 3

P3265 [JLOI2015] 装备购买

线性基模板题,不多赘述。

#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;
}

例题 4

P4839 P 哥的桶

题目要求使异或和尽可能大,自然想到线性基。由于题目中有单点修改,区间操作,自然考虑线段树或树状数组。

为了便于理解,我使用了线段树,对于 P 哥的每一个桶,开一个线性基。在桶里添加一个球,相当于在线性基中插入一个元素。区间查询只需要合并出最后的线性基,即可查询。

对于线段树的每一个节点,我们维护 [l,r] 的桶合并后的线性基,相当于合并两个子区间的线性基。

接下来,就是一个重要且常用的知识点:线性基合并。合并线性基 y 到线性基 x,我们只需要把 y 中的每个元素插入到 x 中。这个过程的时间复杂度是 O(log2a),其中 a 为值域。

总的时间复杂度为 O(nlognlog2a)

#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;
}

例题 5

P3292 [SCOI2016] 幸运数字

同样的,需要维护最大异或值,考虑线性基。又由于静态树上查询路径,考虑倍增。

我们用倍增求出查询点 x,y 的 LCA, 并记录 bas[x][i] 表示从点 i 开始,它以及它的 2i 级祖先的线性基。我们很容易得到下面转移方程:

bas[x][i]=merge(bas[x][i1],bas[f[x][i1]][i1])

其中 merge 操作表示使用线性基合并操作合并两个线性基。

在查询时,我们只需要将路径上倍增查询到的的所有线性基合并起来,直接在合并后的线性基求答案即可。注意容易被卡空间。时间复杂度为 O(nlognlog2a)

这不是最优的做法,可以参考这篇 题解 学习更优做法。

#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;
}

例题 6

P4151 [WC2011] 最大XOR和路径

线性基在图中的运用。

我们发现,从一条路径上走到一个环,再原路返回,经过的非环路径不会影响结果。因为过去经过一次,回来经过一次,刚好抵消。

所以,最终的路径为一条 1n 的路径和若干个环组成。我们利用 DFS 求出每一个环的边权异或和,丢进线性基,再以任意一条 1n 的路径作为查询初始值(具体见代码,证明见例题 7),查询最大异或和。

这条 1n 的路径可以随便选。假设路径 A 比路径 B 优秀一些,而我们最开始选择了路径 B。显然,AB 共同构成了一个环(1n1)。如果我们发现路径 A 要优秀一些,那么我们用 B 异或上这个大环,就会得到我们想要的 A

#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;
}

例题 7

CF724G Xor-matic Number of the Graph

最难的一题。

直接维护异或和之和,显然不可能。由于二进制位相互独立,我们考虑维护每一个二进制位的出现次数,再乘以位权加入结果。

对于路径的维护,我们参考例题 6,分成主路径和若干个环。记路径 1n 的边的异或和为 dis[i],任意两点 x,y 之间的路径的边的异或和可以由 dis[x]dis[y] 得到。如果 1x1y 在点 z 前有重复部分,异或后重复部分消去,留下 xzy。否则,如果没有重复部分,则异或后变为 x1y。结论成立。

如果有环的二进制第 i 位为 1,那么我们肯定可以使第 i 位为 1。如果目前答案第 i 位为 0,直接异或,否则不异或。因此,任意一条 xy 的简单路径都第 i 位都可以为 1,有 Cn2 中选法。除了第 i 位,其他位可以任意选择。这样就转化为了例题 2,方案数为 2x1,其中 x 为线性基大小,即 bas[i]0 的数的数量。运用乘法原理,再乘上权值,这一位的贡献为:

Cn2×2x1×2i

如果没有环的二进制第 i 位为 1,那么走环对于这一位没有影响。此时线性基可以随便选,方案数为 2x,其中 x 为线性基大小。我们只需要统计有多少条简单路径的异或和的第 i 位为 1 即可。

结合路径的计算式,dis[x]dis[y],我们继续进行拆位。对每一个 dis,如果第 i 位为 1,则将 wi,11;如果第 i 位为 0,则将 wi,01wi,1 表示第 i 位为 1dis 的个数,wi,0 表示第 i 位为 0dis 的个数。如果要让一条路径的权值 dis[x]dis[y]i 位为 1,则必须在 wi,1 中和 wi,0 中各选一个。根据乘法原理,方案数为 wi,0×wi,1。运用乘法原理,再乘上权值,这一位的贡献为:

wi,0×wi,1×2x×2i

代码中这一部分实现方式略微不同,其中 ct[i] 表示 wi,1posct[i] 表示 wi,0

注意图不一定联通,对于每个联通块分别计算即可。

#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;
}

后记

我们发现,线性基什么都可以套。把线性基当做元素,就可以套进很多题目。

posted @   w9095  阅读(8)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示