线性基学习笔记

参考博客:线性基学习笔记,本文仅仅用来个人理解。

线性基是针对某个序列生成的一个集合,它具有以下两条性质:

  1. 线性基中任意选择一些数的异或值所构成的集合,等于原序列中任意选择一些数的异或值所构成的集合。
  2. 线性基是满足上述条件的最小集合

线性基的一些推论:

  1. 原序列中任何数,都可以由线性基中一些数异或起来得到。
  2. 线性基中不存在一组数,使得它们的异或值为 \(0\),否则不满足最小的性质。

对于线性基贪心法构建的证明,如果有问题欢迎指出:

首先,如果有两个最高为一的位置一致的,显然有一个没有答案不劣。

假设分别为 \(x,y\),它们可以搭配出 \(x,y,x\operatorname{xor} y\)

考虑去掉 \(x\) 那里为 \(1\) 的,那么直接让 \(x = x\operatorname{xor} y\) 即可,这样还是可以搭配出 \(x\operatorname{xor} y,y,x\) 等价替换掉了。

那么从高往低扫,如果这一位为 \(1\),就必须尝试用 \(d_i\) 去异或掉,否则没有 \(d_i\) 就加进去这个。

接下来的部分偏口胡,只是帮助理解一下。

假设 \(x\) 因为 \(x\) 异或 \(d_i\) 导致接下来多了一个放置,\(d_i\) 是上一个点放置的位置,由于插入顺序对线性基大小没影响,我们可以任意交换,所以我们只对于相邻的看。

那么考虑把 \(d_i\) 换成一个不会让 \(x\) 多放,同时使得原来的不被影响,容易发现这是不可能的。

如果 \(d_i\) 改变,那么上一个点就异或上这里后一定不会变成 \(0\),会在后面放置。

当然了,我们还有高斯消元法求线性基,不过作者没学,可以去 oi wiki 看。

一般来讲贪心法构建线性基复杂度是 \(O(V)\) 的,不过在位数特别大的时候需要 bitset 存储,复杂度 \(O(\frac{V^2}{\omega})\)

接下来一般来讲 \(V\) 都是线性基大小。

Part1. 线性基基础操作

