线性基

线性基

\(n+1\) 次学线性基,希望这次能学会。

模板

线性基通常用来处理异或相关的问题。

假如有 \(n\) 个数,我们想表示它们能相互异或得到的所有数组成的集合。

线性基就是针对这组数生成的集合,它们任意异或得到数的值域与原序列相同,并且满足是一个极小集合(数尽量少)。

联想基向量,这里每一个二进制位可以看成一维,线性基就是一组基向量。(好像叫极大线性无关组

插入一个数的过程:

假如我们想插入 \(x\)\(a_i\) 表示二进制位第二位的基底,如果已经存在基底,那么 \(x \gets x \oplus a_i\),否则 \(a_i \gets x\)

因为 \(a \oplus b \oplus a = b\),所以不妨令 \(c = a \oplus b\),这样仍能得到 \(b\)。上面的过程其实就是这样。

对于这道题,求最大值,直接贪心从高位向低位取,但注意我们取了高位时不知道包不包含低位(第 \(i\) 位的基底只保证最高是 \(i\),不保证没有低位)。

所以注意判断只有异或后会使答案变大才要。

code
#include<bits/stdc++.h>
using namespace std;
const int N = 60;
#define LL long long
int n; LL ck[N];
inline void ins(LL x)
{
	for(int i=55;i>=0;i--)
		if((x>>i)&1)
		{
			if(!ck[i]) {ck[i]=x; break;}
			x^=ck[i];
		}
}

int main()
{
	// freopen("in.in","r",stdin);
	// freopen("out.out","w",stdout);
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		LL x; scanf("%lld",&x);
		ins(x);
	}
	LL ans=0;
	for(int i=55;i>=0;i--) if((ans^ck[i])>ans) ans^=ck[i];
	printf("%lld\n",ans);
	return 0;
}

彩灯

假如找到 \(cnt\) 个基底,那能异或得到的数的个数就是 \(2^{cnt}\)(每一个可选可不选,并且一定不会有重复的数)。

code
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 60;
int n,m;
LL ck[N+5],ans;
void ins(LL x)
{
	for(int j=n-1;j>=0;j--)
		if((x>>j)&1)
		{
			if(!ck[j]) {ck[j]=x; ans++; break;}
			x^=ck[j];
		}
}
int main()
{
	// freopen("in.in","r",stdin);
	// freopen("out.out","w",stdout);
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)
	{
		LL x=0;
		for(int j=n-1;j>=0;j--)
		{
			char c; scanf(" %c",&c);
			if(c=='O') x|=1ll<<j;
		}
		ins(x);
	}
	printf("%lld\n",(1ll<<ans)%2008);
	return 0;
}

元素

直接贪心,按价值从大到小排序,如果能插入线性基就算贡献,否则不算。

首先,由于线性基是一个 极大线性无关组,也就是 任意子集异或和不为零,所以能插进去说明异或和不为零。

贪心证明:

假设最大的数是 \(a_n\),但 \(a_n\) 不在最终答案中,最终答案为集合 \(A\)

那必然有 \(a_n=\bigoplus_{a_j \in A} a_j\),交换任意一个 \(a_j\)\(a_n\) 等式仍成立且更优。所以一定选 \(a_n\)

(问:拟阵是啥?)

code
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 1e3+5;
int n,ans;
LL ck[62];
struct A {int v; LL num;} a[N];
void ins(LL x,int v)
{
	for(int i=60;i>=0;i--)
		if((x>>i)&1)
		{
			if(!ck[i]) {ans+=v; ck[i]=x; break;}
			x^=ck[i];
		}
}

int main()
{
	// freopen("in.in","r",stdin);
	// freopen("out.out","w",stdout);
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%lld%d",&a[i].num,&a[i].v);
	sort(a+1,a+1+n,[&] (A &x,A &y) {return x.v>y.v;});
	for(int i=1;i<=n;i++)
	{
		ins(a[i].num,a[i].v);
	}
	printf("%d\n",ans);
	return 0;
}

新Nim游戏

