线性基学习笔记

还不会用 Markdown 的时候写的文章……重修&复习了一遍。主要修改的还是习题部分。

0 - 意义

线性基是向量空间的一组基,通常可以解决有关异或的一些题目。

简单讲就是由一个集合构造出来的另一个集合,这个集合大小最小且能异或出原来集合中的任何一个数,并且不能表示出除了原集合的其他数。

性质

  1. 线性基能相互异或得到原集合的所有相互异或得到的值。

  2. 线性基是满足性质1的最小的集合

  3. 线性基没有异或和为 \(0\) 的子集。

  4. 假设线性基中有 \(cnt\) 个数,线性基能异或出的数的集合大小为 \(2^{cnt}-1\)(去掉一个都不取),也就是说,线性基中不同的组合异或出的数都不一样。

1 - 构造

设当前插入的数是 \(x\) ,线性基数组为 \(a\) ,从高位向低位走,考虑所有为 \(1\) 的当前位 \(i\)

  • 如果线性基的第 \(i\) 位为 \(0\) ,那么直接在这一位插入 \(x\) ,退出;
  • 否则,令 \(x=x\oplus a[i]\)
  • 重复上述操作直到 \(x=0\)

如果退出循环的时候 \(x=0\) ,那么说明原有的线性基已经可以表示 \(x\) ,无需再插入;反之,则说明为了表示 \(x\) 插入了一个新的元素。

void Insert( ll x )
{
    for ( int i=30; ~i; i-- )
        if ( x&(1ll<<i) )
            if ( !a[i] ) { a[i]=x; return; }
            else x^=a[i];
    flag=1;
}

检验存在

检查一个数是否能被某个线性基表示出来。

和插入类似,只要中途或者最后变成 \(0\) 了,就说明能够表示。

bool check( ll x )
{
	for ( int i=30; ~i; i-- )
		if ( x&(1ll<<i) )
			if ( !a[i] ) return 0;
			else x^=a[i];
	return 1;
}

2 - 查询异或最值

最小值

查询最小值相对比较简单。

考虑在插入的过程中,每一次异或 \(a[i]\) 的操作,\(x\) 的二进制最高位都在降低,所以不可能插入两个二进制最高位相同的数。

此时线性基中的最小值异或上其他的数,必然会增大,所以直接输出线性基中的最小值即可。

注意要特判能否异或出 \(0\) . 因为线性基有性质:没有异或和为 \(0\) 的子集。特判也很简单,只要一个数在插入过程中没有被插入到某个 \(a[i]\) ,那么就被异或成了 \(0\) ,说明 \(0\) 是可以取到的。

ll Query_min( ll res=0 )
{
	if ( fl ) return 0;    //flag 是 Insert 中传出来的变量,表示是否能表示 0 
	for ( int i=0; i<=30; i++ )
		if ( a[i] ) return a[i];
}

最大值

从高到低遍历线性基,设当前考虑到第 \(i\) 位。如果当前答案 \(res\) 的第 \(i\) 位为 \(0\) ,就将 \(res=res\oplus a[i]\) ;否则不操作。或者说,更简便的写法是直接和异或后的值取 \(\max\) (其实是一个道理,高位从 \(0\to 1\) 一定是变大的嘛)

这是显然的,求最小值部分已经说过,线性基中数的最高位显然单调递减,那么每次这样的操作之后答案都不会变劣。

这是对序列中元素求相互异或的最大值。如果是另一个给定的数 \(x\) ,那么用类似的方式可以解决,只需要把 \(res\) 的初始值改变即可。

ll Query_max( ll res=0 )
{
	for ( int i=30; ~i; i-- )
		res=max( res,res^a[i] );
	return res;
}

模板

写到这里就可以写 模板题 了。代码:

//Author:RingweEH
const int N=55,MX=50;
int n;
ll a[N];

void Insert( ll x )
{
	for ( int i=MX; ~i; i-- )
		if ( x>>i&1 )
		{
			if ( !a[i] ) { a[i]=x; return; }
			x^=a[i];
		}
}

