线性代数相关

矩阵

定义

一个 n×m 的矩阵可看作一个 n×m 的二维数组。一般用方括号或圆括号表示矩阵。

(a11a12a1ma21a22a2man1an2anm)

其余定义

同型矩阵

同型矩阵指的是两个矩阵,如果它们的行数和列数对应相同,那么它们两个就被称为同型矩阵。

方阵

行数等于列数的阵称为方阵。一般所说的「n 阶矩阵」实际上是 n 阶方阵。

主对角线

对于方阵 A ,主对角线就是指 Ai,i 上的元素。

对称矩阵

如果方阵的元素关于主对角线对称,即对于任意的 ijij 列的元素和 ji 列的元素相等,则该矩阵被称为对称矩阵。

对角矩阵

主对角线之外的元素均为 0 的方阵称为对角矩阵,一般记作:

diag{λ1,,λn}

其中 λ1,,λn 是主对角线上的元素。

很显然,对角矩阵也是对称矩阵。

单位矩阵

如果对角矩阵的元素均为 1 ,即主对角线上都是 1 ,其余都是 0 的方阵,称为单位矩阵,记为 I 。只要能进行矩阵乘法, 任何矩阵乘单位矩阵都等于原矩阵。

三角矩阵

如果方阵主对角线左下方的元素均为 0 ,称为上三角矩阵。如果方阵主对角线右上方的元素均为 0 ,称为下三角矩阵。

运算

加法(减法)

两个矩阵进行加减法运算就是把两个矩阵对应位置上的数相加减,即 C=A±Bi[1,n],j[1,m],Ci,j=Ai,j±Bi,j

引用子谦
大佬的几张图。

数乘

类似于向量, λA 就等效于 A 中的每个元素都乘以 λ

数乘运算中,类似于向量的,有以下运算律:

(λμ)A=λ(μA)

(λ+μ)A=λA+μA

λ(A+B)=λA+λB

转置

矩阵的转置,就是在矩阵的右上角写上转置 T 记号, 记为 AT , 表示将矩阵的行与列互换。

对称矩阵转置前后保持不变。

乘法

矩阵乘法只有在第一个矩阵的列数和第二个矩阵的行数相同时才有意义。

AN×M 的矩阵, BM×P 的矩阵,设 C=A×B ,那么 C 矩阵是 N×P 的。

其中 C 中的第 i 行第 j 列元素可以表示为

Ci,j=k=1mAi,k×Bk,j

也就是说,在矩阵乘法中,结果 C 矩阵的第 i 行第 j 列的数,就是由矩阵 AiM 个数与矩阵 BjM 个数分别相乘再相加得到的。口诀为左行右列

矩阵乘法满足以下运算律:

结合律A×B×C=A×(B×C)

矩阵的结合律往往是我们解题的关键。

分配律

(A+B)×C=AC+BC

C×(A+B)=CA+CB

不满足交换律

但有一种特殊情况:A×I=I×A (I 为单位矩阵)

常数优化

image-20221006141637037

i,k,j的顺序最优。

因为 i,ji,k 都在缓存里,不需要重复查找,所以最优,可以卡常。

矩阵加速递推

前置:矩阵快速幂

矩阵快速幂,顾名思义,就是矩阵套个快速幂。
首先把 res 定为初始矩阵,那么就把它初始化成单位矩阵 I ,接着就是快速幂的操作了,然后重载一下乘号就行了。

matrix operator *(matrix &x,matrix &y)
{
	matrix t;
	for(re int i=1;i<=n;i++)
		for(re int k=1;k<=n;k++)
			for(re int j=1;j<=n;j++)
				t.c[i][j] = (t.c[i][j] + x.c[i][k]*y.c[k][j]) % mod;
	return t;
}

il void ksm(int b)
{
	while(b)
	{
		if(b&1) res = res*A;
		A = A*A;
		b >>= 1;
	}
	return ;
}

时间复杂度 O(n3logk)

矩阵加速

先来道例题:P1962 斐波那契数列。题意很简单,就是让你求斐波那契数列的第 nmod 109+7 的值,其中 1n<263

显然有暴力的 O(n) 递推做法,更显然的是,它会超时。

考虑优化。我们知道, Fibn 的值只跟 Fibn1Fibn2 有关,我们在递推时保存最近的两个斐波那契数,即可得到下一个斐波那契数。

F(n) 表示一个 1×2 的矩阵,F(n)=(Fibn,Fibn1) 。而我们想要通过 F(n1)=(Fibn1,Fibn2) 来推得 F(n) 。为此,我们就要把 F(n1) 第一列上的数放在 F(n) 的第二列上,并把 F(n1) 两列上的数求和放在 F(n) 的第一列上。因此,我们可以构造出一个矩阵 A,使得

A=(1110)

那么就有

F(n)=F(n1)×A(Fibn  Fibn1)=(Fibn1  Fibn2)×(1110)