转化问题,先手想赢,那么先手拿完剩下的集合的任意子集异或和不为零。

发现先把较大的数插进去,组成一个极大线性无关组,剩下的拿走即可。

code
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 105;
int n; LL a[N],ans,ck[N];

void ins(LL x,LL v)
{
	for(int i=30;i>=0;i--)
		if((x>>i)&1)
		{
			if(!ck[i]) {ck[i]=x; return;}
			x^=ck[i];
		}
	ans+=v;
}
int main()
{
	// freopen("in.in","r",stdin);
	// freopen("out.out","w",stdout);
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
	sort(a+1,a+1+n,greater<int>());
	for(int i=1;i<=n;i++) ins(a[i],a[i]);
	printf("%lld\n",ans);
	return 0;
}

XOR

第 k 小板子。

发现原有的线性基不太好确定一个数在值域值域中的位置,我们试着把它转化为一个好看的形式。

为了有一个标准,我们令每个二进制位都只在它这位的基底上出现,其他的直接异或掉。

\[\begin{array}{cc} 1 &0 &1 &0 &0 \\ 0 &1 &0 &0 &0 \\ 0 &0 &1 &0 &1 \\ 0 &0 &0 &0 &1 \end{array} \Rightarrow \begin{array}{cc} 1 &0 &0 &0 &0 \\ 0 &1 &0 &0 &0 \\ 0 &0 &1 &0 &0 \\ 0 &0 &0 &0 &1 \end{array} \]

这样第 \(i\) 位基底恰好就是第 \(2^i\) 小的数,我们将 \(k\) 二进制拆分后发现它们的意义恰好完全一样。

code
#include<stdio.h>
#include<cstring>
using namespace std;
#define LL long long
const int N = 1e4+5;
int T,tot,n,q,m;
LL ck[65],tmp[65],cnt;
inline void ins(LL x)
{
	for(int i=60;i>=0;i--)
		if((x>>i)&1)
		{
			if(!ck[i]) {ck[i]=x; m++; return;}
			x^=ck[i];
		}
}
void init()
{
	cnt=0;
	for(int i=0;i<=60;i++)
	{
		if(!ck[i]) continue;
		for(int j=i-1;j>=0;j--)
			if((ck[i]>>j)&1) ck[i]^=ck[j];
		tmp[cnt++]=ck[i];
	}
}
inline LL que(LL k)
{
	k-=(n!=m); if(!k) return 0;
	if(k>=(1ll<<cnt)) return -1;
	LL res=0;
	for(int i=0;i<cnt;i++)
		if((k>>i)&1) res^=tmp[i];
	return res;
}
int main()
{
	// freopen("in.in","r",stdin);
	// freopen("out.out","w",stdout);
	scanf("%d",&T);
	while(T--)
	{
		memset(ck,0,sizeof(ck)); m=0;
		printf("Case #%d:\n",++tot);
		scanf("%d",&n);
		for(int i=1;i<=n;i++) 
		{
			LL x; scanf("%lld",&x),ins(x);
		}
		init();
		scanf("%d",&q);
		while(q--)
		{
			LL x; scanf("%lld",&x);
			printf("%lld\n",que(x));
		}
	}
	return 0;
}

Operation

不是板。

容易想到扫描线处理出每一个右端点的答案。

因为线性基的时间空间都是 \(log\) 的,我们不妨直接开 \(a_{i,r}\) 表示第 \(i\) 位,右端点到 \(r\) 的线性基。

考虑如何确定左端点。

直接贪心(线性基和贪心最配?),对基底,我们只在乎这一位能否被表出,而不在乎基底的值,所以尽量选靠右的肯定最优。

单独开一个 \(pos_{i,r}\) 记录当前基底的位置,如果新加进来的更靠右,那么替换,继续判断被换下来的基能否往后插。

插入直接做就好了。