ll Query_mx( ll res=0 )
{
	for ( int i=MX; ~i; i-- )
		res=max( res,res^a[i] );
	return res;
}

int main()
{
	n=read();
	for ( int i=1; i<=n; i++ )
	{
		ll x=read(); Insert( x );
	}

	printf( "%lld\n",Query_mx() );

	return 0;
}

3 - 求第 k 小

首先,线性基的构造方式跟之前不太一样了,我们知道,线性基是以每个二进制为最高位存一个数的,容易想到把 k 二进制分解,这样的话,只需要改点限制:规定 \(a[i]\) 的值最高位是第 \(i\) 位,且在此基础上 \(a[i]\) 最小。

考虑之前的 \(a[i]\) ,它除了在第 \(i\) 位有个 \(1\) 外,在更低的位还有若干个 \(1\) 。那是否可以用线性基中的某些数,尽量消去低位的那些 \(1\) ? 这个很好做,往线性基插入一个新数时,用这个 \(a[i]\) 更新 \(a\) 数组的其它所有值就行了。

详细做法:

  • 对于低位

现在插入的一个数放到了 \(a[i]\) ,它在一个更低的二进制位(设其为第 \(j\) 位)上为 \(1\) ,且 \(a[j]\) 已被赋过值,那就把 \(a[i]\) 更新为 \(a[i]\oplus a[j]\) 。为了方便,从大到小枚举 \(j\) 即可。

  • 对于高位

只考虑低位显然不对,因为有可能 \(a[i]\) 的第 \(j\) 个二进制位为 \(1\) ,而 \(a[j]\) 此时可能没有值,但它以后被赋了值,这种情况下也应该用 \(a[j]\) 更新 \(a[i]\) 。我们只能用赋值晚的更新赋值早的,所以对于插入的一个数 \(a[i]\) ,不仅要用更低位的 \(a[j]\) 更新它,还要用它更新更高位的 \(a[j]\) 。依然从大到小枚举 \(j\)

代码:

for ( int i=N; ~i; i-- )
	if ( x>>i&1 )
	{
		if ( a[i] ) x^=a[i];
		else
		{
			a[i]=x;
			for ( int j=i-1; ~j; j-- )
				if ( a[j] && (a[i]>>j&1) ) a[i]^=a[j];
			for ( int j=N; j>i; j-- )
				if ( a[j]>>i&1 ) a[j]^=a[i];
			return;
		}
	}

其实这才是线性基的通用构造方式(比如对于模板题,多出来的要求对答案没有影响,因此该构造方案可以兼容使用)。

换个角度看这个构造方式,其实就是标准的高斯消元,所谓的把不在对角线上的 \(1\) 能消掉就消掉,其实也就是让每行的数最小。

如上改变线性基的构造方式后,把 \(k\) 二进制分解,若第 \(i\) 位为 \(1\) 就把 \(ans\) 异或上 \(p_i\) 即可。

注意也要特判能否异或出 \(0\) ,并且在插入完之后要压缩线性基数组,只留下 \(a[i]\neq 0\) 的部分。(这个显然,为 \(0\) 相当于是无效位,对 \(k\) 没有任何贡献,当然也不能算进位数里面)

模板题

//Author:RingweEH
const int N=63,M=65;
int n,cnt;
ll a[M],b[M];
bool fl=0;

void Insert( ll x )
{
	for ( int i=N; ~i; i-- )
		if ( x>>i&1 )
		{
			if ( a[i] ) x^=a[i];
			else
			{
				a[i]=x;
				for ( int j=i-1; ~j; j-- )
					if ( a[j] && (a[i]>>j&1) ) a[i]^=a[j];
				for ( int j=N; j>i; j-- )
					if ( a[j]>>i&1 ) a[j]^=a[i];
				return;
			}
		}
	if ( x==0 ) fl=1;
}