至此,我们得到了一个矩阵乘法的递推式,我们初始化 F(0)=[1  0],目标为 F(n)=F(0)×An 的第一项。因为矩阵乘法满足结合律,我们就可以使用矩阵快速幂来快速计算 F(0)×An。时间复杂度就是 O(23logn) ,其中 23 是矩阵乘法所消耗的时间。矩阵乘法的结合律和递推式使得递推速度大为加快,这也就是我们所说的矩阵加速递推

适用情况

  1. 可以抽象出一个长度为 n 的一维向量,该向量在每个单位时间内发生一次变化。也就是说这个矩阵只有一行;

  2. 变化的形式是一个线性递推(只有若干“加法”,或者“乘一个系数”的运算);

  3. 该递推式在每个时间可能作用与不同的数据上,但本身保持不变;

  4. 向量变化时间(即递推轮数)很长,但向量长度 n 并不大。

有了这几个特点,我们就可以考虑矩阵乘法优化了。我们把这个长度为 n 的一维向量称为状态矩阵,把用于递推状态矩阵的固定不变的这个 A 矩阵称为 转移矩阵。若状态矩阵的第 x 个数对下一单位时间的状态矩阵中的第 y 个数产生影响,则把转移矩阵的第 x 行第 y赋值为适当的系数。

矩阵乘法加速递推的难点就在与构造,构造出一个合适的状态矩阵,再构造出一个合适的转移矩阵,之后就没有难度了,直接套矩阵快速幂即可。时间复杂度是 O(n3logT)T 是递推总轮数。

行列式

符号与定义

det(A),又记作 |A|,定义式为

det(A)=p(1)τ(p)i=1nAi,pi

其中,p1n 的所有排列,τ(p) 表示排列 p 的逆序对数。

只有方阵才有行列式

基本性质

  • 1.对于一个上三角矩阵,它的行列式等于主对角线所有值的乘积

    证明:根据定义可知,要使得 an,pn0,那么 pn 只能取 n;要使 an1,pn10,那么 pn1 只能取 n1n,而 pn=n 了,所以 pn 只能等于 n1,以此类推得到唯一的排列 pi=i,此时 τ(p)=0

  • 2.单位矩阵的行列式为 1,即 |I|=1

    根据单位矩阵主对角线都是 1,其余地方都是 0,结合性质 1 易证。

  • 3.交换矩阵两行,行列式变号。

    交换排列的两个数会让排列逆序对数量改变奇偶性,因此相当于为每个 (1)τ(p)i=1nai,pi 乘上了一个 1,将 1 提出来,就得到了答案。

  • 4.将矩阵某一行乘上一个常数,行列式乘上相同常数。

    每个排列里 的值都会多一个 k,求和的时候把 k 提出来就行了。

  • 5.若矩阵有相同的两行,行列式为 0

    交换这两行会让行列式变号,但是矩阵保持不变,即 |A|=|A|,得到 |A|=0

  • 6.若矩阵有两行存在倍数关系,则行列式为 0

    结合性质 4 和性质 5 易证。

  • 7.若两个矩阵至多有一行不相等,则将这不等的一行相加得到的新矩阵的行列式等于原矩阵行列式之和。

    设不等的编号为 c,则 Ai=Bi=Si,Ac+Bc=Sc(ic)。根据乘法分配律就有 i=1nSi,pi=i=1nAi,pi+i=1nBi,pi,即 |S|=|A|+|B|

  • 8.将矩阵的某一行加上另一行的倍数,行列式不变。

    AdAd+kAc(cd),令操作后的 A 矩阵变为 C。令 Ai=Bi,Bd=kAc(id),则 Bd=kBc,结合性质 6 可知 |B|=0,有根据性质 7 可知 |A|+|B|=|C|,即 |A|=|C|

高斯消元

介绍

高斯消元是一种求解线性方程组的方法。所谓线性方程组,是由 MN 元一次方程共同构成的。线性方程组的所有系数可以写成一个 MN 列的“系数矩阵”,再加上每个方程等号右侧的常数,可以写成一个 MN+1增广矩阵,例如。