code
#include<stdio.h>
#include<cmath>
#include<algorithm>
using namespace std;
#define LL long long
const int N = 5e5+5;
int T,n,m;
int ck[N][32],pos[N][32];
void ins(int k,int x,int p)
{
	for(int i=30;i>=0;i--)
		if((x>>i)&1)
		{
			if(!ck[k][i]) {ck[k][i]=x; pos[k][i]=p; break;}
			else
			{
				if(pos[k][i]<p) swap(ck[k][i],x),swap(pos[k][i],p);
				x^=ck[k][i];
			}
		}
}
int main()
{
	// freopen("in.in","r",stdin);
	// freopen("out.out","w",stdout);
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d%d",&n,&m);
		for(int i=1;i<=n;i++)
		{
			int x; scanf("%d",&x);
			for(int j=0;j<=30;j++) ck[i][j]=ck[i-1][j],pos[i][j]=pos[i-1][j];
			ins(i,x,i);
		}
		int ans=0;
		while(m--)
		{
			int c,l,r; scanf("%d%d",&c,&l);
			if(c==0)
			{
				scanf("%d",&r);
				l=(l^ans)%n+1; r=(r^ans)%n+1;
				if(l>r) swap(l,r);
				ans=0;
				for(int i=30;i>=0;i--) 
					if(pos[r][i]>=l&&(ans^ck[r][i])>ans) ans^=ck[r][i];
				printf("%d\n",ans);
			}
			else
			{
				l=l^ans; n++;
				for(int i=0;i<=30;i++) 	ck[n][i]=ck[n-1][i],pos[n][i]=pos[n-1][i];
				ins(n,l,n);
			}
		}
	}
	return 0;
}

Ivan and Burgers

同上

code
#include<stdio.h>
#include<cmath>
#include<algorithm>
using namespace std;
#define LL long long
const int N = 5e5+5;
int T,n,m;
int ck[N][21],pos[N][21];
void ins(int k,int x,int p)
{
	for(int i=20;i>=0;i--)
		if((x>>i)&1)
		{
			if(!ck[k][i]) {ck[k][i]=x; pos[k][i]=p; break;}
			else
			{
				if(pos[k][i]<p) swap(ck[k][i],x),swap(pos[k][i],p);
				x^=ck[k][i];
			}
		}
}
int main()
{
	// freopen("in.in","r",stdin);
	// freopen("out.out","w",stdout);
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		int x; scanf("%d",&x);
		for(int j=0;j<=20;j++) ck[i][j]=ck[i-1][j],pos[i][j]=pos[i-1][j];
		ins(i,x,i);
	}
	scanf("%d",&m);
	while(m--)
	{
		int l,r,ans=0; scanf("%d%d",&l,&r);
		for(int i=20;i>=0;i--) 
			if(pos[r][i]>=l&&(ans^ck[r][i])>ans) ans^=ck[r][i];
		printf("%d\n",ans);
	}
 
	return 0;
}

最大XOR和路径

图论。

感觉难点不在线性基,重点理解异或的性质。

从简单的情况考虑,即只有一条链,肯定都选。

假如在这条链之外有环怎么办。(链不可能和环有重边,如果有,重边异或后不会有贡献,又变成链了。)

考虑走到环又走回来,中间的路径走了两边,异或后没了,所以选环就相当于选了一个单独的环。

所以我们找出 \(1 \to n\) 的链的异或和,再插入所有环的,找最大即可。

一条路径走两遍相当于没走!

图片来自:【[WC2011]最大XOR和路径】