int main()
{
	int T=read();
	for ( int cas=1; cas<=T; cas++ )
	{
		memset( a,0,sizeof(a) ); fl=0; cnt=0;

		n=read();
		for ( int i=1; i<=n; i++ )
		{
			ll x=read(); Insert( x );
		}

		for ( int i=0; i<=N; i++ )
			if ( a[i] ) b[cnt++]=a[i];
		int q=read(); printf( "Case #%d:\n",cas );
		while ( q-- )
		{
			ll k=read(),ans=0; k-=fl;
			if ( k>=(1ll<<cnt) ) { printf( "-1\n" ); continue; }
			for ( int i=0; i<cnt; i++ )
				if ( k>>i&1 ) ans^=b[i];
			printf( "%lld\n",ans );
		}
	}

	return 0;
}

4 - 习题

也许大概或许可能是按难度排序的吧(

彩灯

有一个长度为 \(N\) 的01串,初始全 \(0\) 。给出 \(M\) 个操作,每个操作能使特定的几位取反,问能产生几种不同的 \(01\) 串。

Solution

显然是裸题。将每个操作看成一个数,构造线性基,题目也就是问能表示出多少个数。

注意到有性质:

假设线性基中有 \(cnt\) 个数,线性基能异或出的数的集合大小为 \(2^{cnt}-1\)(去掉一个都不取)

那就做完了。不过这题全 \(0\) 也算一种方案,不需要 \(-1\) .

我怎么又没看见取模(悲)

//Author:RingweEH
const int N=55,MX=50;
const ll Mod=2008;
int n,m;
ll a[N],cnt=0;
char s[N];

void Insert( ll x )
{
	for ( int i=MX; ~i; i-- )
		if ( x>>i&1 )
		{
			if ( !a[i] ) { a[i]=x; cnt++; return; }
			x^=a[i];
		}
}

int main()
{
	n=read(); m=read();
	for ( int i=1; i<=m; i++ )
	{
		scanf( "%s",s ); ll x=0;
		for ( int j=0; j<n; j++ )
		{
			x<<=1;
			if ( s[j]=='O' ) x|=1;
		}
		Insert( x );
	}

	printf( "%lld\n",(1ll<<cnt)%Mod );

	return 0;
}

最大XOR和路径

给定一个边权为非负整数的无向连通图,求 \(1\)\(N\) 的路径,使得边权异或和最大。点边可以重复经过。

\(N\leq 5e4,M\leq 1e5,D_i\leq 1e18\) .

Solution

做法很简单:找出所有环,扔到线性基里,然后随便找一条路径作为初始值,求异或最大值即可。

考虑为什么是对的。

首先找环肯定是没有疑问的,因为重复走两遍相当于没有走,唯一能产生变数的就是环了。

然后考虑为什么随便一条路径就行。假设存在至少两条,设为 \(path_1,path_2\) ,那么它们本身就构成了一个大环,异或一下就能得到对方。因此只要任意一条路径+所有环就好了。

//Author:RingweEH
const int N=5e4+10;
struct Edge
{
	int to,nxt; ll val;
}e[N<<2];
int head[N],tot=0,n,m;
ll path[N],a[64];
bool vis[N];

void Add( int u,int v,ll w )
{
	e[++tot].to=v; e[tot].nxt=head[u]; head[u]=tot; e[tot].val=w;
}

void Insert( ll x )
{
	for ( int i=62; ~i; i-- )
		if ( x>>i&1 )
		{
			if ( !a[i] ) { a[i]=x; return; }
			x^=a[i];
		}
}

void Dfs( int u,int fa,ll now )
{
	vis[u]=1; path[u]=now;
	for ( int i=head[u]; i; i=e[i].nxt )
	{
		int v=e[i].to;
		if ( v==fa ) continue;
		if ( !vis[v] ) Dfs( v,u,now^e[i].val );
		else Insert( path[v]^now^e[i].val );
	}
}

int main()
{
	n=read(); m=read();
	for ( int i=1; i<=m; i++ )
	{
		int u=read(),v=read(); ll w=read();
		Add( u,v,w ); Add( v,u,w );
	}

	Dfs( 1,0,0 ); ll ans=path[n];
	for ( int i=62; ~i; i-- )
		ans=max( ans,ans^a[i] );

	printf( "%lld\n",ans );

	return 0;
}

albus就是要第一个出场

给定一个长度为 \(n\) 的序列 \(A\) ,将所有 \(A\) 的子集的异或和从小到大排成序列 \(B\) ,求一个数在 \(B\) 中第一次出现的下标。

Solution

还是用这个性质:

假设线性基中有 \(cnt\) 个数,线性基能异或出的数的集合大小为 \(2^{cnt}-1\)(去掉一个都不取)

然后注意到除了这 \(cnt\) 个数,还有 \(n-cnt\) 个,而它们所能组成的异或和一定能被线性基中的数表示出来,也就相当于我们有 \(2^{n-cnt}\) 个异或和为 \(0\) 的子集。那么就是,所有能异或出的数的集合中,每个数在 \(B\) 序列里都出现了 \(2^{n-cnt}\) 次。我们只需要查询数 \(x\) 在不重复的序列中的排名 \(rk\) ,然后 \(ans=rk\times2^{n-cnt}+1\) 即可。

现在考虑如何求排名。从高到低枚举每一位 \(a[i]\neq 0\) 的位置,如果 \(x\) 的当前位为 \(1\) ,那么就是比 “当前位为 \(0\)\(2^{n-cnt}\) 个异或和”都要大,就加上这一部分的贡献;否则不加。(注:这里的 \(cnt\) 指的是到当前位位置,\(a[i]\neq 0\) 的个数。这应该很好理解,因为如果当前这个高位为 \(1\) ,那么无论后面怎么取,都比高位为 \(0\) 的要大)

被位运算优先级坑了一发 /kk

//Author:RingweEH
const int N=1e5+10,M=30,Mod=10086;
int n,a[M+5];

ll power( ll a,ll b )
{
	ll res=1;
	for ( ; b; b>>=1,a=a*a%Mod )
		if ( b&1 ) res=res*a%Mod;
	return res;
}

void Insert( int x )
{
	for ( int i=M; ~i; i-- )
		if ( x>>i&1 )
		{
			if ( !a[i] ) { a[i]=x; return; }
			x^=a[i];
		}
}

ll Query_rk( int x )
{
	int cnt=0; ll ans=0;
	for ( int i=M; ~i; i-- )
		if ( a[i] )
		{
			cnt++;
			if ( x>>i&1 ) ans=(ans+power(2ll,n-cnt))%Mod;
		}
	return ans;
}

int main()
{
	n=read();
	for ( int i=1; i<=n; i++ )
		Insert( read() );

	ll Q=read(); ll ans=Query_rk(Q);

	printf( "%lld\n",(ans+1)%Mod );

	return 0;
}

新Nim游戏

在 Nim 游戏的第一轮,允许两个玩家特殊操作:可以拿走若干个整堆,可以一堆都不拿,但是不能全部拿走。其余同 Nim。

问先手是否必胜,如果是那么给出第一轮拿的最小数量。

Solution

其实先手肯定必胜,第一次拿的时候只剩下一堆就好了。

那么问题在于如何让第一轮拿走的数量最小。

显然可以发现,先手第一轮拿完之后不能剩下异或为 \(0\) 的子集。而这显然是个线性基(性质 \(3\) ),也就是要构造和最大的一组线性基。

那么将每一堆排序,然后依次尝试加入线性基,并求出所有成功加入的数之和即可。

//Author:RingweEH
const int N=110,M=30;
int n,a[35],b[N];

bool Insert( int x )
{
	for ( int i=M; ~i; i-- )
		if ( x>>i&1 )
		{
			if ( !a[i] ) { a[i]=x; return 1; }
			x^=a[i];
		}
	return 0;
}

int main()
{
	n=read(); ll sum=0;
	for ( int i=1; i<=n; i++ )
		b[i]=read(),sum+=b[i];

	sort( b+1,b+1+n ); ll ans=0;
	for ( int i=n; i>=1; i-- )
		if ( Insert(b[i]) ) ans+=b[i];

	printf( "%lld\n",sum-ans );

	return 0;
}

元素

给定一个长度为 \(n\) 的序列 \(A[i][0/1]\) ,求一个子集,满足 \(A[i][0]\) 的异或和不为 \(0\) 的情况下,\(A[i][1]\) 和最大。

Solution

神笔题,直接按照 \(A[i][1]\) 排序,然后依次尝试插入即可。和上题差不多。

//Author:RingweEH
const int N=1010,M=60;
struct Node
{
	ll num; ll val;
	bool operator < ( const Node &tmp ) const { return val<tmp.val; }
}b[N];
int n;
ll a[M];

bool Insert( ll x )
{
	for ( int i=M; ~i; i-- )
		if ( x>>i&1 )
		{
			if ( !a[i] ) { a[i]=x; return 1; }
			x^=a[i];
		}
	return 0;
}

int main()
{
	n=read();
	for ( int i=1; i<=n; i++ )
		b[i].num=read(),b[i].val=read();

	sort( b+1,b+1+n ); ll ans=0;
	for ( int i=n; i>=1; i-- )
		if ( Insert(b[i].num) ) ans+=b[i].val;

	printf( "%lld\n",ans );

	return 0;
}

装备购买

\(n\) 个装备,每个装备 \(m\) 个属性,每个装备还有个价格。如果手里有的装备的每一项属性为它们分配系数(实数)后可以相加得到某件装备,则不必要买这件装备。求最多装备下的最小花费。

Solution

“能被已有的装备组合出来”这一点很像线性基,但这里不再是异或线性基了,而是实数。

回归本真的线性基,可喜可贺

其实方式和异或线性基差不多,不过是把原来的 \(x=x\oplus a[i]\) 换成了消元(具体参考高斯消元的方式),之前 \(a[i]\) 记录的是每一位上留下的那个数,现在就记录一个位置,使得矩阵中第 \(i\) 列(也就是第 \(i\) 个变量)只有 \(a[i]\) 这一行不为 \(0\) (对应高斯消元中把每一行的方程消成只剩下一个变量, \(a[i]\) 记录的是第 \(i\) 个变量所在的方程)。每次找当前行不为 \(0\) 的列 \(j\) ,如果 \(a[j]\) 还没有值就赋值并退出,否则就用 \(c[i][j]/c[a[j]][j]\) 乘上 \(c[a[j]][k]\) 去减 \(a[i][k]\)不会高斯消元的你试试看怎么消元解多元方程就好了吧

然后要求最小花费,那就排个序即可。

精度yyds! 要开 long double 或者把 \(eps\) 调成 \(1e-5\) .

//Author:RingweEH
const int N=510;
const db eps=1e-5;
struct Vector
{
	db a[N]; int val;
	db &operator [] ( const int &x ) { return a[x]; }
	bool operator < ( const Vector&tmp ) const { return val<tmp.val; }
}c[N];
int n,m,a[N];


int main()
{
	n=read(); m=read();
	for ( int i=1; i<=n; i++ )
		for ( int j=1; j<=m; j++ )
			scanf( "%lf\n",&c[i][j] );
	for ( int i=1; i<=n; i++ )
		c[i].val=read();

	sort( c+1,c+1+n ); int cnt=0; ll ans=0;
	for ( int i=1; i<=n; i++ )
		for ( int j=1; j<=m; j++ )
		{
			if ( fabs(c[i][j])<eps ) continue;
			if ( !a[j] ) { a[j]=i; cnt++; ans+=c[i].val; break; }
			db tmp=1.0*c[i][j]/c[a[j]][j];
			for ( int k=j; k<=m; k++ )
				c[i][k]-=tmp*c[a[j]][k];
		}

	printf( "%d %lld\n",cnt,ans );

	return 0;
}
posted @ 2021-01-04 10:16  MontesquieuE  阅读(327)  评论(0编辑  收藏  举报