{x1+2x2x3=62x1+x23x3=9x1x2+2x3=7(121621391127)

高斯消元就是利用该矩阵进行一些操作来进行的。

初等行变换

上述操作可以分为三种。

  1. 交换两行;
  2. 把某一行一个非 0 的数;
  3. 把某行的若干倍到另一行上去。

进行上述操作之后,我们可以知道,方程组的解并不会发生变化。上述三类操作就被称为矩阵的初等行变换

思路

高斯消元法

高斯消元法的基本思路是利用矩阵的初等行变换将系数矩阵(除最后一列外的地方)消成上三角矩阵,再下到上回代求解。

  1. 枚举主元(主对角线上的元素),找到主元下面系数不是 0 的一行;

  2. 用变换 1 ,把这一行和主元行交换;

  3. 用变换 2 ,把主元系数变成 1

  4. 用变换 3 ,把主元下面的系数变成 0

引用董晓老师的图片

解可以有三种情况:

  1. 唯一解:i 可以枚举完 n 行,也就是说主元n 个。

  2. 无解:存在 ai,i0 ,常数不为 0 的行。也就是出现 0×x=b(b0) 的情况。

  3. 无穷多解:存在 ai,i0 ,常数为 0 的行。也就是出现 0×x=0 的情况。

il bool Gauss()
{
	for(re int i=1;i<=n;i++)//行查找
	{
		int r = i;
		for(re int k=i;k<=n;k++)
			if(fabs(a[k][i]) > eps) { r = k; break; }
		if(r != i) swap(a[r],a[i]);//变换1
		if(fabs(a[i][i]) < eps) return false;//无解或无穷解
		for(re int j=n+1;j>=i;j--) a[i][j] /= a[i][i];//变换2
		for(re int k=i+1;k<=n;k++)
			for(re int j=n+1;j>=i;j--)//a[k][i]相当于从第i行到第k行变化的系数,而a[i][j]就是变哪个数的系数倍
				a[k][j] -= a[k][i]*a[i][j];
	}
	for(re int i=n-1;i>=1;i--)//回代
		for(re int j=i+1;j<=n;j++)
			a[i][n+1] -= a[i][j] * a[j][n+1];
	return true;
}

高斯-约旦消元法

与高斯消元法不同的是,高斯-约旦消元法把系数矩阵消成主对角矩阵,这样每一行 xi 和常数都是一一对应的。令常数除以主元即为答案。

每轮循环,主元所在行不变,主元所在列消成 0

il bool Gauss_Jordan()
{
	for(re int i=1;i<=n;i++)//行查找
	{
		int r = i;
		for(re int k=i;k<=n;k++)
			if(fabs(a[k][i]) > eps) { r = k; break; }
		if(r != i) swap(a[r],a[i]);//变换1
		if(fabs(a[i][i]) < eps) return false;//无解或无穷解
		for(re int k=1;k<=n;k++)//每一列都消去0
		{
			if(k == i) continue;
			double t = a[k][i] / a[i][i];
			for(re int j=i;j<=n+1;j++)//a[i][1~j-1]都是0,再怎么乘还是0,所以不用管
				a[k][j] -= t*a[i][j];
		}
	}
	for(re int i=1;i<=n;i++) a[i][n+1] /= a[i][i];
	return true;
}

应用-矩阵求逆

对于方阵 A,若存在方阵 B,使得 A×B=B×A=I,则称 BA 的逆矩阵,记为 A1

我们可以利用高斯消元在 O(n3) 的时间复杂度内解决该问题。

思路

其实比较简单。

  1. 我们构造 n×2n 的矩阵 (A,I),这指的是一个大矩阵,左边是方阵 A,右边是单位矩阵 I,是一个矩阵,不是两个。

  2. 用高斯-约旦消元法将其化简为 (I,A1),即可得到 A 的逆矩阵 A1

这个的原理其实挺简单。首先根据定义我们可以比较容易得出一个结论:A×(B|C)=(A×B|A×C),由此我们可以把矩阵 (A,I) 左乘一个 A1,这样就使得这个矩阵变成了 (I,A1),其实高斯消元就等效于这个操作。

如果左边出现了全 0 行,说明矩阵 A 不可逆。

代码其实没变多少,这里以模板题为例,模板题是模意义的高斯消元,所以有些地方加上了快速幂来求逆元。

il bool Gauss_Jordan()
{
	for(re int i=1;i<=n;i++)
	{
		int r = i;
		for(re int k=i;k<=n;k++)
			if(a[k][i]) { r = k; break; }
		if(r != i) swap(a[r],a[i]);
		if(!a[i][i]) return 0;
		int x = ksm(a[i][i],mod-2);
		for(re int k=1;k<=n;k++)
		{
			if(k == i) continue;
			int t = a[k][i]*x % mod;
			for(re int j=i;j<=2*n;j++)
				a[k][j] = ((a[k][j] - t * a[i][j]) % mod + mod) % mod;
		}
	}
	for(re int i=1;i<=n;i++)
	{
		int t = ksm(a[i][i],mod-2);
		for(re int j=n+1;j<=2*n;j++)
			a[i][j] = a[i][j] * t % mod;
	}
	return 1;
}

应用-行列式求值

按照定义计算行列式是 O(n!×n) 的,复杂度有点爆炸,我们可以根据它的性质来简化计算。

由性质 1,我们可以可以想到将矩阵转化成上三角矩阵,这样行列式就仅仅是主对角线值的乘积了。接下来我们思考如何转化。

根据性质 3(交换矩阵两行,行列式变号),性质 4(将某一行乘 k,行列式也的值也乘 k),性质 5(将矩阵的某一行的倍数加到另一行上行列式不变),我们可以高斯消元 n3 求行列式。

代码也是模板题的,这个模板题模数非质数,需要用到辗转相除的思想,复杂度均摊 O(n2(logp+n)),证明不大会。

il void Gauss()
{
	sign = det = 1;
	for(re int i=1;i<=n;i++)//行查找
	{
		for(re int k=i+1;k<=n;k++)
		{
			while(a[i][i])
			{
				int t = a[k][i] / a[i][i];
				for(re int j=i;j<=n;j++)
					a[k][j] = (a[k][j] - t * a[i][j] % mod + mod) % mod;
				swap(a[k],a[i]) ,  sign = -sign;
			}
			swap(a[k],a[i]) , sign = -sign;
		}
	}
	for(re int i=1;i<=n;i++) det = (det * a[i][i]) % mod;
	det = (det * sign + mod) % mod;
	cout << det;
}

Matrix-tree 定理

写的会比较简略,因为证明不是很会。

前置:高斯消元求行列式。

Matrix-Tree 定理,顾名思义,它阐明了矩阵和生成树之间的关系,它要用到的矩阵叫基尔霍夫(Kirchhoff)矩阵,我们来说一下它怎么求得。

内容

首先我们给出 Matrix-Tree 定理的内容:

基尔霍夫矩阵的任意一个代数余子式的值,就是所有生成树的边权积的和。

当边权为 1 时,显然我们求的就是生成树个数了。

其中代数余子式的简略定义就是一个 n 阶方阵去掉第 i 行和第 i 列后剩下的 n1 阶方阵的行列式。

此定理对于有向图和无向图之间还有一些区别,下面我们分别讨论一下。

(x,y,z)xy 的一条边权为 z 的无向/有向边。

无向图

其实无向图才是 Matrix-Tree 最本质的内容,有向图其实是一个扩展。

假设给定一个图 G

度数矩阵 D:若存在边 (x,y,z),则 Dx,x+=z,Dy,y+=z

邻接矩阵 C:若存在边 (x,y,z),则 Cx,y+=z,Cy,x+=z

基尔霍夫矩阵的定义是度数矩阵减去邻接矩阵,即 A=DC

我们删去任意一行和任意一列,求剩下的矩阵的行列式即可。

有向图

假设给定一个图 G

度数矩阵 D:若存在边 (x,y,z),则如果要求外向树Dy,y+=z;如果要求内向树Dx,x+=z

邻接矩阵 C:若存在边 (x,y,z),则外向树和内向树均为 Cx,y+=z

基尔霍夫矩阵即 A=DC

我们删去指定的根所在的行和列,求剩下的矩阵的行列式即可。

简式

其实 Matrix-Tree 定理用数学语言来写的话,它求的就是

TeTwe

其中 T 取遍这个图的生成树的边集。运用这个式子,我们可以转化题目中的一些条件。

例题

以模板题为例,给出无环图生成树和有环图外向树的代码,模板题中以 1 为根,所以我们求 2n 的行列式即可。

#include<bits/stdc++.h>
#define int long long
#define ll long long
#define next nxt
#define re register
#define il inline
const int N = 3e2 + 5;
const int mod = 1e9 + 7;
using namespace std;
int max(int x,int y){return x > y ? x : y;}
int min(int x,int y){return x < y ? x : y;}

int n,m,T;
int x,y,w,inv;
int ans,sign;
int a[N][N];

il int read()
{
	int f=0,s=0;
	char ch=getchar();
	for(;!isdigit(ch);ch=getchar()) f |= (ch=='-');
	for(; isdigit(ch);ch=getchar()) s = (s<<1) + (s<<3) + (ch^48);
	return f ? -s : s;
}

il int ksm(int a,int b)
{
	int res = 1;
	while(b)
	{
		if(b&1) res = res * a % mod;
		a = a * a % mod;
		b >>= 1;
	}
	return res;
}

il void Gauss()
{
	sign = ans = 1;
	for(re int i=2;i<=n;i++)//从第二列开始枚举
	{
		int r = i;
		for(re int k=i;k<=n;k++) if(a[k][i]) { r = k; break; }
		if(!a[r][i]) { puts("0"); exit(0); }
		if(r != i) swap(a[r],a[i]) , sign = -sign;
		inv = ksm(a[i][i],mod-2);
		for(re int k=i+1;k<=n;k++)
		{
			int t = a[k][i] * inv % mod;//a[k][i]/a[i][i]
			for(re int j=i;j<=n;j++)
				a[k][j] = ((a[k][j] - t * a[i][j]) % mod + mod) % mod;
		}
	}
	return ;
}

signed main()
{
	n = read() , m = read() , T = read();
	for(re int i=1;i<=m;i++)
	{
		x = read() , y = read() , w = read();
		if(!T)//无向图
		{
			a[x][x] = (a[x][x] + w) % mod , a[y][y] = (a[y][y] + w) % mod;
			a[x][y] = (a[x][y] - w + mod) % mod , a[y][x] = (a[y][x] - w + mod) % mod;
		}
		else a[y][y] = (a[y][y] + w) % mod , a[x][y] = (a[x][y] - w + mod) % mod;//有向图外向树
	}
	Gauss();
	for(re int i=2;i<=n;i++) ans = (ans * a[i][i]) % mod;
	ans = (ans * sign + mod) % mod;
	cout << ans;
	return 0;
}

这个题就运用到了我前面所说的简式 TeTwe,实际上我补充了这个地方就是因为这个题。

这个题因为还要保证只有 n1 条边,所以说这个题的贡献不仅仅是生成树的概率 TeTpe(这里设 pe 就是 e 这条边还在的概率),还要乘上一个 eT(1pe),于是式子就变成了

TeTpeeT(1pe)

此时就有一步比较巧妙的转化:

eT(1pe)=e(1pe)eT(1pe)

那么式子就转化成了

TeTpeeT(1pe)e(1pe)

我们发现最后那个连乘其实是一个常数,我们把它提到外面去

e(1pe)×TeTpe(1pe)

我们以右边的分式作为 we,运用 Matrix-Tree 定理解决即可。注意有可能 pe=1,会出现分母等于 0 的情况,我们给它微小扰动一下即可。

for(re int i=1;i<=n;i++)
	for(re int j=1;j<=n;j++)
	{
		scanf("%lf",&A[i][j]);
		if(i != j)
		{
			if(1-A[i][j] < eps) A[i][j] -= eps;
			if(i < j)
			{
				ans *= (1 - A[i][j]);
				w = A[i][j] / (1-A[i][j]);
				a[i][i] += w , a[j][j] += w;
				a[i][j] -= w , a[j][i] -= w;
			}
		}
	}

发现我是个神必..其实这个建图很好建立,相邻的 . 连边就行了。但是需要注意的是,矩阵树定理是给出一个连通图求生成树个数的,你如果图都不连通你求啥生成树啊。

就是说我们需要保留有用的状态,题目中的柱子就是无用状态,你不能把它放到矩阵中,因为这样一定会出现对角线为 0 的情况,这样会导致行列式为 0,而答案为 0 的情况只有可能是有那种被四个柱子或边界挡住的房间。

for(re int i=1;i<=n;i++)
	for(re int j=1;j<=m;j++)
	{
		cin >> ch[i][j];
		if(ch[i][j] == '.') ++cnt , id[i][j] = cnt;//只保留有用状态!!!
	}
for(re int i=1;i<=n;i++)
	for(re int j=1;j<=m;j++)
	{
		if(ch[i][j] == '.')
		{
			if(j+1 <= m && ch[i][j+1] == '.')
			{
				x = id[i][j] , y = id[i][j+1];
				a[x][x]++ , a[y][y]++;
				a[x][y] = (a[x][y] - 1 + mod) % mod , a[y][x] = (a[y][x] - 1 + mod) % mod;
			}
			if(i+1 <= n && ch[i+1][j] == '.')
			{
				x = id[i][j] , y = id[i+1][j];
				a[x][x]++ , a[y][y]++;
				a[x][y] = (a[x][y] - 1 + mod) % mod , a[y][x] = (a[y][x] - 1 + mod) % mod;
			}
		}
	}

容斥 + Matrix-Tree。

如果在求行列式的过程中考虑不同公司的贡献的话是比较难的,这就指引我们要在求生成树的过程前把这种贡献表示出来,这就又提醒我们可以用容斥。我们枚举子集,表示哪几个公司上去修路。设 fi 表示有 i 个公司上去修路的可能,那么最后的答案就是

i=0n1(1)n1ifi

我们 O(2n1) 枚举即可,最后的复杂度就是 O(2n1n3)

int n,N,x,y,ans,tot,inv,sign,cnt;
int a[M][M],num[M];
struct node{
	int u,v;
}opt[M][M*M];

il void Gauss()
{
	sign = tot = 1;
	for(re int i=2;i<=n;i++)
	{
		int r = i;
		for(re int k=i+1;k<=n;k++) if(a[k][i]) { r = k; break; }
		if(!a[r][i]) { tot = 0; return ; }
		if(r != i) swap(a[r],a[i]) , sign = -sign;
		inv = ksm(a[i][i],mod-2);
		for(re int k=i+1;k<=n;k++)
		{
			int t = a[k][i] * inv % mod;
			for(re int j=i;j<=n;j++)
				a[k][j] = ((a[k][j] - t * a[i][j]) % mod + mod) % mod;
		}
	}
	for(re int i=2;i<=n;i++) tot = tot * (a[i][i] + mod) % mod;
	tot = (tot * sign + mod) % mod;
}

signed main()
{
	n = read() , N = n-1;
	for(re int i=0;i<N;i++)
	{
		num[i] = read();
		for(re int j=1;j<=num[i];j++) x = read() , y = read() , opt[i][j] = {x,y};
	}
	for(re int i=0;i<(1<<N);i++)
	{
		memset(a , 0 , sizeof a);
		cnt = 0;
		for(re int j=0;j<N;j++)
		{
			if(i & (1<<j))
			{
				cnt++;
				for(re int k=1;k<=num[j];k++)
				{
					x = opt[j][k].u , y = opt[j][k].v;
					a[x][x]++ , a[y][y]++;
					a[x][y] = (a[x][y] - 1 + mod) % mod , a[y][x] = (a[y][x] -1 + mod) % mod;
				}
			}
		}
		Gauss();
		if((N-cnt)&1) ans = (ans - tot + mod) % mod;
		else ans = (ans + tot) % mod;
	}
	cout << ans;
	return 0;
}

BEST 定理

G 是有向欧拉图,那么 G 的本质不同的欧拉回路的总数为

TvV(deg(v)1)!

其中 T 是这个图的内向生成树的个数。

用的很少,提一嘴就行了。

线性空间

矩阵的加法和数乘运算所具有的不少性质,是一般线性空间同样具有的,事实上线性空间的定义本身就是从矩阵(以及其它一些数学对象)所具有的运算性质中抽象出来的。”

定义

线性空间

线性空间是一个关于以下两个运算封闭的向量集合。

  1. 向量加法 a+b ,其中 a,b 均为向量。

    而加法要满足这么几个性质:

    (1) α+β=β+α

    (2) α+β+γ=α+(β+γ)

    (3) 线性空间中有这么一个 0 元素,满足 α+0=α

    (4) 对于向量 α ,都有一个 α 与之对应

  2. 标量乘法 k×a ,也称数乘运算,其中 a 是向量,k 是标量。

    而数乘需要满足这么几个性质:

    (1) 1α=α

    (2) k(lα)=(kl)α    (k,l为标量)

    (3) (k+l)α=kα+lα

    (4) k(α+β)=kα+kβ

满足这两个大条件,就说这是一个线性空间。我们由此可以知道,这里的加法和乘法是广义的加法和乘法,也就是只要满足这几个条件,就能把它称之为加法和乘法,类似于 C++ 里面的运算符重载。

生成子集

给定若干个向量 a1,a2,,ak , 若向量 b 能由 a1,a2,,ak 经过加法和数乘得出,则称向量 b 能被向量 a1,a2,,ak 表出。那么,a1,a2,,ak 能表出的所有向量构成一个线性空间,a1,a2,,ak 被称为这个线性空间的生成子集

线性相关和线性无关

任意选出线性空间内的若干个向量,如果其中存在一个向量能被其他向量表出,则称这些向量线性相关,否则称这些向量线性无关

基和维数

线性无关的生成子集被称为线性空间的基底,简称。基的另一种定义是线性空间的极大线性无关子集。一个线性空间内的所有基包含的向量个数一定都相等,这个个数被称为线性空间的维数

以平面直角坐标系为例。平面直角坐标系中的所有向量构成一个二维线性空间,它的一个就是单位向量集合{(0,1),(1,0)} 。实际上,任意两个不共线的向量都是这个线性空间的一个基底,而不共线就满足了它们是线性无关的。平面直角坐标系的 x 轴上的所有向量构成一个一维线性空间,它的一个基就是 {(1,0)}

矩阵的秩

对于一个 nm 列的矩阵,我们可以把它的每一行当作一个长度为 m 的向量,称为行向量。矩阵的 n 个行向量能够表出的所有向量构成一个线性空间,这个线性空间的维数被称为矩阵的行秩。类似地,我们可以定义列向量和列秩。事实上,矩阵的列秩一定等于行秩,他们都被称为矩阵的

把这个 n×m 的矩阵看作系数矩阵进行高斯消元(增广矩阵的最后一列全看作 0),得到一个对角矩阵。显然,对角矩阵的所有非零 行向量线性无关。事实上,矩阵的初等行变化就是行向量之间进行的加法和数乘,所以高斯消元不会改变矩阵的行向量表出的线性空间。由此可知,对角矩阵的所有非零行向量矩阵该线性空间的一个基,非零行向量的个数就是矩阵的

为零的行向量因为可以由其它非零行向量 ×0 的出,把它加上就线性有关了,因此要去掉,保证是线性无关的生成子集。

线性基

前置知识:上文提到的线性空间的一些内容。

下文只讨论异或空间的线性基,如果是实数空间,高斯消元是求实数空间的线性基的好方法。

类似于上文线性空间的一些定义,我们简单定义一下异或空间的一些定义。

参考文章:

定义

异或和

S 为一无符号整数集,其异或和为

xor-sum(S)=S1S2S|S|

生成子集与张成

若整数 k 能由整数 S1,S2,,S|S| 经异或运算得出,则称 k 能被 S1,S2,,S|S| 表出。S1,S2,,S|S| 能表出的所有整数构成一个异或空间,S1,S2,,S|S| 称为这个异或空间的生成子集。

这个构成的异或空间称为集合 S 的张成,记作 span(S)

线性相关与线性无关

任意选出异或空间中的若干个整数,如果存在一个整数能被其他整数表出,则称这些整数线性相关,否则称这些整数线性无关。异或空间的就是异或空间中一个线性无关的生成子集,或者定义为异或空间的极大线性无关子集。

线性基

其实上面已经给出了,我们详细地给它定义一下。

若称集合 B 是集合 S 的线性基,当且仅当:

  1. Sspan(B),即 SB 张成的子集

  2. B 是线性无关的。

集合 B 中元素的个数,称为线性基的长度。

线性基有一下基本性质:

  1. B 是极小的满足线性基性质的集合,它的任何真子集都不可能是线性基;

  2. S 中的任意元素都可以唯一表示为 B 中若干个元素异或起来的结果。

构造与性质

这种构造学的 Menci,它的原理其实是高斯消元,这玩意只可意会不可言传,看多了其实你能感觉到它和高斯消元的相同之处。

设集合 S 中最大的数在二进制意义下有 L 位,我们使用一个 [0L] 的数组 a 来存储线性基。

这种线性基的构造方法保证了一个特殊性质,对于每一个 i,ai,有以下两种可能:

  1. ai=0,并且
  • 只有满足 j>iaj(就是位于 ai 后面的 aj)的第 i 个二进制位可能1;
  1. ai0,并且
  • 整个 a 数组只有 ai 的第 i 个二进制位为 1

  • ai 更高的二进制位(>i 的二进制位)一定0

  • ai 更低的二进制位(<i 的二进制位)可能1

我们称第 i存在于线性基中,当且仅当 ai0

这其实就相当于第 ai 存储了第 i 位是 0 还是 1,我们利用这个性质能解决诸多问题。

Menci 的博客里给出了例子,我不要脸的引用一下/cy。

知道了性质以后,我们根据性质来构造线性基。

首先,线性基是动态构造的,你给定不同的数它就可能构造出不同的线性基,这比较显然。我们从空的 a 数组开始,每次考虑在一个已存在的线性基中插入一个数 t 即可。

t 最高位上的 1 开始考虑,设这是第 j 位,如果这一位已经存在与线性基中,则我们需要将 t 中的这一位消掉(将 t 异或上 aj),才可以继续插入(因为只有 aj 的第 j 位可以为 1),如果这一位不存在于线性基中,则可以将 t 插入到 aj 的位置上,但插入的时候需要保证:

  1. t 中比第 j 位更高的已经存在于线性基中的二进制位必须为 0,而这时候 t 的最高位上的 1 一定是第 j 位,这个我们可以保证,无需考虑。

  2. t 中比第 j 位更已经存在于线性基中的二进制位上必须为 0,为了满足这个性质,我们可以枚举 t 中为 1 的这些二进制位 k(k[0,j)) 对应的元素 ak,类似的我们用 t 异或上 ak 的方式来消去这些位上的 1

  3. a 中必须只有 aj 的第 j 位为 1

  • a 中在 aj 前面的也就是位数比它小元素的第 j 位必须为 0,这一点必然已经满足,假设有一个 k[0,j) 对应的元素 ak 满足 ak 的第 j 位为 1,那么它在插入的时候一定被插入到 aj 的位置上,所以无需考虑。

  • a 中在 aj 后面的也就是比 aj 大的元素的第 j 位必须为 0,这个就不一定能满足了,我们需要维护一下。也很简单,我们可以枚举 aj 后面的元素 ak(k(j,L]),将每个第 j 位为 1ak 异或上 t,注意我们一定要先进行第二步再进行这个第三步,因为在 ak 异或 t 的时候一定要保证第二条的性质,而通过第二条操作完后,这样的性质就能被保证了。

流程

我们再来简易地捋一遍这个流程

从高位到低位枚举 t 所有的二进制位 j=L0,对于每个为 1j

  • 如果 aj0,则令 t=t xor aj

  • 如果 aj=0,那么

    • 枚举 k[0,j),如果 t 的第 k 位为 1,则令 t=t xor ak

    • 枚举 k(j,L],若 ak 的第 j 位为 1,则令 ak xor t

    • aj=t,结束插入。

给出代码

il void Insert(int x)//插入
{
	if(!x) return ;//0就不用被插进去了
	for(re int i=N;i>=0;i--)
	{
		if(!(x & (1ll << i))) continue;//这一位是0,跳过
		if(a[i]) x ^= a[i];//不是0,如果有数,异或一下往后走
		else//如果没数,就插到这里
		{
			for(re int k=0;k<i;k++) if(x & (1ll << k)) x ^= a[k];//维护线性基的性质
			for(re int k=i+1;k<=N;k++) if(a[k] & (1ll << i)) a[k] ^= x;
			a[i] = x;
			return ;//插入完了应结束流程
		}
	}
	flag = 1;//这个flag后面有用
}

证明

我们枚举 S 中所有的元素 t,挨个插入,并将数组 a 中的数放到一个集合 A 里,那么这个 A 就是 S 的线性基。

我们想一想它的插入过程,我们每次找到 t 最高位上的 1,用线性基里的数把它消去,如果最终 t 被消成 0 了,说明它可以被线性基里的数表出,我们就不放进去,这样就保证 A 是线性无关的。对于被插入到 a 中的元素,它们在插入前都做了一些变换,这些变化都是使它异或上 a 中已存在的元素,或者使 a 中的元素异或上它,这些变换都是可逆的,所以用 a 中的一些元素的异或和可以表示出 S 集合里的元素。

可以看出,一次插入的复杂度为 O(logt),空间复杂度也是 O(logt)

应用

学会构造以后,我们看一下它的一些应用。

合并线性基

这个其实不算应用,就是把两个集合的线性基在 O(log2t) 的时间内进行合并,合并后得到的线性基为两个集合的并的线性基。

这其实也很简单,把一个线性基中的所有元素插入到另一个线性基中即可。

il void Merge(int a[],int b[])//合并线性基
{
	for(re int i=0;i<=N;i++) Insert(b[i]);
	return ;
}

判断 x 是否能被表出

这个的原理和构造的原理类似,如果 x 的第 j 位为 1,那么若 aj=0,说明 x 不能被表出,否则令 x 异或上 aj 继续往后扫就行了,最终如果能扫到 0,说明 x 能被表出。

il bool check(int x)//查询x是否在线性基中
{
	if(!x) { if(flag) return true; else return false; }
	for(re int i=N;i>=0;i--)
	{
		if(x & (1ll<<i))
		{
			if(!a[i]) return false;
			else x ^= a[i];
		}
	}
	return true;
}

异或最大值

给定一个集合 S,求它的一个子集 TS,使得 xor-sum(T) 最大,求出这个最大值。

这种构造方法的好处之一就体现在这里。从高到低考虑在线性基中的二进制位 j,如果第 j 位存在于线性基中,则考虑到线性基中只有唯一一个元素的第 j 位为 1,所以它之前的异或和第 j 位肯定为 0,我们把它加入到 T 中一定会使得答案更大,因此,求出线性基所有元素的异或和,即为答案。

il int Query_Max()//查询最大值
{
	ans = 0;
	for(re int i=0;i<=N;i++) ans ^= a[i];
	return ans;
}

异或和最小值

有异或最大值,就有异或最小值。

这里我之前在构造线性基里提到的 flag = 1 就有作用了。如果说我们插入了一个数 t,它没有被插入到线性基中,说明它是能被线性基中某些元素表出的,我们让这些元素异或上 t ,最小值就出来了:0。所以说这个标记的作用就是让我们判断最小值是否是 0 的。

如果 0 不会被表示出来。那么我们就考虑最小的第 j 位是 1aj 了,因为任何一个线性基里的数异或它都会使得答案变大,所以它即为答案。

il int Query_Min()//查询最小值
{
	if(flag) return 0;
	for(re int i=0;i<=N;i++) if(a[i]) return a[i];
	return 0;
}

k 小异或和

这种构造方式的第二个优点体现在这里。

首先我们求出集合的线性基 B,选择线性基的一个非空子集共有 2|B|1 种方案,如果 |B|<n,则说明至少有一个数没有被插入到线性基当中,也就是被表出了,这样的话 0 就能被表示出来了,也就是当 flag=1 的时候,会有 2|B| 种方案。

k> 方案数,无解。

否则,将 k 表示成一个长度为 |B| 的二进制数(高位补 0)。k 的二进制排列符合以下性质:

  1. 高位上的 1 比低位上的 1 能使 k 更大;

  2. 不论哪一位,如果这一位是 1,一定会使得 k 更大。

而线性基的 |B| 个元素刚好代表了异或后结果的 |B| 个二进制位,而二进制的规律恰好
与从线性基中选数相对应:

  1. 选择「控制较高位上的 1 的元素」比「控制较低位上的 1 的元素」能使得异或和更大

  2. 不论哪一位,如果选择了哪一位「控制 1 的元素」,一定会使得异或和更大。

根据这个性质,解法就出来了:枚举 k 所有为 1 的二进制位,如果第 i 位为 1,则令答案异或上线性基中控制第 i 小的元素,注意,这不一定是 ai,因为有些 ai 是等于 0 的,不会产生贡献,因此我们会开一个 vector 存储那些不是 0 的位。

il int Query_kth(int k)
{
	if(flag) k--;//0的情况,如果有 0 的话,0算第 1 小,而线性基统计的是非 0 的第 k小,所以-1
	if(k > (1ll<<v.size()) - 1) return -1;
	ans = 0;
	for(re int i=0;i<(int)v.size();i++)
		if(k & (1ll<<i)) ans ^= v[i];
	return ans;
}
signed main()
{
	n = read();
	for(re int i=1;i<=n;i++) x = read() , Insert(x);
	v.clear();
	for(re int i=0;i<=N;i++) if(a[i]) v.push_back(a[i]);
	return 0;
}

后记

当然线性基的应用远不止于此,还有线性基求交,待删除线性基等一些科技,鉴于我现在的能力范围还达不到,故略去。

总·后记

OI 中有关线性代数的部分明面上就这么多,当然还是鉴于能力范围,一些更深入层次上的部分我还没有涉及。还是那句话,如果你想学数学,在有时间的情况下建议还是看书,成体系的知识往往比零碎的好。但可惜我没有那么多的时间,因此只能自己整理一下可能用到的内容。也希望这篇博客对读者能够有所帮助。

posted @   Bloodstalk  阅读(183)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· Trae初体验
点击右上角即可分享
微信分享提示