I.求 \(\max\) 异或值(即【模板】线性基

贪心的从高往低选,如果选了变大就选。然后求出来的就是答案了。

原因:

线性基中选任意数异或出来的集合会等于原数组中任意数异或出来的集合。

那么原数组中选等价与在线性基中选。

然后线性基中每个 \(d_i\) 都是二进制下第 \(i\) 位为 \(1\)\(i+1\) 之后的都是 \(0\),所以肯定按 \(i\) 从大到小贪心的选,毕竟 \(2^i > 2^0+2^1+...+2^{i-1}\)

II.求 \(\min\) 异或值

其实我一开始学的时候就在想,怎么没有求 \(\min\) 异或值的题?也许是因为这个的各方面难度都是小于求 \(\max\) 的,所以没有开设的必要。

还是利用线性基,不过注意一个东西,如果有 \(a_i\) 能被线性基完全表示,即插入失败,那么答案就是 \(0\),就是它异或上哪些值。

所以有一个性质,如果 \(n > V\),那么 \(\min\) 异或值就是 \(0\)\(V\) 是二进制下值域的位数,原因是因为线性基大小最多为 \(V\),所以最多成功 \(V\) 次。

如果没有任何插入失败时,答案为 \(\min d_i\),而且 \(d_i\) 必须有值,其实 \(i\) 就是最后一个有值的地方。

原因跟 \(\max\) 的一样,读者可以自行思考。

III.求第 \(k\) 小异或(即 LOJ#114.k大异或和

在此之前先说一个东西,对于一个求出来的线性基,\(d_i\) 异或上 \(d_j\) 之后仍然是原来的一组线性基(\(j\) 不等于 \(i\))。换句话说,线性基具有异或等价性。

由于要求第 \(k\) 小异或,所以我们需要一个每个 \(d_i\) 都是最小的线性基。

构造是容易的,考虑对于每个 \(d_i\) 从大到小枚举 \(j < i\),然后如果 \(d_i \operatorname{xor} d_j < d_i\),那么 \(d_i \operatorname{xor} d_j\),否则不变,其实可以理解为如果 \(d_i\) 的第 \(j\) 位为 \(1\),就尝试删掉,如果能删掉,就算异或上后面都变成一它的值都是变小了,其实就是一个贪心的过程。

然后我们就求出来了一个每个 \(d_i\) 都是最小的线性基,接下来的解法我觉得那位大佬讲的太复杂了,说一下我的理解吧,先不考虑有插入失败,即最小值不是 \(0\) 的情况。

先假设有值的 \(d_i\) 分别是 \(d_{b_0},d_{b_1},d_{b_2}\) 等等,那么最小的肯定是 \(d_{b_0}\),然后就是 \(d_{b_1}\),然后是 \(d_{b_0} \operatorname{xor} d_{b_1}\)

根据线性基的性质 \(d_i\) 是前 \(i\) 个中唯一一个在二进制下第 \(i\) 位为 \(1\) 的点,也就是说前面 \(i-1\) 个点随便怎么异或值都小于 \(d_i\),而前面组成的总数就是 \(2^i-1\)(前提前面每个 \(d_j\) 都有值)。

对于 \(d_{b_i}\) 来说,前 \(2^i-1\) 小就是按前面的顺序求出来的,第 \(2^i\) 小就是 \(d_{b_i}\),然后 \(2^i+1\)\(2^{i+1}-1\) 小就是选择了 \(d_i\),然后按前面 \(2^i-1\) 小的顺序求出来。

所以对于求第 \(k\) 小,若存在,那么设其二进制下 \(1\) 的位置分别是 \(c_1,c_2,...\),那么答案就是 \(d_{b_{c_1}} \operatorname{xor} d_{b_{c_2}} \operatorname{xor} ...\)

读者可以自行理解一下,笔者太菜了不知道怎么描述

实现细节的话就是先看有没有插入失败的,然后就是要看是否存在第 \(k\) 小,即 \(k \le 2^{tot}-1\)\(tot\) 是有值的 \(d_i\) 的个数,注意到不是\(2^{tot+1}-1\) 是因为 \(b\) 是从 \(0\) 开始的,而 \(tot\) 是统计的个数。

code

#include<bits/stdc++.h>
using namespace std;
#define int long long
namespace IO
{
	template<typename T>
	void read(T &_x){_x=0;int _f=1;char ch=getchar();while(!isdigit(ch)) _f=(ch=='-'?-1:_f),ch=getchar();while(isdigit(ch)) _x=_x*10+(ch^48),ch=getchar();_x*=_f;}
	template<typename T,typename... Args>
	void read(T &_x,Args&...others){Read(_x);Read(others...);}
	const int BUF=20000000;char buf[BUF],top,stk[32];int plen;
	#define pc(x) buf[plen++]=x
	#define flush(); fwrite(buf,1,plen,stdout),plen=0;
	template<typename T>inline void print(T x){if(!x){pc(48);return;}if(x<0) x=-x,pc('-');for(;x;x/=10) stk[++top]=48+x%10;while(top) pc(stk[top--]);}
}
using namespace IO;
const int N = 1e5+10,M = 50;
int n,m,x,d[M+10],sum,o,ly,ans;
inline void xxj(int x)
{
	for(int i = M;i >= 0;i--)
		if(((1ll<<i)&x))
		{
			if(!d[i]){ d[i] = x; return; }
			else x ^= d[i];
		}
	o = 1;
}
signed main()
{
	//freopen(".in","r",stdin);
	//freopen(".out","w",stdout);
	read(n);
	for(int i = 1;i <= n;i++) read(x),xxj(x);
	for(int i = 1;i <= M;i++)//求出最小xxj 
		if(d[i])
			for(int j = i-1;j >= 0;j--)
				if((d[i]^d[j]) < d[i])
					d[i] = (d[i]^d[j]);
	for(int i = 0;i <= M;i++) sum += (d[i]!=0);
	read(m);
	while(m--)
	{
		read(x); x -= o; ans = ly = 0;
		if(x >= (1ll<<sum)){ print(-1),pc('\n'); continue; }
		if(!x){ print(0),pc('\n'); continue; }
		for(int i = 0;i <= M;i++)
			if(d[i])
			{
				if(((1ll<<ly)&x))
					ans ^= d[i];
				ly++;
			}
		print(ans),pc('\n');
	}
	flush();
	return 0;
}

IV.线性基合并

例题:[SCOI2016]幸运数字P4839 P 哥的桶

线性基是具有可并性的,原因很显然,不妨在回顾一下线性基的性质,从它里面任意选数异或出来的集合等同于原数组任意选数异或出来的集合,所以一个区间的线性基可以理解为两个区间的线性基合并。

合并方式也很简单,把一边的暴力放入另一边,复杂度 \(V^2\)\(V\) 是线性基大小,一般是 \(\log\) 大小的。

然后我们根据题目要求随便套点数据结构就可以了,复杂度 \(\log^3\)

当然并非所有情况都是 \(\log^2\),详情可以看 Part7。

对了,套数据结构的话空间一般是 \(log^2\) 的,开空间时不要太奢侈等会超空保龄就老实了

这里详细说一下幸运数字。

对了,线性基是支持 \(RMQ\) 的,因为重复部分插入多次在此题中是没有影响的,所以此题我们还是先倍增,然后每次询问我们不需要严丝合缝的去合并,而是对于每条线段找两个极大区间恰好能完全覆盖,然后合并,复杂度可以做到 \(n\log^3+m\log^2\),求这两个点可以倍增跳,不影响整体复杂度,能够轻松通过。

code

#include<bits/stdc++.h>
using namespace std;
#define int long long
namespace IO
{
	template<typename T>
	void read(T &_x){_x=0;int _f=1;char ch=getchar();while(!isdigit(ch)) _f=(ch=='-'?-1:_f),ch=getchar();while(isdigit(ch)) _x=_x*10+(ch^48),ch=getchar();_x*=_f;}
	template<typename T,typename... Args>
	void read(T &_x,Args&...others){Read(_x);Read(others...);}
	const int BUF=20000000;char buf[BUF],top,stk[32];int plen;
	#define pc(x) buf[plen++]=x
	#define flush(); fwrite(buf,1,plen,stdout),plen=0;
	template<typename T>inline void print(T x){if(!x){pc(48);return;}if(x<0) x=-x,pc('-');for(;x;x/=10) stk[++top]=48+x%10;while(top) pc(stk[top--]);}
}
using namespace IO;
const int N = 2e4+10,M = 60;
int n,q,a[N],head[N],cnt,x,y,z,k,ans,o;
int fa[N][15],id[N][15],dep[N],lg[N],cnt1;
int d[N*15][M+5];//xxj 
struct w
{
	int to,nxt;
}b[N<<1];
inline void add(int x,int y)
{
	b[++cnt].nxt = head[x];
	b[cnt].to = y;
	head[x] = cnt;
}
inline void xxj(int x,int y)//额外需要一个下标
{
	for(int i = M;i >= 0;i--)
		if(((1ll<<i)&y))
		{
			if(!d[x][i]){ d[x][i] = y; break; }
			else y ^= d[x][i];
		}
} 
inline void merge(int x,int y)//x -> y
{
	for(int i = M;i >= 0;i--)
		if(d[x][i]) xxj(y,d[x][i]);
}
void dfs(int x,int y)
{
	dep[x] = dep[y]+1; fa[x][0] = y,id[x][0] = ++cnt1; xxj(cnt1,a[x]),xxj(cnt1,a[y]);
	for(int i = 1;i <= lg[dep[x]];i++) 
	{
		fa[x][i] = fa[fa[x][i-1]][i-1],id[x][i] = ++cnt1;
		merge(id[x][i-1],id[x][i]),merge(id[fa[x][i-1]][i-1],id[x][i]);
	}
	for(int i = head[x];i;i = b[i].nxt)
		if(b[i].to != y)
			dfs(b[i].to,x);
}
inline int Get(int x,int y)//返回x向上移动y次的下标 
{
	while(y) o = (y&-y),x = fa[x][lg[o]],y -= o;
	return x;
}
inline void query(int x,int y,int X,int Y)
{//注意接下来的都没有加一,因为我写的id_{x,i} 表示管理x到x向上2^i,而不是2^i-1 
	if(dep[x] < dep[y]) swap(x,y),swap(X,Y);
	while(dep[x] != dep[y]) x = fa[x][lg[dep[x]-dep[y]]];
	if(x == y)
	{
		k = lg[dep[X]-dep[Y]];
		merge(id[X][k],cnt1);
		merge(id[Get(X,dep[X]-dep[Y]-(1<<k))][k],cnt1);
		return;
	}
	for(int i = lg[dep[x]];i >= 0;i--)
		if(fa[x][i] != fa[y][i])
			x = fa[x][i],y = fa[y][i];
	x = fa[x][0];
	k = lg[dep[X]-dep[x]];
	merge(id[X][k],cnt1);
	merge(id[Get(X,dep[X]-dep[x]-(1<<k))][k],cnt1);
	k = lg[dep[Y]-dep[x]];
	merge(id[Y][k],cnt1);
	merge(id[Get(Y,dep[Y]-dep[x]-(1<<k))][k],cnt1);
}
signed main()
{
//	freopen("P3292_1.in","r",stdin);
//	freopen(".out","w",stdout);
	read(n),read(q);
	for(int i = 1;i <= n;i++) read(a[i]);
	for(int i = 1;i < n;i++) read(x),read(y),add(x,y),add(y,x);
	for(int i = 2;i <= n;i++) lg[i] = lg[i/2]+1;
	dfs(1,0); ++cnt1;//额外开一个就是答案xxj 
	while(q--)
	{
		for(int i = M;i >= 0;i--) d[cnt1][i] = 0;
		read(x),read(y); ans = 0;
		if(x == y) xxj(cnt1,a[x]);//十分特殊,特判掉 
		else query(x,y,x,y);
		for(int i = M;i >= 0;i--)
			if((ans^d[cnt1][i]) > ans)
				ans ^= d[cnt1][i];
		print(ans),pc('\n');
	}
	flush();
	return 0;
}

Part2.线性基上排序与贪心

I.P4570 [BJWC2011] 元素

显然,最终选择的矿石一定构成线性基,若不然肯定还会选一些,然后线性基的定义都说了里面的任意数异或起来不会为 \(0\)

考虑每次插入会发生什么。如果能插入就插入,否则,考虑 \(x\)\(d_i,d_j,...\) 异或而来,那么 \(d_i,d_j,...,x\) 异或起来为 \(0\),从中随便扔掉一个就好了。

所以,我们在插入时从大到小插入,就不会有替换的情况了。

code

#include<bits/stdc++.h>
using namespace std;
#define int long long
namespace IO
{
	template<typename T>
	void read(T &_x){_x=0;int _f=1;char ch=getchar();while(!isdigit(ch)) _f=(ch=='-'?-1:_f),ch=getchar();while(isdigit(ch)) _x=_x*10+(ch^48),ch=getchar();_x*=_f;}
	template<typename T,typename... Args>
	void read(T &_x,Args&...others){Read(_x);Read(others...);}
	const int BUF=20000000;char buf[BUF],top,stk[32];int plen;
	#define pc(x) buf[plen++]=x
	#define flush(); fwrite(buf,1,plen,stdout),plen=0;
	template<typename T>inline void print(T x){if(!x){pc(48);return;}if(x<0) x=-x,pc('-');for(;x;x/=10) stk[++top]=48+x%10;while(top) pc(stk[top--]);}
}
using namespace IO;
const int M = 60,N = 1e3+10;
int n,d[M+10],ans;
struct w
{
	int x,y;
}a[N];
inline bool cmp(w x,w y){ return x.y > y.y; }
inline void xxj(int x,int y)
{
	for(int i = M;i >= 0;i--)
		if(((1ll<<i)&x))
		{
			if(!d[i]){ d[i] = x; ans += y; return; }
			else x ^= d[i];
		}
}
signed main()
{
	//freopen(".in","r",stdin);
	//freopen(".out","w",stdout);
	read(n);
	for(int i = 1;i <= n;i++) read(a[i].x),read(a[i].y);
	sort(a+1,a+1+n,cmp);
	for(int i = 1;i <= n;i++) xxj(a[i].x,a[i].y);
	print(ans); flush();
	return 0;
}

II.[CQOI2013] 新Nim游戏

回顾一下 Nim 游戏先手必败条件是所有点异或值为 \(0\)

那么如果第一回合到对手时,有一个子集异或起来为 \(0\),那么对手就比胜。

换句话说,先手必须要在操作完后保证任意一个子集异或起来都不为 \(0\),当然不包括空集。

这不就是线性基定义吗,所以我们按元素那道题一样排序然后线性基插入即可。

Part3.每个异或值出现了多少次

I.[TJOI2008]彩灯

练习题,不讲。

II.P4869 albus就是要第一个出场

好神秘的题目名。

可以理解为求第 \(k\) 小,不过是可重集,容易猜到是每个出现 \(2^{n-tot}\) 次。

证明其实很容易。

考虑匹配失败的都可以表示为线性基若干个异或起来的形式。

对于一个值 \(x\),假设它可以由线性基中若干个数异或得到。

哦对了,先说一个东西,线性基里面每一个 \(d_i\),其实都是由那插入的 \(tot\) 个数部分异或起来得到的。

这个应该很显然。

然后继续,那么失配的随便选,是 \(2^{n-tot}\),然后异或完的值其实就是线性基里若干个 \(d_i\) 异或起来得到。

然后那个 \(x\) 是由线性基若干个数异或得到。

根据 \(x\) 看哪些 \(d_i\) 多了或者少了,假设补上的为 \(y\)

那么 \(y\) 肯定能由这 \(tot\) 个数搞出来。

\(tot\) 个数能够组成 \(2^{tot}\) 个互不相同的,每一个有 \(2^{n-tot}\) 个。

知道这个后,直接二分答案即可。

code

#include<bits/stdc++.h>
using namespace std;
#define int long long
namespace IO
{
	template<typename T>
	void read(T &_x){_x=0;int _f=1;char ch=getchar();while(!isdigit(ch)) _f=(ch=='-'?-1:_f),ch=getchar();while(isdigit(ch)) _x=_x*10+(ch^48),ch=getchar();_x*=_f;}
	template<typename T,typename... Args>
	void read(T &_x,Args&...others){Read(_x);Read(others...);}
	const int BUF=20000000;char buf[BUF],top,stk[32];int plen;
	#define pc(x) buf[plen++]=x
	#define flush(); fwrite(buf,1,plen,stdout),plen=0;
	template<typename T>inline void print(T x){if(!x){pc(48);return;}if(x<0) x=-x,pc('-');for(;x;x/=10) stk[++top]=48+x%10;while(top) pc(stk[top--]);}
}
using namespace IO;
const int N = 1e5+10,M = 30,mod = 10086;
int n,a[N],x,d[M+10],l,r,mid,tot,o,sum,ans;
inline int ksm(int x,int p)
{
	int ans = 1;
	while(p)
	{
		if((p&1)) ans = ans*x%mod;
		p>>=1,x = x*x%mod;
	}
	return ans;
}
inline void xxj(int x)
{
	for(int i = M;i >= 0;i--)
		if(((1<<i)&x))
		{
			if(!d[i]){ d[i] = x; return; }
			else x ^= d[i];
		}
}
signed main()
{
	//freopen(".in","r",stdin);
	//freopen(".out","w",stdout);
	read(n);
	for(int i = 1;i <= n;i++) read(a[i]),xxj(a[i]);
	read(x);
	for(int i = 0;i <= M;i++)
		for(int j = 0;j < i;j++)
			if((d[i]^d[j]) < d[i]) d[i] ^= d[j];//要变成最小线性基 
	for(int i = M;i >= 0;i--) if(d[i]) tot++;
	l = 0,r = (1<<tot)-1;
	while(l <= r)
	{
		mid = ((l+r)>>1); o = sum = 0;
		for(int i = 0;i <= M;i++)
			if(d[i])
			{
				if(((1<<o)&mid)) sum ^= d[i];
				o++;
			}
		if(sum > x) r = mid-1;
		else if(sum == x){ ans = mid%mod*ksm(2,n-tot)%mod; break; }
		else l = mid+1;
	}
	print((ans+1)%mod); flush();
	return 0;
}
/*
可以理解为求第k小,不过是可重集
可以证明,其实就是原来的,每个出现2^{n-tot}次
证明很显然:
首先求出一组线性基
然后所有匹配失败的都可以表示为里面若干个异或
然后线性基异或出来的集合和原来的一样
假设是x,那么从匹配失败的里面随便选
然后异或完后,每个d_i要么出现了要么没出现
然后这里面的我是没选择过的
比如d_i出现,而我不需要那么^d_i
d_i没出现,但我需要也^d_i,其它不管
那么这个数y我们可以用那tot个数表示出来
回到这个题,我们直接二分答案即可
对于下标问题,注意到值域<=1e9
也就是异或出来不同的数在int范围内
这个我们可以直接二分求出来
然后在取模就好啦 
*/ 

III.CF959F Mahmoud and Ehab and yet another xor task

洛谷访问连接

容易发现答案要么是 \(0\) 要么是 \(2^{n-tot}\)

只需前缀询问,直接可持久化线性基即可。

嗯,其实就是暴力 copy 一遍前面的然后在插入,复杂度 \(nV\)

至于查询线性基是否能构成 \(x\),那更简单了,你插入进去如果无法插入那不就是能被表示吗。

code

#include<bits/stdc++.h>
using namespace std;
#define int long long
namespace IO
{
	template<typename T>
	void read(T &_x){_x=0;int _f=1;char ch=getchar();while(!isdigit(ch)) _f=(ch=='-'?-1:_f),ch=getchar();while(isdigit(ch)) _x=_x*10+(ch^48),ch=getchar();_x*=_f;}
	template<typename T,typename... Args>
	void read(T &_x,Args&...others){Read(_x);Read(others...);}
	const int BUF=20000000;char buf[BUF],top,stk[32];int plen;
	#define pc(x) buf[plen++]=x
	#define flush(); fwrite(buf,1,plen,stdout),plen=0;
	template<typename T>inline void print(T x){if(!x){pc(48);return;}if(x<0) x=-x,pc('-');for(;x;x/=10) stk[++top]=48+x%10;while(top) pc(stk[top--]);}
}
using namespace IO;
const int N = 1e5+10,M = 20,mod = 1e9+7;
int n,q,a[N],x,y,d[N][M+10],tot;
inline int ksm(int x,int p)
{
	int ans = 1;
	while(p)
	{
		if((p&1)) ans = ans*x%mod;
		p>>=1,x = x*x%mod;
	}
	return ans;
}
inline void xxj(int x,int y)
{
	for(int i = M;i >= 0;i--)
		if(((1<<i)&y))
		{
			if((!d[x][i])) { d[x][i] = y; return; }
			else y ^= d[x][i];
		}
}
inline bool check(int x,int y)//检查线性基x里是否存在y
{
	for(int i = M;i >= 0;i--)
		if(((1<<i)&y)) y ^= d[x][i];
	return (y==0); 
} 
signed main()
{
	//freopen(".in","r",stdin);
	//freopen(".out","w",stdout);
	read(n),read(q);
	for(int i = 1;i <= n;i++)
	{
		for(int j = M;j >= 0;j--) d[i][j] = d[i-1][j];
		read(a[i]),xxj(i,a[i]);
	}
	while(q--)
	{
		read(x),read(y); tot = 0;
		for(int i = M;i >= 0;i--) tot += (d[x][i]!=0);
		if(check(x,y)) print(ksm(2,x-tot)),pc('\n');
		else print(0),pc('\n');
	}
	flush();
	return 0;
}
/*
直接做就好了
答案要么0,要么2^{n-tot}
至于前缀询问,直接可持久化线性基即可
具体就暴力copy下来就没了 
*/

IV.CF895C Square Subsets

洛谷链接

注意到 \(a_i \le 70\),考虑把 \(a_i\) 搞成二进制的形式,这样搞完就是在求选出若干个数异或起来为 \(0\)

答案就是 \(2^{n-tot}-1\),注意减去空集。

据说还有一道双倍经验是:200. Cracking RSA,不过好像需要更高的精度。

code

#include<bits/stdc++.h>
using namespace std;
#define int long long
namespace IO
{
	template<typename T>
	void read(T &_x){_x=0;int _f=1;char ch=getchar();while(!isdigit(ch)) _f=(ch=='-'?-1:_f),ch=getchar();while(isdigit(ch)) _x=_x*10+(ch^48),ch=getchar();_x*=_f;}
	template<typename T,typename... Args>
	void read(T &_x,Args&...others){Read(_x);Read(others...);}
	const int BUF=20000000;char buf[BUF],top,stk[32];int plen;
	#define pc(x) buf[plen++]=x
	#define flush(); fwrite(buf,1,plen,stdout),plen=0;
	template<typename T>inline void print(T x){if(!x){pc(48);return;}if(x<0) x=-x,pc('-');for(;x;x/=10) stk[++top]=48+x%10;while(top) pc(stk[top--]);}
}
using namespace IO;
const int N = 1e5+10,M = 19,mod = 1e9+7;
int n,a[N],d[M],o,x,ans,pri[M+10] = {2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67};//可以打表打出来质数有哪些 
inline void xxj(int x)
{
	for(int i = M;i >= 0;i--)
		if(((1<<i)&x))
		{
			if(!d[i]){ d[i] = x; return; }
			else x ^= d[i];
		}
	ans = ans*2%mod;//失配 
}
signed main()
{
	//freopen(".in","r",stdin);
	//freopen(".out","w",stdout);
	read(n); ans = 1;
	for(int i = 1;i <= n;i++) 
	{
		read(a[i]); x = 0;
		for(int j = 0;j < M;j++) 
		{
			o = 0;
			while(a[i]%pri[j] == 0) a[i] /= pri[j],o = !o;
			x += (1<<j)*o; 
		} xxj(x);
	} ans = (ans-1+mod)%mod;
	print(ans),pc('\n');
	flush(); 
	return 0;
}

V.TopCoder13145-PerfectSquare

习题,请自行尝试。

Part4.线性基在图论方面的应用

I.P4151 [WC2011] 最大XOR和路径

很经典的线性基题,说说我的理解。

首先,这个问题感觉很不可做,考虑画图自己模拟一下,然后我发现了一个东西,就是假设 \(x\) 要到 \(y\),那么只能经过奇数次不同路径。

然后我发现我在走的时候可以去其它地方取若干个环在回来。

大概就是首先有一条从 \(1\)\(n\) 的路径,然后我可以选择若干环,诶这个环可以和路径有交集,相当于不走这边,换一边走。

想一想发现很合理,只能经过奇数次其实就是环加一条链,所以我们随便找一条 \(1\)\(n\) 的路径,然后把环加入线性基,然后异或一下就好了。

找环直接 dfs,只需要简单环,也就是链加返祖边,因为两个有交集的环异或起来就变成新环啦,所以不管跳了 \(>1\) 条返祖边的。

#include<bits/stdc++.h>
using namespace std;
#define int long long
namespace IO
{
	template<typename T>
	void read(T &_x){_x=0;int _f=1;char ch=getchar();while(!isdigit(ch)) _f=(ch=='-'?-1:_f),ch=getchar();while(isdigit(ch)) _x=_x*10+(ch^48),ch=getchar();_x*=_f;}
	template<typename T,typename... Args>
	void read(T &_x,Args&...others){Read(_x);Read(others...);}
	const int BUF=20000000;char buf[BUF],top,stk[32];int plen;
	#define pc(x) buf[plen++]=x
	#define flush(); fwrite(buf,1,plen,stdout),plen=0;
	template<typename T>inline void print(T x){if(!x){pc(48);return;}if(x<0) x=-x,pc('-');for(;x;x/=10) stk[++top]=48+x%10;while(top) pc(stk[top--]);}
}
using namespace IO;
const int N = 5e4+10,M = 60;
int n,m,x,y,z,head[N],dis[N],cnt,d[M+10],v[N],ans;
struct w
{
	int to,nxt,z;
}b[N<<2];
inline void add(int x,int y,int z)
{
	b[++cnt].nxt = head[x];
	b[cnt].to = y,b[cnt].z = z;
	head[x] = cnt;
}
inline void xxj(int x)
{
	for(int i = M;i >= 0;i--)
		if(((1ll<<i)&x))
		{
			if(!d[i]){ d[i] = x; return; }
			else x ^= d[i];
		}
}
void dfs(int x,int y,int z)
{
	dis[x] = z,v[x] = 1;
	for(int i = head[x];i;i = b[i].nxt)
		if(v[b[i].to]) xxj(z^dis[b[i].to]^b[i].z);
		else dfs(b[i].to,x,z^b[i].z);
}//找环放入 
signed main()
{
	//freopen(".in","r",stdin);
	//freopen(".out","w",stdout);
	read(n),read(m);
	for(int i = 1;i <= m;i++) read(x),read(y),read(z),add(x,y,z),add(y,x,z);
	dfs(1,0,0); ans = dis[n];
	for(int i = M;i >= 0;i--)
		if((ans^d[i]) > ans)
			ans ^= d[i];//求最大值 
	print(ans); flush();
	return 0;
}

II.CF845G Shortest Path Problem?

洛谷链接

这与上面是一样的,只是求最大变成求最小了,改一下就好啦,代码就不放了。

双倍经验:P12259 [蓝桥杯 2024 国 Java B] 最优路径

篮球杯出原题

注意一下双倍经验要额外看一下两个点是否联通,数据范围足以让我们暴力枚举不愧是暴力杯

注意无解输出 \(-1\),开始结束点不能一样,还有就是记得每次搞完清空数组。

III.P3733 [HAOI2017] 八纵八横

好酷的名字诶。

首先,修改操作理解成一次删除和一次加,然后根据上上题的性质,那么此题其实就是找若干个简单环的异或最大值,直接线性基就可以了。

不过注意到每条边是有存在时间的,不过注意到题目保证原图联通,直接先跑原图就出来,\(dis_i\) 就是 \(1\)\(i\) 的一条路径异或和,然后每次加边删边就理解为是在加入删除 \(dis_x \operatorname{xor} dis_y \operatorname{xor} z\)

考虑一个叫可删除线性基的东西,我们额外记一个时间戳,然后保留时间最大的。

具体就是我们额外记录一个时间戳,然后对于这一位如果为 \(1\)

没有,就放进去。 否则,保留一个时间大的,然后去跑小的。

这样求得时候只需要保证 \(id_{x,i} \ge l\) 就说明存在。

为什么这样一定不劣,首先显然我们希望位置高的失效时间尽可能在后面,然后就是如果交换了 \(y,d_{x,i}\),容易发现换不换,值都会变成 \(y \operatorname{xor} d_{x,i}\),所以保留时间大的一定不劣。

注意到此题值域较大,考虑用 bitset 存储,具体实现见代码:

#include<bits/stdc++.h>
using namespace std;
#define int long long
//#define getchar() (p1 == p2 && (p2 = (p1 = buf1) + fread(buf1, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
//char buf1[1 << 23], *p1 = buf1, *p2 = buf1, ubuf[1 << 23], *u = ubuf;
namespace IO
{
	template<typename T>
	void read(T &_x){_x=0;int _f=1;char ch=getchar();while(!isdigit(ch)) _f=(ch=='-'?-1:_f),ch=getchar();while(isdigit(ch)) _x=_x*10+(ch^48),ch=getchar();_x*=_f;}
	template<typename T,typename... Args>
	void read(T &_x,Args&...others){Read(_x);Read(others...);}
	const int BUF=20000000;char buf[BUF],to,stk[32];int plen;
	#define pc(x) buf[plen++]=x
	#define flush(); fwrite(buf,1,plen,stdout),plen=0;
	template<typename T>inline void print(T x){if(!x){pc(48);return;}if(x<0) x=-x,pc('-');for(;x;x/=10) stk[++to]=48+x%10;while(to) pc(stk[to--]);}
}
using namespace IO;
const int N = 1010,O = 1000;
int n,m,q,x,y,head[N],cnt,L,cnt1;
bitset<N>d[N],dis[N],B;
string s;
int v[N],id[N],bj[N],ly[N];
struct w1
{
	int x,y,l,r;
	bitset<N>z;
}Q[N];
struct w
{
	int to,nxt;
	bitset<N>z;
}b[N<<1];
inline void add(int x,int y)
{
	bitset<N>z; cin >> z;
	b[++cnt].nxt = head[x];
	b[cnt].to = y,b[cnt].z = z;
	head[x] = cnt;
	
	b[++cnt].nxt = head[y];
	b[cnt].to = x,b[cnt].z = z;
	head[y] = cnt;
}
inline void xxj(bitset<N>x,int id)
{
	for(int i = O;i >= 0;i--)
		if(x[i])
		{
			if(!d[i][i]){ d[i] = x,v[i] = id; return; }
			else 
			{
				if(v[i] < id) swap(v[i],id),swap(x,d[i]);//取时间大的 
				x ^= d[i];
			}
		}
} 
void dfs(int x,int y,bitset<N>z)
{
	bj[x] = 1; dis[x] = z;
	for(int i = head[x];i;i = b[i].nxt)
		if(ly[i] != ly[y])
		{
			if(!bj[b[i].to]) dfs(b[i].to,i,z^b[i].z);
			else xxj(z^dis[b[i].to]^b[i].z,q+1);//必定出现,就赋个>q的 
		}
}
inline bool cmp(w1 x,w1 y){ return x.l < y.l;}
signed main()
{
	read(n),read(m),read(q);
	for(int i = 1;i <= m;i++) read(x),read(y),add(x,y),ly[cnt] = ly[cnt-1] = i;
	dfs(1,0,0); cnt = cnt1 = 0;
	for(int i = 1;i <= q;i++)
	{
		cin >> s;
		if(s == "Add") 
		{
			read(x),read(y);
			Q[++cnt].x = x,id[++cnt1] = cnt,Q[cnt].l = i,Q[cnt].y = y,cin >> Q[cnt].z,Q[cnt].r = q+1;
		}
		else if(s == "Cancel") read(x),Q[id[x]].r = i-1;
		else
		{
			read(x); 
			Q[++cnt].x = Q[id[x]].x,Q[cnt].y = Q[id[x]].y,Q[cnt].l = i;
			cin >> Q[cnt].z; Q[cnt].r = q+1; Q[id[x]].r = i-1; id[x] = cnt;
		}//注意这里的处理,id_i表示第i号高铁的实际下标,不要赋错了 
	} L = 1;
	sort(Q+1,Q+1+cnt,cmp);
	for(int i = 0;i <= q;i++)
	{
		while(L <= cnt && Q[L].l == i) xxj(Q[L].z^dis[Q[L].x]^dis[Q[L].y],Q[L].r),L++;
		B.reset(); for(int j = O;j >= 0;j--) if(!B[j] && i <= v[j]) B^=d[j]; 
		bool op = 0;
		for(int i = O;i >= 0;i--)
			if(B[i])
			{ op = 1;
				for(int j = i;j >= 0;j--) putchar('0'+B[j]);
				putchar('\n'); break;
			}//输出处理 
		if(!op) putchar('0'),putchar('\n');
	}
	flush();
	return 0;
}
/*
修改看为删除和加入
对每条铁路看其结束时间
先按原图跑,然后每次就多一个dis_x^dis_y^z的环
然后搞一个所谓的可删除线性基,我们希望越高位结束时间越晚
然后每次求就是只看v_i>=i的
复杂度大概是qlen^2/w的 
*/

当然,注意到线性基是其实是可撤销的,所以可以直接上线段树分治,复杂度应该是多一只 \(\log\),不过也能过。

IV.CF938G Shortest Path Queries

洛谷链接

Part5.线性基可形成的所有不同异或值的和

I.CF724G Xor-matic Number of the Graph

注意到本质上就是求线性基中数的和,考虑一个经典的拆位计算,考虑对于每一位,这一位为 \(0\) 的有 \(x\) 个,那么为 \(1\) 的就有 \(tot-x\) 个,那么这一位为 \(0\) 就必须选偶数个 \(1\),那么就是 \(C(tot-x,0)+C(tot-x,2)+...\),根据二项式定理可知值为 \(2^{tot-x-1}\),证明就是 \(\left(1-1\right)^{tot-x}\),然后化出来,又知道这些总和是 \(2^{tot-x}\),所以得出。

所以,我们得出,如果这一位 \(tot-x\) 不为 \(0\),那么两边都是出现 \(2^{tot-1}\),否则就是 \(0\) 出现 \(2^{tot}\) 次。

至于点对,其实就是 \(dis_u \operatorname{xor} dis_v\),反正都拆位算贡献了,这里也直接看即可,具体见代码。

code

#include<bits/stdc++.h>
using namespace std;
#define int long long
namespace IO
{
	template<typename T>
	void read(T &_x){_x=0;int _f=1;char ch=getchar();while(!isdigit(ch)) _f=(ch=='-'?-1:_f),ch=getchar();while(isdigit(ch)) _x=_x*10+(ch^48),ch=getchar();_x*=_f;}
	template<typename T,typename... Args>
	void read(T &_x,Args&...others){Read(_x);Read(others...);}
	const int BUF=20000000;char buf[BUF],top,stk[32];int plen;
	#define pc(x) buf[plen++]=x
	#define flush(); fwrite(buf,1,plen,stdout),plen=0;
	template<typename T>inline void print(T x){if(!x){pc(48);return;}if(x<0) x=-x,pc('-');for(;x;x/=10) stk[++top]=48+x%10;while(top) pc(stk[top--]);}
}
using namespace IO;
const int N = 1e5+10,V = 62,mod = 1e9+7;
int n,m,x,y,z,head[N],cnt,dis[N],v[N],d[V+10],pw[N],tot,S,sum,sum1,ans;
vector<int>W;
struct w
{
	int to,nxt,z;
}b[N<<3];
inline int ksm(int x,int p)
{
	int ans = 1;
	while(p)
	{
		if((p&1)) ans = ans*x%mod;
		p >>= 1,x = x*x%mod;
	}
	return ans;
}
inline void add(int x,int y,int z)
{
	b[++cnt].nxt = head[x];
	b[cnt].to = y; b[cnt].z = z;
	head[x] = cnt;  
}
inline void xxj(int x)
{
	for(int i = V;i >= 0;i--)
		if(((1ll<<i)&x))
		{
			if(!d[i]){ d[i] = x; return; }
			else x ^= d[i];
		} 
}
void dfs(int x,int y)
{
	v[x] = 1; dis[x] = y; cnt++; W.push_back(x);
	for(int i = head[x];i;i = b[i].nxt)
		if(!v[b[i].to]) dfs(b[i].to,y^b[i].z);
		else xxj(dis[x]^dis[b[i].to]^b[i].z);
}
signed main()
{
	//freopen(".in","r",stdin);
	//freopen(".out","w",stdout);
	read(n),read(m);
	for(int i = 1;i <= m;i++) read(x),read(y),read(z),add(x,y,z),add(y,x,z);
	for(int A = 1;A <= n;A++) 
		if(!v[A])//图不一定联通,单独求解 
		{
			cnt = tot = S = 0; W.clear();
			for(int j = 0;j <= V;j++) d[j] = 0;
			dfs(A,0); pw[0] = 1;
			for(int i = 0;i <= V;i++)
			{
				for(int j = i-1;j >= 0;j--)//构建最小线性基 
					if((d[i]^d[j]) < d[i]) d[i] ^= d[j];
				if(d[i]) S |= d[i],tot++;
			}
			for(int i = 1;i <= V;i++) pw[i] = pw[i-1]*2%mod;
			for(int i = 0;i <= V;i++)//枚举每一位
				if(((1ll<<i)&S))//能够出现,每个出现2^{tot-1} 
				{
					ans = (ans+pw[i]*pw[tot-1]%mod*cnt%mod*(cnt-1)%mod*ksm(2,mod-2)%mod)%mod;
					//无论如何都有,都有对照的,直接算 
				}
				else//不出现就是0出现2^tot次 
				{//需要搞出一,求出0的就好了 
					x = 0;
					for(int z = 0;z < W.size();z++) x += (((1ll<<i)&dis[W[z]])==0);
					ans = (ans+pw[i]*pw[tot]%mod*x%mod*(cnt-x)%mod)%mod;
				}
		}
	print(ans); flush();
	return 0;
}
/*
求解(u,v,s)中s的和
众所周知,固定(u,v)后,就是找一条(u,v)路径,然后和若干环异或
环放入线性基,现在我们不是求s的总类数,而是求和
建出最小线性基,然后我们考虑每一位来计算?
对于当前位x,求出它为0和为1的总个数
为0,那么出现了偶数次,预处理每个位置有多少个点在这里为0/1,分别时x,y
那么0就是出现了2^x*2^(y-1)(即C(y,0)+C(y,2)+C(y,4)...,根据二项式展开可以得到是2^(y-1))
嗯?那不就是2^{tot-1},那1也知道了,也是2^{tot-1}
这里解决了,接下来是看左边(u,v)这种路径了
还是需要求出每一位出现几次
跑log遍,然后考虑启发式合并似的计算贡献
即对于每个点认为它是lca,然后跑
复杂度nlog^2+nV
汤包了,直接dis_u^dis_v就好了呀,反正是异或路径 
复杂度nlog
*/ 

II.P11713 [清华集训 2014] 玛里苟斯

奇怪名字配奇怪题

有趣的题,先说一个结论,记 \(S\) 为线性基元素或起来的和,只要 \(k \ge 1\),考虑 \(S\) 最后一个为 \(1\) 的位置,没有的话,答案恒为 \(0\),那么那个 \(1\) 至少出现 \(2^{n-1}\) 次,那么答案要么是整数,要么就是带个 \(0.5\)

注意到 \(k=1\) 就是求线性基组成的和,答案就是 \(\frac{S}{2}\),原理就是每一位出现了就是 \(2^{tot-1}\) 次,然后根据结论每一种数有 \(2^{n-tot}\) 个,所以每一位出现 \(2^{n-1}\) 次,除以总数就是了。

对于 \(k=2\),其实本质上就是拆开看,枚举两位 \(i < j\),然后构建等价线性基,使得要么没有 \(i,j\) 这两位,要么都有 \(i,j\) 这两位,注意到只有 \(d_i\) 达不到,那么不考虑这个,那么就是 \(2^{tot-2}\),对于 \(i=j\) 还是 \(2^{tot-1}\),所以其实答案就是 \(\frac{2^{i+j}\times 2^{tot-2}}{2^{tot}}\),其实就是 \(2^{i+j-2}\)

对于 \(k>2\) 有类比上面的做法,当然由于答案 \(<2^{63}\),考虑直接枚举线性基的子集即可,因为线性基大小一定 \(\le 21\)

code

#include<bits/stdc++.h>
using namespace std;
#define int __int128
#define getchar() (p1 == p2 && (p2 = (p1 = buf1) + fread(buf1, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
char buf1[1 << 23], *p1 = buf1, *p2 = buf1, ubuf[1 << 23], *u = ubuf;
namespace IO
{
	template<typename T>
	void read(T &_x){_x=0;int _f=1;char ch=getchar();while(!isdigit(ch)) _f=(ch=='-'?-1:_f),ch=getchar();while(isdigit(ch)) _x=_x*10+(ch^48),ch=getchar();_x*=_f;}
	template<typename T,typename... Args>
	void read(T &_x,Args&...others){Read(_x);Read(others...);}
	const int BUF=20000000;char buf[BUF],to,stk[32];int plen;
	#define pc(x) buf[plen++]=x
	#define flush(); fwrite(buf,1,plen,stdout),plen=0;
	template<typename T>inline void print(T x){if(!x){pc(48);return;}if(x<0) x=-x,pc('-');for(;x;x/=10) stk[++to]=48+x%10;while(to) pc(stk[to--]);}
}
using namespace IO;
const int N = 1e5+10,V = 63;
int n,k,d[V+10],a[N],ans,sum,S,x,pw[V+10],o,p,T[V+10],cnt; 
inline void xxj(int x)
{
	for(int i = V;i >= 0;i--)
		if(((1ll<<i)&x))
		{
			if(!d[i]){ d[i] = x; return; }
			x ^= d[i];
		}
}
signed main()
{
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	read(n),read(k);
	for(int i = 1;i <= n;i++) read(a[i]),xxj(a[i]);
	if(k == 1)
	{
		for(int i = 0;i <= V;i++) S |= d[i];
		if(S%2 == 0) print(S/2),pc('\n');
		else print(S/2),pc('.'),pc('5'),pc('\n');
		flush(); 
		return 0;
	}
	if(k == 2)
	{ 
		pw[0] = 1;
		for(int i = 1;i <= V;i++) pw[i] = pw[i-1]*2;
		for(int i = 0;i <= V;i++) S |= d[i];
		for(int i = 0;i <= V+2;i++)
			if(((1ll<<i)&S)) d[i] = 1;
			else d[i] = 0;
		for(int i = 0;i <= V;i++)
			for(int j = 0;j <= V;j++)
				if(d[i] && d[j])
				{
					x = i+j-2+(i==j);
					if(x == -1) ans++;//最多有这个 
					else ans += pw[x]*2;
				} 
		if(ans%2 == 0) print(ans/2),pc('\n');
		else print(ans/2),pc('.'),pc('5'),pc('\n');
		flush(); 
		return 0;
	}
	for(int i = 0;i <= V;i++)
		if(d[i]) T[cnt] = d[i],cnt++;
	o = (1<<cnt); p = 0;
	for(int i = 0;i < o;i++)
	{
		sum = 0;
		for(int j = 0;j < cnt;j++)
			if(((1<<j)&i))
				sum ^= T[j]; p = sum;
		for(int j = 1;j < k;j++) sum *= p; 
		ans += sum;//ans<=2^{63+cnt},肯定是可以的,cnt<=21 
	} o = 0;//注意到如果有至少出现cnt-1次,那么最多就是/2然后小数点为0.5 
	for(int i = 1;i <= cnt;i++)
	{
		if(ans%2==1) o++;
		ans /= 2;
	}
	if(o == 0) print(ans),pc('\n');
	else print(ans),pc('.'),pc('5'),pc('\n');
	flush(); 
	return 0;
}
/*
k=1是简单的,我们已经知道,是S*2^{tot-1},S为所有d_i或起来的值,tot是个数
然后在乘上2^{n-tot},最后/2^n其实就是S/2,特判奇偶性即可 
对于k=2,对于一个x^2=(2^a_{0}+2^a_{1}+...)^2
考虑一个有趣的,我们构建一个等价线性基,只有d_i只有i,其它的要么都有(i,j),要么都没有
然后就是2^{tot-2}*2^{i+j}*2^{n-tot}/2^n,不过记得特判一些东西(比如i=j)
其实就是求2^{i+j-2}的和(i,j必须有),注意到这里又不是求第k小之类的,最小线性基毫无必要,因为是等价的
对于k>2也可以同理做,就是枚举k个数,复杂度大概就是k*log^k,不过还有一个更巧妙的方法
注意到答案<2^63,开三次方根是2^21,可以暴力枚举 
*/

Part6.线性基区间修改

I.P11620 [Ynoi Easy Round 2025] TEST_34

本来叫P5607 [Ynoi2013] 无力回天 NOI2017,莫名其妙改名字了。

Part7.一些拓展/杂题

I.线性基合并 and 求 \(x\) 的排名

线性基合并不是一定就是 \(\log^3\) 了,比如此题:P12827 「DLESS-2」XOR and Even

\(n,q \le 5\times 10^5\),最多也就跑跑两只 \(\log\)

就拿此题作为例子,首先选出偶数个数有两种方法,第一种是类似反悔贪心的办法,\(a_i = a_i \operatorname{xor} a_{i+1}\),区间查询直接查 \(l\)\(r-1\) 就好了,这个就是要么重新选两个,要么不选这个换一个,不改变奇偶性。

还有一种,根据异或的性质,同时异或上 \(2^{V+1}\),这样零操作为了合法必须进行偶数次。

一操作给 \(x\) 也异或上,那么由于 \(x\) 必选,为了最大接下俩只能选偶数个。

接下来就是普通选择了。

对于一操作,直接线性基贪心求最大值即可。

对于零操作,这不就约等于求排名吗,可以直接二分,复杂度 \(\log^2\),不过这里介绍另一种更快的方式。

我们考虑按数位 dp 的方式,从高往低看,如果这一位 \(x\)\(1\),那么计算一下这一位为 \(0\) 的答案,定义 \(cnt_i\) 表示 \(i\) 后面的个数,那么贡献就是 \(2^{cnt_i}\),然后继续往后扫,计算这一位为 \(1\) 的答案。

实现就是记一个 \(S\),表示仅由线性基里面的数构成,同时在满足前面与 \(x\) 均一样的情况下,自己为多少。

这样写甚至不需要构建最小线性基,具体的可以看代码,等会会标注。

因为最小线性基其实就是每个 \(d_i\) 异或上前面的 \(d_j\)

在任意选择最小线性基的数0后,其实还是原来的线性基每个 \(d_i\) 被选择或者没被选择(异或的性质,异或两次抵消)。

而我在处理完 \(\left(i,V\right)\) 这个区间后,每个 \(d_j\) 都被处理了,根据性质第 \(i\) 为不行异或 \(< i\) 那些位是没用的,所以只考虑 \(\left(i,V\right)\) 这个区间,竟然我都看着选了,那么肯定没问题。

完事乘上 \(2^{n-tot}\) 就是答案了,单次复杂度 \(O\left(V\right)\)

好的,现在唯一问题就是如何快速求出区间的线性基。

朴素 st 表,复杂度 \(n\log^3+q\log^2\),不嘻嘻。

区间询问,然后线性基有可并性,直接考虑猫树分治。

具体的就分治,然后解决 \(L_i \le mid \le R_i\) 的询问。

然后每一层前缀线性基复杂度 \(\left(r-l+1\right)\times V\),然后总复杂度 \(n\log^2\)

每次询问就由两个合并而来,复杂度 \(q\log^2\)

其实此题也可以单 \(\log\) 解决,具体就是我们额外记录一个时间戳,然后每次 copy 一下前面的线性基,然后对于这一位如果为 \(1\)

没有,就放进去。 否则,保留一个时间大的,然后去跑小的。

这样求得时候只需要保证 \(id_{x,i} \ge l\) 就说明存在。

为什么这样一定不劣,例如交换了 \(y,d_{x,i}\),容易发现换不换,值都会变成 \(y \operatorname{xor} d_{x,i}\),所以保留时间大的一定不劣。

code\(\log\) 的。

#include<bits/stdc++.h>
using namespace std;
#define int long long
namespace IO
{
	template<typename T>
	void read(T &_x){_x=0;int _f=1;char ch=getchar();while(!isdigit(ch)) _f=(ch=='-'?-1:_f),ch=getchar();while(isdigit(ch)) _x=_x*10+(ch^48),ch=getchar();_x*=_f;}
	template<typename T,typename... Args>
	void read(T &_x,Args&...others){Read(_x);Read(others...);}
	const int BUF=20000000;char buf[BUF],top,stk[32];int plen;
	#define pc(x) buf[plen++]=x
	#define flush(); fwrite(buf,1,plen,stdout),plen=0;
	template<typename T>inline void print(T x){if(!x){pc(48);return;}if(x<0) x=-x,pc('-');for(;x;x/=10) stk[++top]=48+x%10;while(top) pc(stk[top--]);}
}
using namespace IO;
const int N = 5e5+10,M = 30,mod = 1e9+7;
int t,n,q,a[N],op,l,r,x,ans,tot,o,S;
int T[N][M+5],d[N][M+5];
inline void xxj(int id,int x,int y)
{
	for(int i = M;i >= 0;i--)
		if(((1<<i)&x))
		{
			if(!d[id][i]){ d[id][i] = x,T[id][i] = y; return; }
			else 
			{
				if(y > T[id][i]) swap(y,T[id][i]),swap(x,d[id][i]);
				x ^= d[id][i];
			}
		}
}
inline int ksm(int x,int p)
{
	int ans = 1;
	while(p)
	{
		if((p&1)) ans = ans*x%mod;
		p >>= 1,x = x*x%mod;
	}
	return ans;
}
inline int solve(int id,int L,int x)//区间(l,id),询问<=x的个数 
{
	ans = tot = o = S = 0;
	for(int i = M;i >= 0;i--) tot += (T[id][i] >= L);
	for(int i = M;i >= 0;i--) 
	{
		o += (T[id][i] >= L);
		if(((1<<i)&x))
		{
			if(((1<<i)&S) && T[id][i] >= L) ans = (ans+(1<<(tot-o)))%mod;//可以凑出零
			else if(!((1<<i)&S))//可以凑出零
			{
				ans = (ans+(1<<(tot-o)))%mod;
				if(T[id][i] < L) return ans*ksm(2,id-L+1-tot)%mod;//不能为1,跑不动了
				S ^= d[id][i]; 
			}
		}
		else if(((1<<i)&S))
		{
			if(T[id][i] < L) return ans*ksm(2,id-L+1-tot)%mod;//消不掉了
			S ^= d[id][i]; 
		}
	}//能活到最后,那么算上漏掉的=x的,当然其实你写x+1就好了
	return (ans+1)%mod*ksm(2,id-L+1-tot)%mod;
}
signed main()
{
	//freopen(".in","r",stdin);
	//freopen(".out","w",stdout);
	read(t);
	while(t--)
	{
		read(n),read(q);
		for(int i = 1;i <= n;i++) read(a[i]);
		for(int i = 1;i < n;i++) 
		{
			a[i] = a[i]^a[i+1]; 
			for(int j = 0;j <= M;j++) T[i][j] = T[i-1][j],d[i][j] = d[i-1][j];
			xxj(i,a[i],i);
		}
		while(q--)
		{
			read(op),read(l),read(r),read(x); r--;
			if(op == 0) print(solve(r,l,x)),pc('\n');
			else
			{
				ans = x;
				for(int i = M;i >= 0;i--)
					if(T[r][i] >= l) 
						ans = max(ans,ans^d[r][i]);
				print(ans),pc('\n');
			}
		}
	}
	flush();
	return 0;
}

附一些有趣的性质:

对于一个长度为 \(n\) 的序列 \(a_1,a_2...,a_n\),其子序列异或和的和等于 \(2^{n-1}\times \left(a_1 \operatorname{xor} a_2 \operatorname{xor} ... \operatorname{xor} a_n\right)\)

证明?还不知道(

posted @ 2025-08-11 15:57  kkxacj  阅读(9)  评论(0)    收藏  举报