线性代数相关内容汇总

线性代数

替换定理

对于向量空间 \(S\) 的一组基底 \(A=\{a_1,a_2,...,a_n\}\)\(S\) 的一个线性无关组 \(B=\{b_1,b_2,...,b_m\}\) ,有 \(A'\subset A\) 满足 \(A'\cup B\) 仍然为 \(S\) 的一组基底,而且 \(|A'|=n-m\)

\(m\) 归纳证明,略。

推论1

向量空间的基底全部大小相等且线性无关。

推论2

向量空间的任意线性无关组大小小于其基底,且可以通过加入新元素称为一组基底。反之线性相关。

行列式小应用

行列式表示体积

行列式的绝对值为多个高维向量围成的体积。

l行展开

选取一行,每一位的权值乘上其对应的代数余子式的值的和为其行列式的值。

laplace展开

选多行,

高斯消元

高斯消元模板题

理论支撑

发现对于矩阵,我们可以增广矩阵:

1.交换两行矩阵本质不变。

2.矩阵行(或列)相加减本质不变。

那么我们可以得到一种做法,我们考虑一个一个元进行操作。

具体来说,第 \(i\) 次选择元 \(x_i\) 并任意选取一个行 \(k\in[i,n]\),这个行满足其元 \(x_i\) 有非零系数(推荐选择最大的,可以保证最优精度)。然后将其他行(其他方程)的 \(x_i\) 元上的系数消去(通过行相加减的方式)。重复这个过程,可以发现每次选择的行,都满足以下条件:

1.当前行 \(x_i\) 之前的元的系数全为 \(0\)

2.本次消元完成后,除了当前行外,其他行的 \(x_i\) 元的系数都为 \(0\)

结论显然。

最后得到的矩阵一定是一个倒三角矩阵(至少有唯一解的矩阵是,以及忽略常数项),而且更是只有对角线才有非 \(0\) 系数。直接按照系数以及常数项即可得到答案。
这是有唯一解的情况。

而对于没有唯一解的,我们判断其为无解或无数解,这个是问题的难点。

什么情况无解?你如果发现对于没有任何元有非零系数的行,它的常数项有值,则无解。否则如果存在没有任何元有非零系数的行,则为无数解。

但是需要注意的是,对于一个元所有方程都没有这个元的系数,我们不应该给其分配一个方程作为最后的解,因为它根本不会去消任何的元,因此方程数减一,可能出现无法解出方程的结果。因此我们每次选择的行 \(k\in[s,n]\)\(s\) 是完成消元的元的个数。这个问题曾经困扰了我很久,直到今天才给出合理的解释,惭愧。

具体实现
#include<bits/stdc++.h>
#define ll long long
#define db double
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define Better_IO true
namespace IO{
	#if Better_IO==true
		char buf[(1<<20)+3],*p1(buf),*p2(buf);
		const int lim=1<<20;
		inline char gc(){
			if(p1==p2) p2=(p1=buf)+fread(buf,1,lim,stdin);
			return p1==p2?EOF:*p1++;
		}
		#define pc putchar
	#else
		#define gc getchar
		#define pc putchar
	#endif
	inline bool blank(const char &c){
		return c==' ' or c=='\n' or c=='\t' or c=='\r' or c==EOF;
	}
	inline void gs(char *s){
		char ch=gc();
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {*s++=ch;ch=gc();}
		*s=0;
	}
	inline void gs(std::string &s){
		s="";char ch=gc();s+='#';
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {s+=ch;ch=gc();}
	}
	inline void ps(char *s){
		while(*s!=0) pc(*s++);
	}
	inline void ps(const std::string &s){
		for(auto it:s) 
			if(it!='#') pc(it);
	}
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			db p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p*=0.1;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
using IO::gs;
using IO::ps;
const int N=50+3;
int n;
db a[N][N+1];
int main(){
	file(a);
	read(n);
	for(int i=1;i<=n;++i){
		for(int j=1;j<=n+1;++j){
			read(a[i][j]);
		}
	}
	int s=1;
	for(int i=1;i<=n;++i){
		int mx=s;
		for(int j=s+1;j<=n;++j){
			if(fabs(a[j][i])>fabs(a[mx][i]) ) mx=j;
		}
		if(!a[mx][i]) continue;
		if(mx!=s)
		for(int j=1;j<=n+1;++j) std::swap(a[s][j],a[mx][j]);
		for(int j=1;j<=n;++j){
			if(j==s) continue;
			db tmp=a[j][i]/a[s][i];
			for(int k=i;k<=n+1;++k){
				a[j][k]-=a[s][k]*tmp;
			}
		}
		++s;
	}
	if(s<=n){
		while(s<=n){
			if(a[s][n+1]!=0){
				printf("-1\n");
				return 0;
			}
			++s;
		}
		printf("0\n");
		return 0;
	}
	for(int i=1;i<=n;++i){
		printf("x%d=",i);
		if(int(a[i][n+1]/a[i][i]*100)==0) printf("0.00\n");
		//防止出现 -0.00 的鬼畜情况
		else printf("%.2lf\n",a[i][n+1]/a[i][i]);
	}
	return 0;
}

行列式求值

行列式求值模板题

理论支撑

这个考虑高斯消元求解。

我们先总结一下要用到的性质:

1.行列式行(或列)相加减行列式的值不变。

2.行列式行(或列)交换行列式的值变为原来的相反数。

3.行列式同行(或列)的所以元素乘上 \(k\) ,行列式增大 \(k\)

4.行列式转置后值不变。

5.三角行列式的值由对角线上的元素的乘积决定。

考虑一种做法,我们不断的行列加减使得最终行列式变为三角行列式,然后直接对角线相乘即可。然后这个加减的过程使用高斯消元即可。复杂度由 \(O(n!)\) 变为 \(O(n^3 \log n)\)。这个过程无需判断无解和无数解,舒服的。

但是考虑到模意义下,不能按照倍数直接消元,只能考虑辗转相减。总之就是一直这么弄,一直做直到把其中一个消空,非空的那个拿走继续去消别的行。

那个辗转的可以提前处理出来,然后后面按照系数消元,可以做到 \(O(n^3)\)

代码实现
#include<bits/stdc++.h>
#define ll long long
#define db double
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define Better_IO true
namespace IO{
	#if Better_IO==true
		char buf[(1<<20)+3],*p1(buf),*p2(buf);
		const int lim=1<<20;
		inline char gc(){
			if(p1==p2) p2=(p1=buf)+fread(buf,1,lim,stdin);
			return p1==p2?EOF:*p1++;
		}
		#define pc putchar
	#else
		#define gc getchar
		#define pc putchar
	#endif
	inline bool blank(const char &c){
		return c==' ' or c=='\n' or c=='\t' or c=='\r' or c==EOF;
	}
	inline void gs(char *s){
		char ch=gc();
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {*s++=ch;ch=gc();}
		*s=0;
	}
	inline void gs(std::string &s){
		s="";char ch=gc();s+='#';
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {s+=ch;ch=gc();}
	}
	inline void ps(char *s){
		while(*s!=0) pc(*s++);
	}
	inline void ps(const std::string &s){
		for(auto it:s) 
			if(it!='#') pc(it);
	}
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			db p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p*=0.1;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
using IO::gs;
using IO::ps;
const int N=600+3;
int n,mods;
inline int inc(int x,int y){
	return (x+=y)>=mods?x-mods:x;
}
inline int dec(int x,int y){
	return (x-=y)<0?x+mods:x;
}
inline int mul(int x,int y){
	return 1ll*x*y%mods;
}
int a[N][N];
int main(){
	file(a);
	read(n,mods);
	for(int i=1;i<=n;++i){
		for(int j=1;j<=n;++j){
			read(a[i][j]);
			a[i][j]%=mods;
		}
	}
	int det=1;bool s=0;
	for(int i=1;i<=n;++i){
		int mx=i;
		for(int j=i+1;j<=n;++j){
			if(a[j][i]>a[mx][i]) mx=j;
		}
		if(!a[mx][i]){
			det=0;
			break;
		}
		if(mx!=i){
			s^=1;
			for(int j=1;j<=n;++j){
				std::swap(a[i][j],a[mx][j]);
			}
		}
		for(int j=i+1;j<=n;++j){
			while(a[i][i]){
				int tmp=a[j][i]/a[i][i];
				for(int k=i;k<=n;++k){
					a[j][k]=dec(a[j][k],mul(a[i][k],tmp) );
				}
				s^=1;
				for(int k=i;k<=n;++k){
					std::swap(a[i][k],a[j][k]);
				}
			}
			s^=1;
			for(int k=i;k<=n;++k){
				std::swap(a[i][k],a[j][k]);
			}
		}
		det=mul(det,a[i][i]);
	}
	det=s?inc(-det,mods):det;
	printf("%d\n",det);
	return 0;
}

预处理系数的参考代码。(从矩阵树定理的题目弄出来的)

后面好像发现原来那个直接辗转相除的就是 \(O(n^3+n^2\log n)\) 的(均摊)?下面这个看个乐子吧。

int a[N][N];
inline void kill(int a,int b,int &aii,int &aij,int &aji,int &ajj,int &s){
	s=0;
	aii=ajj=1;
	aij=aji=0;
	while(b){
		aii=dec(aii,mul(a/b,aji) );
		aij=dec(aij,mul(a/b,ajj) );
		a%=b;
		std::swap(a,b);
		std::swap(aii,aji);
		std::swap(aij,ajj);
		s^=1;
	}
}
inline int Main(){
	int n=::n-1;
	for(int i=1;i<=n;++i){
		for(int j=1;j<=n;++j){
			a[i][j]=lap[i][j];
		}
	}
	int det=1;bool s=0;
	for(int i=1;i<=n;++i){
		int mx=i;
		for(int j=i+1;j<=n;++j){
			if(a[j][i]>a[mx][i]) mx=j;
		}
		if(!a[mx][i]){
			return 0;
		}
		if(mx!=i){
			s^=1;
			for(int j=i;j<=n;++j){
				std::swap(a[i][j],a[mx][j]);
			}
		}
		for(int j=i+1;j<=n;++j){
			int k1,k2,k3,k4,si;
			kill(a[i][i],a[j][i],k1,k2,k3,k4,si);
			s^=si;
			for(int k=i;k<=n;++k){
				int ta=inc(mul(a[i][k],k1),mul(a[j][k],k2) );
				int tb=inc(mul(a[i][k],k3),mul(a[j][k],k4) );
				a[i][k]=ta;a[j][k]=tb;
			}
		}
		det=mul(det,a[i][i]);
	}
	return s?dec(0,det):det;
}

矩阵求逆

【模板】矩阵求逆

一个奇妙的性质,我们对原矩阵做高斯消元使之变为单元矩阵,其中的每个操作同样对着一个单元矩阵也做一遍,就可以得到其逆矩阵。原因是反向做初等变换确实是可以这样的。无解情况是它不能被消为单位矩阵,就是它不是满秩矩阵。

#include<bits/stdc++.h>
#define ll long long
#define db double
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define gc getchar
#define pc putchar
namespace IO{
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			T p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p/=10;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
const int N=400+3;
const int mod=1e9+7;
inline int inc(int x,int y){
	return (x+=y)>=mod?x-mod:x;
}
inline int dec(int x,int y){
	return (x-=y)<0?x+mod:x;
}
inline int mul(int x,int y){
	return 1ll*x*y%mod;
}
inline int qpow(int a,int b){
	int res=1;
	while(b){
		if(b&1) res=mul(res,a);
		a=mul(a,a);
		b>>=1;
	}
	return res;
}
int n;
int a[N][N<<1];
int main(){
	file(a);
	read(n);
	for(int i=1;i<=n;++i){
		for(int j=1;j<=n;++j){
			read(a[i][j]);
		}
		a[i][i+n]=1;
	}
	for(int i=1;i<=n;++i){
		int mx=i;
		for(int j=i+1;j<=n;++j){
			if(abs(a[mx][i])<abs(a[j][i]) ){
				mx=j;
			}
		}
		if(!a[mx][i]){
			printf("No Solution\n");
			return 0;
		}
		if(i!=mx) std::swap(a[mx],a[i]);
		int inv=qpow(a[i][i],mod-2);
		for(int j=1;j<=n;++j){
			if(i==j) continue;
			int tmp=mul(a[j][i],inv);
			for(int k=1;k<=(n<<1);++k){
				a[j][k]=dec(a[j][k],mul(tmp,a[i][k]) );
			}
		}
		for(int j=1;j<=(n<<1);++j){
			a[i][j]=mul(a[i][j],inv);
		}
	}
	for(int i=1;i<=n;++i){
		for(int j=1;j<=n;++j){
			printf("%d ",a[i][j+n]);
		}pc('\n');
	}
	return 0;
}

LGV引理

LGV 引理模板题

内容:

对于

\[M= \left\{ \begin{matrix} e(a_1,b_1) & e(a_1,b_2) & \dots & e(a_1,b_n) \\ e(a_2,b_1) & e(a_2,b_2) & \dots & e(a_2,b_n) \\ \vdots & \vdots & \ddots & \vdots \\ e(a_n,b_1) & e(a_n,b_2) & \dots & e(a_n,b_n) \end{matrix} \right\} \]

\[|M|=\sum_{S(A\rightarrow B)}(-1)^{inv(S) }\prod_{i=1}^ne(a_i,b_{S_i}) \]

其中 \(S(A\rightarrow B)\)\(A\)\(B\) 的每一组不相交路径,对于每一组 \(S\),表示从 \(A_i\)\(B_{S_{i}}\) 的路径,且对于 \(i\not=j\),满足 \(S_i\)\(S_j\) 没有公共点。

\(w(P)\) 表示,对于一条路径 \(P\) ,其上的边权之积。\(e(u,v)\) 表示,\(u\)\(v\) 每条路径的 \(w\) 之和。 \(inv(P)\) 表示对于排列 \(P\) ,其写作排列形式后逆序对个数。

用人话说,\(|M|\) 的行列式为所有 \(A\)\(B\) 的不相交路径的带符号和。

证明:

展开行列式,我们发现它与我们定理的区别在于没有保证枚举的路径组不相交。考虑分成 \(S_1\)\(S_2\) 两组,分别表示集合不存在路径相交和存在路径相交。

\[|M|=\sum_{p}(-1)^{inv(p)}\prod_{i=1}^n e(a_i,b_{p_i})\\ =\sum_{p}(-1)^{inv(p)}\prod_{i=1}^n\sum_{P:a_i\rightarrow b_{p_i}} w(P)\\ =\sum_{p}(-1)^{inv(p)}\sum_{P_i:a_i\rightarrow b_{p_i} }w(P_i)\\ =\sum_{p}(-1)^{inv(p)}\sum_{P_i:a_i\rightarrow b_{p_i},P\in S_1}w(P_i)+\\ \sum_{p}(-1)^{inv(p)}\sum_{P_i:a_i\rightarrow b_{p_i},P\in S_2}w(P_i)\\ \]

因此考虑证明:

\[\sum_{p}(-1)^{inv(p)}\sum_{P_i:a_i\rightarrow b_{p_i},P\in S_2}w(P_i)=0 \]

这里的式子感性的理解一下就是 \((-1)\) 那个项正好把整个式子消掉了,具体证明可以看 这里

考虑在两个有交的路径在交点处交换路径,相当于在原先的排列里交换两个数,于是奇偶性改变。关于 \(w\) 值为何相等,你把权为 \(x\not =0,1\) 的看作 \(x\) 条权为 \(1\) 的边即可得到等价证明。

当起点终点有对应关系而不是随意匹配的时候,直接 LGV引理 是做不了的。但是在网格图中有更多性质,在网格图中,如果对应顺序不是排列,那么必定存在相交,于是直接求即可。

参考代码啥的就没啥必要了,和求行列式的代码没太大区别吧。

特征多项式(矩阵特征值)

矩阵树定理

Matrix-Tree 定理模板题

本内容不给出证明。

先声明一下记号:

度数矩阵:\(D(G)\) 表示一个矩阵,只有对角线上的元素有值,\(D(G)_{i,i}\) 值为 \(deg(i)\)(在图 \(G\) 上而言)。对于有向图有出入度矩阵 \(D^{in}(G),D^{out}(G)\),定义类似。

定义领接矩阵 \(E(G)\) ,\(E(G)_{i,j}\) 上为 \(i\)\(j\) 的边数。

定义 Laplace 矩阵 \(L(G)\) 满足:

\[L(G)=D(G)-E(G) \]

\(t(G)\) 表示 \(G\) 的生成树个数。

内容:

那么对于无向图有:

\[t(G)=|M(G)|\tag{1} \]

其中 \(M(G)\)\(L(G)\) 的余子式。(删去一行一列,可知 \(L(G)\) 的任意余子式相同)。

以及:

\[t(G)=\frac{1}{n}\lambda_1\lambda_2\dots\lambda_{n-1}\tag{2} \]

其中 \(\lambda_i (i\in[1,n-1] )\) 表示 \(L(G)\)\(n-1\) 个非零特征值。

对于有向图有:

\[t^{in}(G,k)=|M^{in}(G,k)|\tag{3} \]

\[t^{out}(G,k)=|M^{out}(G,k)|\tag{4} \]

其中 \(t^{out}(G,k)\) 表示 \(k\) 为根的内向树个数,\(t^{in}(G,k)\) 表示 \(k\) 为根的外向树个数。\(M(G,k)\)\(L(G)\) 的删去第 \(k\) 行第 \(k\) 列的余子式。

BEST定理

\(G\) 是又向欧拉图,那么 \(G\) 的不同欧拉回路总数 \(e(G)\) 满足:

\[e(G)=t^{in}(G,k)\prod_{v\in V}(deg(v)-1)!\tag{5} \]

其中 \(k\) 任选(且欧拉图 \(G\) 的所有节点出入度都相等)。

线性基

线性基模板题

定义与性质

线性基是向量空间的一组基。对此我们先了解一点概念。

如果把向量空间看作集合,那么它满足,对于其中所有元素满足某些操作的封闭性。这些操作是视需求而定的。

而对于向量空间的一组基,它则满足:

1.基内部元素对于某些操作线性无关。(即互相不可通过某些操作来得到)

2.基内的元素可以通过某些操作得到整个向量空间。

而对于线性基来说,它是一个原集合的一组基,且对于异或操作满足上述条件(当然你可以自己创造一些对于别的操作来说的线性基,比如加法,虽然这有点蠢,因为你会发现很少有让加法满足封闭性的条件)。可以认为它是由原集合导出的一个集合。

然后你就会发现它满足了一些性质:

1.线性基内部的元素不由线性基内的其他元素相异或得到。

2.原集合的元素都可以通过线性基内的元素异或得到。

3.线性基中的元素不一定在原集合中存在。但是如果将原集合扩展到满足异或封闭性,那么一定存在。

4.线性基中元素的异或得到原集合的元素的方案唯一且互不相同。

5.在某种意义下,\(0\) 可以看做线性基中的一个元素,它甚至满足线性基的性质,但是它似乎没有太大实际用处。因此线性基不存在异或和为 \(0\) 的非空子集。

6.线性基中元素的二进制最高位不互相同。

构造

考虑如果构造一个线性基。

6,可以考虑按照二进制的最高位来存储线性基。因此我们枚举原集合的元素,然后对于每个元素 \(x\),从二进制高位到低位扫描,如果这一位没有存储且 \(x\) 二进制的这一位为 \(1\) ,那么直接把这个元素放进去,结束。如果这一位有存储且 \(x\) 二进制的这一位为 \(1\),那么让当前这个数异或上存储的数,再向低位扫描。如果 \(x\) 二进制的这一位为 \(0\) ,那么直接向低位扫描。

容易发现这样的构造满足所有性质。

代码实现
#include<bits/stdc++.h>
#define ll long long
#define db double
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define Better_IO true
namespace IO{
	#if Better_IO==true
		char buf[(1<<20)+3],*p1(buf),*p2(buf);
		const int lim=1<<20;
		inline char gc(){
			if(p1==p2) p2=(p1=buf)+fread(buf,1,lim,stdin);
			return p1==p2?EOF:*p1++;
		}
		#define pc putchar
	#else
		#define gc getchar
		#define pc putchar
	#endif
	inline bool blank(const char &c){
		return c==' ' or c=='\n' or c=='\t' or c=='\r' or c==EOF;
	}
	inline void gs(char *s){
		char ch=gc();
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {*s++=ch;ch=gc();}
		*s=0;
	}
	inline void gs(std::string &s){
		s="";char ch=gc();s+='#';
		while(blank(ch) ) {ch=gc();}
		while(!blank(ch) ) {s+=ch;ch=gc();}
	}
	inline void ps(char *s){
		while(*s!=0) pc(*s++);
	}
	inline void ps(const std::string &s){
		for(auto it:s) 
			if(it!='#') pc(it);
	}
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			db p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p*=0.1;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
};
using IO::read;
using IO::gs;
using IO::ps;
const int N=50+3;
int n;
ll s[N];
inline void ins(ll x){
	for(int i=50;i>=0;--i){
		if((x>>i)&1){
			if(s[i]){
				x^=s[i];
				continue;
			}
			s[i]=x;
			break;
		}
	}
}
int main(){
	file(a);
	read(n);
	for(int i=1;i<=n;++i){
		ll x;read(x);
		ins(x);
	}
	ll ans=0;
	for(int i=50;i>=0;--i){
		if(s[i]){
			if((ans^s[i])>ans)
				ans^=s[i];
		}
	}
	printf("%lld\n",ans);
	return 0;
}
应用

由于我们是按照二进制的最高位存储,可以发现我们从二进制高位向低位扫一遍,然后尝试异或。如果变大就更新,否则不变。因为只有这个位置存在改变这一位的机会,而且如果这一位变大,那么低位再怎么变大都不如这样操作大,因此正确性可得。于是可以这样求集合异或得到的最大值。

对于最小值,那么线性基中最小的元素就是答案。除非原集合有 \(0\) 的情况,那么答案是 \(0\)。你发现这个元素不管异或线性基中其他的哪个元素,它都会增大,因此可知。

查询一个数可否被异或得到,那么只需按照构造时插入的方法做一次,如果最后得到了 \(0\) ,那么可异或得到。

线性基求交

国家候选队互测 2022 枪打出头鸟

首先证明一个引理:

如果 \(V_1,V_2\) 两个线性空间,其分别的一组基底 \(B_1,B_2\) ,对于 \(W=V_1\cap B_2\),若 \(B_1\cup (B_2/W)\) 线性无关,那么 \(W\)\(V_1 \cap V_2\) 的一组基底。

证明:

考虑反证法,假设 \(\exist x\in V_1\cap V_2\),使得 \(x\) 不能被 \(W\) 线性表示。那么 \(x\) 可被 \(W\)\(B_2/W\) 共同线性表示,由于我们反证的前提,\(B_2/W\) 对于线性表示给出的贡献不为空。那么 \(B_1\)\(B_2/W\) 线性相关,与引理内容的前提矛盾。由此得证。

只要 \(B_1 \cup (B_2/W)\) 满足线性无关即可。如果线性相关了就换一组基底即可。

怎么弄?首先尝试用 \(B_1\cup C\) 去线性表示 \(B_2\) 中的每个元素,如果不能线性表示,直接把这个元素加入 \(C\) 。否则考虑把它异或上这一位的数,进行一个线性组合,然后记录 \(B_1\) 对于这个线性组合的异或和的贡献。这个数被线性组合完成后,由于我们的记录,我们可以知道那些对这个数进行过操作的 \(B_1\) 的元素的异或和,他们在组合当前这个数时被用于合成可以表明他们是交集。

很显然不能被线性表示的 \(B_2\) 中的元素会被丢弃,这是正确的。理解一下这个就是我们前面证明的引理中描述的对象。

这里 有更加严谨的证明。不过也可以感性理解一下,不怎么会考本质。

给出参考代码:

inline int highpos(ull x){
	return std::__lg(x);
}
struct Basis{
	ull b[64];
	Basis(){
		memset(b,0,sizeof(b) );
	}
	inline ull& operator [](int x){
		return b[x];
	}
	friend inline Basis operator & (Basis a,Basis b){
		Basis path(a),c(a),ans;
		for(int i=0;i<=63;++i){
			if(b[i]){
				ull d=b[i],p=0;
				while(d){
					int j=highpos(d);
					if(c[j]){
						d^=c[j];p^=path[j];
						if(d) continue;
						ans[i]=p;
					}else c[j]=d,path[j]=p;
					break;
				}
			}
		}
		return ans;
	}
	inline void ins(ull x){
		while(x){
			int i=highpos(x);
			if(b[i]){
				x^=b[i];continue;
			}else b[i]=x;
			break;
		}
	}
};

一个玄学的点在于合并时如果不从小到大就会错,原因不明。

原因大概从大到小的话同组间不会互相贡献,退化成了一个一个加入 \(B_1\) 中参与组成的元素。

posted @ 2022-09-04 22:40  cbdsopa  阅读(241)  评论(1编辑  收藏  举报