code
#include<cstdio>
#include<math.h>
#include<algorithm>
#include<cstring>
using namespace std;
#define LL long long
const int N = 5e4+5,M = 1e5+5;
int n,m;
int head[N],tot; LL d[N];
struct E {int u,v; LL w;} e[M<<1];
inline void add(int u,int v,LL w) {e[++tot]={head[u],v,w}; head[u]=tot;}
bool vs[N];
LL ck[70];
inline void ins(LL x)
{
	for(int i=62;i>=0;i--)
		if((x>>i)&1)
		{
			if(!ck[i]) {ck[i]=x; break;}
			x^=ck[i];
		}
}
void dfs(int u,int f,LL res)
{
	d[u]=res; vs[u]=1;
	for(int i=head[u];i;i=e[i].u)
	{
		int v=e[i].v;
		if(!vs[v]) dfs(v,u,res^e[i].w);
		else ins(res^e[i].w^d[v]);
	}
}
int main()
{
	// freopen("in.in","r",stdin);
	// freopen("out.out","w",stdout);
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)
	{
		int x,y; LL z; scanf("%d%d%lld",&x,&y,&z);
		add(x,y,z); add(y,x,z);
	}
	dfs(1,0,0ll);
	LL ans=d[n];//主链的贡献
	for(int i=62;i>=0;i--) if((ans^ck[i])>ans) ans^=ck[i];
	printf("%lld\n",ans);
	return 0;
}

幸运数字

如果你知道线性基能在 \(log^2(t)\) 的复杂度内合并,就做完了。

先树剖转化为区间问题,发现就是若干个区间求异或最大值,区间内异或最大值同上。

最后将若干个区间的线性基合并,求最大值就行了。

复杂度 \(q\log(n)\log^2(t)\),vjudge 最优解。(你永远可以相信树剖)

code
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N =2e4+5;
int n,q,pos[N][62];
LL a[N],ck[N][62];
int head[N],tot;
struct E {int u,v;} e[N<<1];
inline void add(int u,int v) {e[++tot]={head[u],v}; head[u]=tot;}
inline void ins(LL x,int k,int p)
{
	for(int i=61;i>=0;i--)
		if((x>>i)&1)
		{
			if(!ck[k][i]) {ck[k][i]=x; pos[k][i]=p; break;}
			else
			{
				if(pos[k][i]<p) swap(pos[k][i],p),swap(ck[k][i],x);
				x^=ck[k][i];
			}
		}
}
int sz[N],son[N],top[N],dfn[N],num,rk[N],fa[N],dep[N];
void dfs1(int u,int f)
{
	fa[u]=f; sz[u]=1; son[u]=-1; dep[u]=dep[f]+1;
	for(int i=head[u];i;i=e[i].u)
	{
		int v=e[i].v; if(v==f) continue;
		dfs1(v,u); sz[u]+=sz[v];
		if(son[u]==-1||sz[son[u]]<sz[v]) son[u]=v;
	}
}
void dfs2(int u,int t)
{
	top[u]=t; dfn[u]=++num; rk[num]=u;
	if(son[u]==-1) return;
	dfs2(son[u],t);
	for(int i=head[u];i;i=e[i].u)
	{
		int v=e[i].v; if(v==fa[u]||v==son[u]) continue;
		dfs2(v,v);
	}
}
LL tmp[62];
inline void merge(int l,int r)
{
	for(int i=61;i>=0;i--)
	{
		if(pos[r][i]>=l)
		{
			LL tt=ck[r][i];
			for(int j=i;j>=0;j--)
				if((tt>>j)&1)
				{
					if(!tmp[j]) {tmp[j]=tt; break;}
					tt^=tmp[j];
				}
		}
	}
}
inline LL que(int x,int y)
{
	LL res=0;
	memset(tmp,0,sizeof(tmp));
	while(top[x]!=top[y])
	{
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		merge(dfn[top[x]],dfn[x]);
		x=fa[top[x]];
	}
	if(dfn[x]>dfn[y]) swap(x,y);
	merge(dfn[x],dfn[y]);
	for(int i=61;i>=0;i--) if((res^tmp[i])>res) res^=tmp[i];
	return res;
}
int main()
{
	// freopen("in.in","r",stdin);
	// freopen("out.out","w",stdout);
	scanf("%d%d",&n,&q);
	for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
	for(int i=1;i<n;i++)
	{
		int x,y; scanf("%d%d",&x,&y);
		add(x,y); add(y,x);
	}
	dfs1(1,0); dfs2(1,1);
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<=61;j++) ck[i][j]=ck[i-1][j],pos[i][j]=pos[i-1][j];
		ins(a[rk[i]],i,i);
	}
	while(q--)
	{
		int l,r; scanf("%d%d",&l,&r);
		printf("%lld\n",que(l,r));
	}
	return 0;
}

