线性代数学习笔记

1. 基础知识

推荐 3b1b 《线性代数的本质》

1.1.向量

向量的英文叫 vector,就是那个超级好用的 STL 的容器的名字来源。向量可以表示一组元素,如:向量 A=(a1,a2,a3,...,an)

其中向量 A=(a1,a2,a3,...,an) 这样的向量被称作行向量。

而形如:

(a1a2an)

则称作列向量

1.1.1.向量数量积

两个 n 维向量 A=(a1,a2,,an)B=(b1,b2,,bn) 的数量积(也称作点积、内积)为

A·B=a1b1+a2b2++anbn

1.2.矩阵

矩阵可以理解为一个 nm 列的矩阵。比如:

(123456)

一般来说,我们主要会用到的矩阵都是方阵,即 n=m 的矩阵。

1.2.1.矩阵乘法

矩阵乘法只有在前一个矩阵是 nm 列,后一个矩阵是 mk 列时才有意义。

A 是一个 nm 列的矩阵,B 是一个 mk 列的矩阵,设 A×B=C,则 C 是一个 nk 列的矩阵。并且对于所有 1in,1jk,都有:

Ci,j=l=1mAi,l×Bl,j

矩阵乘法满足结合律,但不满足交换律。

1.2.2.单位矩阵

一个 nn 列的单位矩阵 I 指的是从左上到右下这条对角线上的数都为 1,其他位置都是 0 的矩阵。

n=3 时:

I=(100010001)

对于任意一个 nn 列的方阵 A,都有:

A×I=A

I×A=A

可以把它视作普通乘法中的 1

1.2.3.代码实现

我们可以用一个结构体来存矩阵,具体可以用 vector 或者二维数组来存。

点击查看代码
struct Mtr {
	int n;
	vector<vector<long long> > m;
	Mtr () {
		n = 0;
	} 
	Mtr (int x) {
		n = x;
		m = vector<vector<long long> >(n, vector<long long>(n, 0));
	}	
};

对于矩阵乘法,我们可以通过重载运算符来实现:

点击查看代码
Mtr operator*(Mtr x, Mtr y) {
	Mtr ans = Mtr(x.n);
	for (int i = 0; i < ans.n; i++)
		for (int j = 0; j < ans.n; j++)
			for (int k = 0; k < ans.n; k++)
				ans.m[i][j] += x.m[i][k] * y.m[k][j] % mod, ans.m[i][j] %= mod;
	return ans;
}

注意矩阵乘法复杂度是 O(n3) 的。

1.3.矩阵的应用

1.3.1.矩阵快速幂

我们知道,计算 an 这种幂可以用快速幂来加速,时间复杂度 O(logn)

由于矩阵乘法是满足结合律的,所以对于一个方阵 AAn 也可以用快速幂加速计算。

如果 A 是一个 mm 列的方阵,则快速幂的时间复杂度是 O(m3logn)

点击查看代码
Mtr fpow(Mtr x, long long b) {
	if (b == 0) {
		Mtr ans = Mtr(x.n);
		for (int i = 0; i < ans.n; i++)
			for (int j = 0; j < ans.n; j++)
				ans.m[i][j] = (i == j);
		return ans;
	}
	Mtr t = fpow(x, b / 2);
	return (b % 2 == 1) ? (t * t * x) : (t * t);
}

1.3.2.加速递推

有一些动态规划或者计数问题中,递推的式子都是一样的,这样的递推有时可以用矩阵快速幂来加速。

P1962 斐波那契数列

比如斐波那契的递推式,正常递推是 O(n) 的:

