线性基求交详解

参考资料:线性基求交

关于线性空间

给定两个线性空间 \(V_1,V_2\) 的基底 \(B_1,B_2\)。你需要求出 \(V_1\cap V_2\) 的一组的基底。

我们可以很容易摸出 \(V_1\cap V_2\) 的一个子空间的基底,具体地,我们记 \(W=V_1\cap B_2\),由于线性空间的封闭性,\(\text{span}(W)\) 的元素都在 \(V_1\) 中。又由于它能被 \(B_2\) 表出,那么就有 \(\text{span}(W)\) 中的所有元素都在 \(V_1\cap V_2\) 中。

什么情况下,\(W\) 就是 \(V_1\cap V_2\) 的基底呢?我们有一个很有意思的断言:如果 \(B_1\cup (B_2/W)\) 是线性无关的,那么 \(\text{span(W)}=V_1\cap V_2\)

由于 \(\text{span(W)}\)\(V_1\cap V_2\) 的一个子空间,我们只需要证明,没有元素 \(v\in V_1\cap V_2\) 不能被 \(W\) 表出就可以了。

假设有这么个元素 \(v\),它不能被 \(W\) 线性表出,但它却在 \(V_1\cap V_2\) 中,那么 \(B_2\) 想要表出 \(v\) 就必须涉及到 \(B_2/W\) 中的元素,设涉及到的这些元素异或和为 \(t\),那么 \(v\oplus t\) 能被 \(B_1\) 表出,再连同 \(B_2/W\) 中异或和为 \(t\) 的元素,\(v\) 就能被 \(B_1\cup(B_2/W)\) 线性表出。然而 \(v\) 由于在 \(V_1\) 中,只用 \(B_1\) 中的元素就可以表出 \(v\) 了,由于同一个元素不能用两种方式被线性无关组表出,与 \(B_1\cup (B_2/W)\) 线性无关的条件矛盾。

现在我们好奇,怎样才能使 \(B_1\cup (B_2/W)\) 线性无关呢?为此,我们需要对于 \(V_2\) 换一组基底 \(B_2'\)。我们考虑如何构建出一个合法的 \(B_2'\)

我们先初始化两个线性基 \(X=\emptyset,Y=\emptyset\),依次加入原来 \(B_2\) 中的元素 \(b\),在中途保持 \(X\subseteq V_1\) 以及 \(Y\cup B_1\) 线性无关的不变式。最后 \(X\cup Y\) 就作为 \(B'\)\(X\) 就作为 \(W\),算法返回 \(X\) 即可。

具体来说,加入 \(b\) 时,判断当前 \(Y\cup B_1\) 是否能表出 \(b\),如果能,考虑想要表出 \(b\) 涉及到的 \(B_1\) 中的元素的异或和 \(b'\),我们发现,在 \(B_2\) 中如果将 \(b\) 替换成 \(b'\),形成的新的基与原来的基等效,因为我们可以取出表出 \(b\) 时在 \(Y\) 中涉及到的元素再异或上 \(b'\),就可以表出原来的 \(b\) 了。

现在由于 \(b'\) 使用 \(B_1\) 中的元素表出的,于是它就可以属于 \(W\),于是我们将 \(b'\) 丢进 \(X\) 中。

如果当前 \(b\) 不能被表出,丢进 \(Y\) 中,记住在这个过程中我们需要一直保持 \(Y\cup B_1\) 线性无关。

朴素实现上述算法是 \(O(d^3)\),因为我们每次都要 \(O(d^2)\) 合并 \(Y\)\(B_1\),在合并出的线性基中标记出哪些元素来自 \(B_1\),然后再查询 \(x\) 的表出在 \(B_1\) 中的部分,我们需要一个更好写高效的实现。

研究了一下参考资料中的 \(O(d^2)\) 实现,它动态维护了 \(Y\cup B_1\) 的三角线性基。那怎么维护出 \(x\) 的表出在 \(B_1\) 中的部分呢?对于 \(Y\cup B_1\) 线性基的每一位我们注意到它是由 \(B_1\)\(Y\) 中的若干个值异或而成的,我们再维护一个辅助数组表示“当前这个元素在 \(B_1\) 的组成部分的异或和”。如果一个 \(b\) 需要插入 \(Y\) 的话更新 \(Y\cup B_1\) 的线性基和对应的辅助数组即可。

比如例题:枪打出头鸟

我们求出前缀线性基交后再反过来把下标当权值做一遍最大权线性基,查询的时候输出表出 \(x\) 的元素的权值最小值,复杂度 \(O(n\log^2 V+q\log V)\)

#include <cstdio>
#include <cstring>
using namespace std;
typedef unsigned long long ull;
template<typename T> T read(){/* read */}
const int N=100003;
int n,q;
ull a[N][64];
void ins(ull *a,ull x){
	for(int i=0;i<64;++i)
		if(x>>i&1){
			if(a[i]) x^=a[i];
			else{a[i]=x;return;}
		}
}
bool qry(ull *a,ull x){
	for(int i=0;i<64;++i)
		if(x>>i&1) x^=a[i];
	if(x) return 0;
	return 1;
}
void inter(ull *a,ull *b){
	ull ct[64],ca[64];
	memcpy(ct,a,512);
	memcpy(ca,a,512);
	memset(a,0,512);
	for(int i=0;i<64;++i)
		if(b[i]){
			ull c=0,d=b[i];
			for(int j=i;j<64;++j)
				if(d>>j&1){
					if(ct[j]) d^=ct[j],c^=ca[j];
					else{ct[j]=d;ca[j]=c;break;}
				}
			if(!d) ins(a,c);
		}
}
ull p[64];int pt[64];
int main(){
	n=read<int>();q=read<int>();
	for(int i=1;i<=n;++i){
		int k=read<int>();
		while(k--) ins(a[i],read<ull>());
	}
	for(int i=2;i<=n;++i) inter(a[i],a[i-1]);
	for(int x=n;x;--x)
		for(int i=0;i<64;++i){
			if(a[x][i]){
				ull d=a[x][i];
				for(int j=i;j<64;++j)
					if(d>>j&1){
						if(p[j]) d^=p[j];
						else{p[j]=d;pt[j]=x;break;}
					}
			}
		}
	while(q--){
		ull x=read<ull>();
		int res=n;
		for(int i=0;i<64;++i)
			if(x>>i&1){
				x^=p[i];
				if(res>pt[i]) res=pt[i];
			}
		if(x) puts("1");
		else printf("%d\n",res+1);
	}
	return 0;
}

此外这道题由于前缀交只有 \(O(\log V)\) 段,直接二分复杂度为 \(O(n\log^2 V+q\log V\log\log V)\)

由于建出最大权线性基为复杂度瓶颈之一,实测这个做法跑得没实现良好的二分快。

posted @ 2023-06-29 22:03  yyyyxh  阅读(302)  评论(0编辑  收藏  举报