欧几里得的噩梦

题面

不需要线性基,但是题目本身就是在模拟线性基插入的过程。

性质很明显,每个二进制数只有两位是 \(1\),所以直接将这两位连边(只有一位那就和虚点连)。

显然如果成环了就不是最小的子集了,所以模拟生成树的过程加边就行。答案就是选了多少边。

code
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5+5,mod = 1e9+7;
int n,m;
int fa[N],cnt;
inline long long qpow(long long a,int b)
{
	long long res=1;
	while(b)
	{
		if(b&1) res=res*a%mod;
		a=a*a%mod; b>>=1;
	}
	return res;
}
inline int find(int x) {return x==fa[x]?(x):(fa[x]=find(fa[x]));}
vector<int> v;
int main()
{
	freopen("Euclid.in","r",stdin);
	freopen("Euclid.out","w",stdout);
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m+1;i++) fa[i]=i;
	for(int i=1;i<=n;i++)
	{
		int c,x,y=m+1; scanf("%d%d",&c,&x);
		if(c==2) scanf("%d",&y);
		int fx=find(x),fy=find(y);
		if(fx==fy) continue;
		fa[fx]=fy; cnt++;
		v.push_back(i);		
	}
	sort(v.begin(),v.end());
	printf("%lld %d\n",qpow(2,cnt),cnt);
	for(int i:v) printf("%d ",i);
	return 0;
}

装备购买

真·线性基

上面的题都是二进制下的线性基来处理异或问题,而真正的线性基其实是实数范围内的。

容易看出本题仍是求一个极大线性无关组。考虑如何将一个向量插入线性基。

类比二进制下的异或操作,我们插入的过程其实就是想消掉某一位。实数意义下的异或其实就是加减法。

假如对于向量 \(\bm{a}\)\(\bm{a}_i\) 表示第 \(i\) 维的值),我们在第 \(i\) 维已经有了基底 \(\bm{c}\),那么想要消去 \(\bm{a}\) 的第 \(i\) 维,即 \(\bm{a} \gets \bm{a}-\frac{\bm{a}_i}{\bm{c}_i} \times \bm{c}\)

其他操作和线性基一样。注意精度问题。

code
#include<bits/stdc++.h>
using namespace std;
#define LD long double
const int N = 505;
const LD del = 1e-7;
int n,m; long long ans,cnt;
struct A {int w; vector<LD> p;} a[N];
int ck[N];
inline void ins(int x)
{
	for(int j=m-1;j>=0;j--)
	{
		if(fabs(a[x].p[j])>del)
		{
			if(!ck[j]) {ck[j]=x; cnt++; ans+=a[x].w; break;}
			LD tmp=a[x].p[j]/a[ck[j]].p[j];
			for(int k=j;k>=0;k--) a[x].p[k]-=tmp*a[ck[j]].p[k];
		}
	}
}
int main()
{
	// freopen("in.in","r",stdin);
	// freopen("out.out","w",stdout);
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		a[i].p.resize(m);
		for(int j=0;j<m;j++) scanf("%Lf",&a[i].p[j]);
	}
	for(int i=1;i<=n;i++) scanf("%d",&a[i].w);
	sort(a+1,a+1+n,[&] (const A &x,const A &y) {return x.w<y.w;});
	for(int i=1;i<=n;i++) ins(i);
	printf("%d %lld\n",cnt,ans);
	return 0;
}

圣剑护符

好题捏。

容易想到树剖。发现如果路径上的数全能插进线性基,那么它一定是线性无关的,也就是不合法。

一开始想维护线性基大小,发现修改不太可做。

发现有一个很好的性质:如果多于 \(30\) 个数一定线性相关,鸽笼易证。

所以我们需要解决的就是小于三十个数是否线性相关,直接暴力插。

对于区间修改,差分!!!(总是想不到差分),用个树状数组维护一下就好了。(异或也可以用树状数组维护)