Fi={1i=1,2Fi1+Fi2Otherwise.

我们尝试构造一个式子形如:

Ax=X

其中 A 是方阵,x,X 都是大小和 A 行数相同的向量。把矩阵写出来就是:

(A1,1A1,2A1,nA2,1An,1An,n)(x1x2xn)=(X1X2Xn)

根据矩阵乘法的定义我们可以的得到:

Xi=k=1nAi,k×xk

由于斐波那契的递推式只涉及前两项,我们的目标就是构造一个 2×2 的矩阵 A 使得:

(A1,1A1,2A2,1A2,2)(Fi2Fi1)=(Fi1Fi)

所以我们有:

{Fi1=A1,1Fi2+A1,2Fi1Fi=A2,1Fi2+A2,2Fi1

再根据 Fi=Fi1+Fi2 就可以得到:

A=(0111)

所以我们就有:

(0111)(Fi2Fi1)=(Fi1Fi)

相当于我们只要不断地乘上一个 A,向量便会不断更新,每次更新都可以多求出一个斐波那契数。

即:

(0111)n2(11)=(Fn1Fn)

这样我们就可以用矩阵快速幂来加速这个过程,时间复杂度 O(logn)

当然,实现中我们可以把 x,X 都写成一个 2×2 的方阵,除了第一列都是 0

这样递推就是 O(logn) 的了。

点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int mod = 1e9 + 7;

struct Mtr {
	int n;
	vector<vector<long long> > m;
	Mtr () {
		n = 0;
	} 
	Mtr (int x) {
		n = x;
		m = vector<vector<long long> >(n, vector<long long>(n, 0));
	}	
};
Mtr operator*(Mtr x, Mtr y) {
	Mtr ans = Mtr(x.n);
	for (int i = 0; i < ans.n; i++)
		for (int j = 0; j < ans.n; j++)
			for (int k = 0; k < ans.n; k++)
				ans.m[i][j] += x.m[i][k] * y.m[k][j] % mod, ans.m[i][j] %= mod;
	return ans;
}
Mtr fpow(Mtr x, long long b) {
	if (b == 0) {
		Mtr ans = Mtr(x.n);
		for (int i = 0; i < ans.n; i++)
			for (int j = 0; j < ans.n; j++)
				ans.m[i][j] = (i == j);
		return ans;
	}
	Mtr t = fpow(x, b / 2);
	return (b % 2 == 1) ? (t * t * x) : (t * t);
}
long long n;
Mtr A;
int main() {
	cin >> n;
	Mtr A = Mtr(2), B = Mtr(2);
	A.m[0][0] = 0, A.m[0][1] = 1;
	A.m[1][0] = 1, A.m[1][1] = 1;
	B.m[0][0] = 1, B.m[0][1] = 0;
	B.m[1][0] = 1, B.m[1][1] = 0;
	if (n <= 2)
		cout << 1 << endl;
	else
		cout << (fpow(A, n - 2) * B).m[1][0] << endl;
	return 0;
} 

1.4.高斯消元

高斯消元其实就是解一个 n 元一次方程组的流程,本质是消元

以下面的方程组为例:

{x1+x2+x3=3x1+2x2+3x3=6x1x2x3=1

很明显,这个方程的解就是 x1=x2=x3=1

高斯消元会这么做:

把方程组看成一个矩阵:

(111312361111)

把矩阵每一行是做一个整体,看做一个向量。我们现在能做的是向量之间的加减,单个向量乘以一个常数。我们的目标是把矩阵变成一个这样的矩阵:

(100x1010x2001x3)

即单位矩阵。

首先我们要把第一行的第一个数变成 1,由于已经是 1 了,所以不用动。

然后我们用第一行把第二三行的第一个数都变成 0:

(11131121316311111113)=(111301230224)

类似的,我们把第二行第二个数变成 1,再消去其他行的第二个数:

(101112330123002+22+44+6)=(101001230022)

第三行也是同理:

(101001230022)=(101001230011)

(10001+10+1001022320012)=(100101010011)

然后就完事儿了。

代码实现起来也非常简单,我们可以用 vector 来存向量,重载运算符。

点击查看代码
#include <iostream>
#include <cstdio>
#include <cmath> 
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
int n;
vector<double> a[105];

vector<double> operator+(vector<double> x, vector<double> y) {
	vector<double> ans((int)x.size());
	for (int i = 0; i < (int)ans.size(); i++)
		ans[i] = x[i] + y[i];
	return ans;
}
vector<double> operator-(vector<double> x, vector<double> y) {
	vector<double> ans((int)x.size());
	for (int i = 0; i < (int)ans.size(); i++)
		ans[i] = x[i] - y[i];
	return ans;
}
vector<double> operator*(vector<double> x, double y) {
	vector<double> ans((int)x.size());
	for (int i = 0; i < (int)ans.size(); i++)
		ans[i] = x[i] * y;
	return ans; 
}
bool Gauss() {
	for (int i = 0; i < n; i++) {
		if (abs(a[i][i]) < 1e-9)
			return false;
		a[i] = a[i] * (1.0 / a[i][i]);
		for (int j = 0; j < n; j++)	
			if (i != j)
				a[j] = a[j] - a[i] * a[j][i];
	}
	return true;
}

int main() {
	cin >> n;
	for (int i = 0; i < n; i++) {
		a[i] = vector<double>(n + 1, 0);
		for (int j = 0; j <= n; j++)
			cin >> a[i][j];
	}
	if (!Gauss())
		cout << "No Solution" << endl;
	else {
		for (int i = 0; i < n; i++)
			printf("%.2lf\n", a[i][n]);
	}
	return 0;
}

1.5 线性代数的本质

以下讨论的都是实数域下的。

1.5.1 向量和矩阵

向量其实就是一条从原点出发的箭头,而矩阵描述的是一个坐标变化,矩阵乘法本质上是坐标变化的复合。

1.5.2 线性相关与线性无关

对于一组向量 (v1,v2,,vn) 如果存在不全为 0 的实数 (a1,a2,,an) 满足 i=1naivi 为零向量,则称这一组向量线性相关,否则无关。

1.5.3 生成空间

对于向量集合 {v1,v2,,vn},其生成空间是 {i=1naivi|aiR}

1.5.4 基和维度

如果一个向量集合线性无关且生成空间包括所有向量,则这组向量称为这个空间的一个基,其大小等于这个空间的维度。

1.6 行列式

1.6.1 定义

对于一个 n×n 矩阵 A,定义其行列式为:

det(A)=σ:π(n)A1,π(1)A2,π(2)An,π(n)sgn(π)

其中 sgn(π) 定义为当 π 是奇排列时为 1,偶排列为 1,奇偶排列等于排列中逆序对的个数的奇偶性。

1.6.2 本质

本质上来说就是 A 作用与 n 维空间后所有单位向量组成的 n 维意义下的体积。

有一个推论:对于一个二维平面上 (0,0),(a,b),(c,d) 构成三角形面积等于 adbc,如果三点不共线。

1.6.3 运算规则

直接根据定义计算复杂度很高,但是我们可以通过以下规则在 O(n3) 解决这个问题。

下面都是对列的讨论,对行同样成立。

规则 1: An 个列向量如果有若干个向量线性相关,则行列式为 0

感性理解,行列式是体积,如果凑不出 n 维空间就只有面积甚至长度,显然无论如何 n 维意义下的体积都是 0

规则 2: det(a,b+c)=det(a,b)+det(a,c)

这个可以通过观察定义式得到。

规则 3: 将第 i 列乘以 a 后加到第 j 列,行列式不变。

可以通过规则 1 和规则 2 推出。

规则 4: 交换两列,行列式取反。

规则 5: 某列增大 a 倍,则行列式也增大 a 倍。

规则 6: det(A)=det(AT)AT 是转置矩阵,也就是沿着对角线翻折得到。

规则 7(Binet-Cauchy定理): 如果 ABn×m 的矩阵且 nm,则 det(AB)=S[m],|S|=ndet(AS)det(BS),其中 AS 表示 A 中只保留 S 中的列,BS 表示只保留 S 中的行。

显然有了这些规则就可以用类似高斯消元来做了。

对于任意模数,我们只能够通过辗转相除来代替求逆元的方式计算。

这里给一下 模板题 的代码:

点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 605;
int mod;

int n;

int a[N][N] = {{0}};

int DET() {
	int ans = 1;
	for (int i = 1; i <= n; i++) {
		for (int j = i + 1; j <= n; j++) {
			while (a[i][i]) {
				int x = a[j][i] / a[i][i];
				for (int k = i; k <= n; k++)
					a[j][k] = (a[j][k] - 1ll * x * a[i][k] % mod + mod) % mod;
				swap(a[i], a[j]), ans = (mod - ans) % mod;
			}
			swap(a[i], a[j]), ans = (mod - ans) % mod; 
		}
		ans = 1ll * ans * a[i][i] % mod;
	}
	return ans;
}

int main() {
	cin >> n >> mod;
	for (int i = 1; i <= n; i++) 
		for (int j = 1; j <= n; j++)
			cin >> a[i][j];
	cout << DET() << endl;
	return 0;
} 

2 矩阵树定理(Kirchhoff's matrix tree theorem)

主要与生成树计数相关。

2.1 定理

2.1.1 定理描述

对于一张无向图 G=(V,E),定义矩阵 Q 如下:

对于对角线上的每个值 Qi,i=degi(G)

对于 Qi,j,如果 (i,j) 没有边,则为 0;否则为 (i,j) 这条边的数量的相反数。

删去 Q 的第一行第一列得到 M1,1,则 G 中生成树个数等于 det(M1,1)

2.1.2 证明

证明很重要!

首先,我们构造一个 n×m 的矩阵 E,其中每一列代表一条边,假设这条边链接 (u,v),则我们将这一列第 u 个设为 1,第 v 个设为 1,其实谁设 1 谁设 1 无所谓,为了方便我们假定 u<v。对于这一列其余元素全部为 0

于是我们先证明第一步: Q=E×ET

对于 Qi,i,其值等于 E 的第 i 行与自己的乘积,于是有多少个 ±1 就有多少条边,等于其度数。

对于 Qi,j 它等于 E 的第 i 行与第 j 行的乘积,如果存在 (i,j) 就会贡献 1,于是也和 Q 相等。

接下来,不妨设 F 表示 E 去掉第一行的矩阵,我们可以推出 M1,1=F×FT

根据 Binet-Cauchy 定理和行列式的性质我们得出:

det(M1,1)=det(F×FT)=Sdet(FS)det(FST)=Sdet(FS)2

而选取 S 可以理解为在 m 条边中选 n1 条边,我们要证明当且仅当 S 是生成树 det(FS)=±1,否则为 0

如果不是生成树,说明有环,把环的那几列都选出来,然后就会发现全都集中到某一列后这一列会变成 0,于是行列式就是 0

如果是生成树,一定可以通过交换变成上三角矩阵,然后行列式就是对角线的乘积了,必须是 ±1

于是就证明了这个定理。

2.1.3 带权版本

如果边带权,要求出所有生成树权值的积的总和,我们依然可以用这个定理。

在证明过程中,我们把 E 中设为 11 改为设成 we0.5we0.5,这样我们依然可以求出 F,然后求出 M1,1 最后求出行列式即可。证明与上面类似。

具体,Qi,i 变成所有邻边的权值和,Qi,j 也是负的边权和。

2.1.4 内向树与外向树

这个定理也可以用来求无向图中内向生成树与外向生成树的个数。

外向生成树:对于转置矩阵,只用入点为 1,最后的 Q 中对角线上只统计入度,Qi,j 只统计 ij 的边。

内向生成树:只用出点,统计出度。

同样都可以推广到带权版本。

这里给一下 P6178 【模板】Matrix-Tree 定理,这个是外向树和无向图的情况:

点击查看代码
#include <iostream>
#include <vector>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 305;
const int mod = 1e9 + 7;

int n, m, T, a[N][N] = {{0}};

int DET() {
	int ans = 1;
	for (int i = 1; i <= n; i++) {
		for (int j = i + 1; j <= n; j++) {
			while (a[i][i]) {
				int x = a[j][i] / a[i][i];
				for (int k = i; k <= n; k++)
					a[j][k] = (a[j][k] - 1ll * x * a[i][k] % mod + mod) % mod;
				swap(a[i], a[j]), ans = (mod - ans) % mod;
			}
			swap(a[i], a[j]), ans = (mod - ans) % mod; 
		}
		ans = 1ll * ans * a[i][i] % mod;
	}
	return ans;
}


int main() {
	cin >> n >> m >> T;
	for (int i = 1, u, v, w; i <= m; i++) {
		cin >> u >> v >> w;
		u = n - u + 1, v = n - v + 1;
		if (T == 0) {
			a[u][u] = (a[u][u] + w) % mod;
			a[v][v] = (a[v][v] + w) % mod;
			a[u][v] = (a[u][v] - w + mod) % mod;
			a[v][u] = (a[v][u] - w + mod) % mod; 
		}
		else {
			a[v][v] = (a[v][v] + w) % mod;
			a[u][v] = (a[u][v] - w + mod) % mod;
		}
	}
	--n;
	cout << DET() << endl;
	return 0;
} 

对于内向图,模板是 P4455 [CQOI2018] 社交网络

切记矩阵树定理需要删掉一行再计算行列式!!!!!!!!!!!!!!!!!!

2.2 例题

P3317 [SDOI2014] 重建

带权版本,我们边权换成 pi1pi,然后最后答案再乘上 (1pi) 的积即可。

CF917D Stranger Trees

恰有 k 条黑白生成树计数。

我们考虑借助多项式来做。我们依然是将 E 中每条边的两个值改一下,如果是黑边就改成 ±x0.5,否则就是 ±1,显然最后 det(M1,1) 是一个 n1 次多项式,我们需要知道 xk 的系数。如果直接 FFT 等科技会比较慢,所以考虑插值,只用将 n 个值带进去算一下,由于需要求系数,可以用高斯消元来求解。

考虑到存在 n1 条黑白的生成树,这意味这这个多项式恰好是 n1 次,所以高斯消元不会存在自由元。

时间复杂度 O(n4)

牛客网 计数

要求选出边权和是 k 的倍数,求有多少种选法,模质数 P,满足 P1(modk)

还是需要借助多项式,将两个值变成 ±xwe2,最后看所有 k 的倍数的系数之和。

但是这样时间复杂度会比较高,因为多项式次数比较高,于是我们考虑如何利用 P 的条件。

我们发现,如果答案是 f(x)=i=0naixi,那么我们构造 F(x) 使得 F(x)=i=0k1i+jknai+jkxi,对余数分类,这样我们就可以只求常数项。

我们为了插值,找到干个满足 xk1(modp)x 来算,具体可以找原根,这样 F(x)=f(x),然后我们就只用求 k 次就可以找到足够我们算出常数项的了。

P4336 [SHOI2016] 黑暗前的幻想乡

子集反演,设 g(S) 表示不用 S 中的,其它随便,f(S) 表示不用 S 中的,其它必须用。这样就可以得到 g(S)=TSf(T),反演一下即可求出 f(S),而我们需要知道 f(),就等于 S(1)|S|g(S)

而计算 g 只用把 S 中的边去掉,剩下的边的生成树计数即可。时间复杂度 O(n32n)

CF578F Mirror Box

神题。

考虑将网格图的格点视为点然后黑白染色。发现条件成立的等价条件是黑点或白点形成一棵生成树,另一颜色的点连上所有可以连的边。

然后就可以缩点然后分别求黑点和白点的生成树个数即可。

posted @   rlc202204  阅读(81)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示