code
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
int n,q;
int head[N],tot,a[N];
struct E{int u,v;} e[N<<1];
inline void add(int u,int v) {e[++tot]={head[u],v}; head[u]=tot;}
int sz[N],top[N],son[N],fa[N],dep[N],num,rk[N],dfn[N];
void dfs1(int u,int f)
{
	fa[u]=f; dep[u]=dep[f]+1; sz[u]=1; son[u]=-1;
	for(int i=head[u];i;i=e[i].u)
	{
		int v=e[i].v; if(v==f) continue;
		dfs1(v,u); sz[u]+=sz[v];
		if(son[u]==-1||sz[son[u]]<sz[v]) son[u]=v;
	}
}
void dfs2(int u,int t)
{
	top[u]=t; dfn[u]=++num; rk[num]=u;
	if(son[u]==-1) return;
	dfs2(son[u],t);
	for(int i=head[u];i;i=e[i].u)
	{
		int v=e[i].v; if(v==fa[u]||v==son[u]) continue;
		dfs2(v,v);
	}
}
inline int lca(int x,int y)
{
	while(top[x]!=top[y])
	{
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		x=fa[top[x]];
	}
	if(dfn[x]>dfn[y]) swap(x,y);
	return x;
}
namespace BIT
{
	int c[N];
	inline void _mdf(int x,int v) {for(;x<=n;x+=(x&-x)) c[x]^=v;}
	inline int que(int x) {int res=0; for(;x;x-=(x&-x)) res^=c[x]; return res;}
	inline void mdf(int l,int r,int v) {_mdf(l,v); _mdf(r+1,v);}
} using namespace BIT;
int tmp[31];
bool ins(int l,int r)
{
	for(int i=l;i<=r;i++)
	{
		bool fl=0; int x=que(i);
		for(int j=30;j>=0;j--)
			if((x>>j)&1)
			{
				if(!tmp[j]) {tmp[j]=x; fl=1; break;}
				x^=tmp[j];
			}
		if(!fl) return 1;
	}
	return 0;
}
bool inspath(int x,int y)
{
	memset(tmp,0,sizeof(tmp));
	while(top[x]!=top[y])
	{
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		if(ins(dfn[top[x]],dfn[x])) return 1;
		x=fa[top[x]];
	}
	if(dfn[x]>dfn[y]) swap(x,y);
	if(ins(dfn[x],dfn[y])) return 1;
	return 0;
}
void mdfpath(int x,int y,int z)
{
	while(top[x]!=top[y])
	{
		if(dep[top[x]]<dep[top[y]]) swap(x,y);
		mdf(dfn[top[x]],dfn[x],z);
		x=fa[top[x]];
	}
	if(dfn[x]>dfn[y]) swap(x,y);
	mdf(dfn[x],dfn[y],z);
}
int main()
{
	// freopen("in.in","r",stdin);
	// freopen("out.out","w",stdout);
	scanf("%d%d",&n,&q);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<n;i++)
	{
		int x,y; scanf("%d%d",&x,&y);
		add(x,y); add(y,x);
	}
	dfs1(1,0); dfs2(1,1);
	for(int i=1;i<=n;i++) _mdf(i,a[rk[i]]^a[rk[i-1]]);
	// for(int i=1;i<=n;i++) printf("%d ",que(i)); putchar('\n');
	while(q--)
	{
		string c; cin>>c;
		if(c[0]=='Q')
		{
			int x,y; scanf("%d%d",&x,&y);
			int l=lca(x,y),dis=dep[x]+dep[y]-(dep[l]<<1)+1;
			if(dis>30) printf("YES\n");
			else
			{
				if(inspath(x,y)) printf("YES\n");
				else printf("NO\n");
			}
		}
		else
		{
			int x,y,z; scanf("%d%d%d",&x,&y,&z);
			mdfpath(x,y,z);
		}
	}
	return 0;
}
posted @ 2024-10-10 10:57  ppllxx_9G  阅读(47)  评论(9编辑  收藏  举报