线性代数学习笔记

1. 基础知识

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

1.1.向量

向量的英文叫 vector,就是那个超级好用的 STL 的容器的名字来源。向量可以表示一组元素,如:向量 \(\vec{A}=(a_1,a_2,a_3,...,a_n)\)

其中向量 \(\vec{A}=(a_1,a_2,a_3,...,a_n)\) 这样的向量被称作行向量。

而形如:

\[\begin{pmatrix} a_1\\ a_2\\ \dots\\ a_n\\ \end{pmatrix} \]

则称作列向量

1.1.1.向量数量积

两个 \(n\) 维向量 \(\vec{A}=(a_1,a_2,\ldots,a_n)\)\(\vec{B}=(b_1,b_2,\ldots,b_n)\) 的数量积(也称作点积、内积)为

\[\vec{A}·\vec{B}=a_1b_1+a_2b_2+\ldots+a_nb_n \]

1.2.矩阵

矩阵可以理解为一个 \(n\)\(m\) 列的矩阵。比如:

\[\begin{pmatrix} 1 & 2 & 3\\ 4 & 5 & 6\\ \end{pmatrix} \]

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

1.2.1.矩阵乘法

矩阵乘法只有在前一个矩阵是 \(n\)\(m\) 列,后一个矩阵是 \(m\)\(k\) 列时才有意义。

\(A\) 是一个 \(n\)\(m\) 列的矩阵,\(B\) 是一个 \(m\)\(k\) 列的矩阵,设 \(A \times B=C\),则 \(C\) 是一个 \(n\)\(k\) 列的矩阵。并且对于所有 \(1 \le i \le n,1 \le j \le k\),都有:

\[C_{i,j}=\sum_{l=1}^mA_{i,l} \times B_{l,j} \]

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

1.2.2.单位矩阵

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

\(n=3\) 时:

\[I = \begin{pmatrix} 1 & 0 & 0\\ 0 & 1 & 0\\ 0 & 0 & 1\\ \end{pmatrix} \]

对于任意一个 \(n\)\(n\) 列的方阵 \(A\),都有:

\[A \times I =A \]

\[I \times 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(n^3)\) 的。

1.3.矩阵的应用

1.3.1.矩阵快速幂

我们知道,计算 \(a^n\) 这种幂可以用快速幂来加速,时间复杂度 \(O(\log n)\)

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

如果 \(A\) 是一个 \(m\)\(m\) 列的方阵,则快速幂的时间复杂度是 \(O(m^3 \log n)\)

点击查看代码
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)\) 的:

\[F_i = \begin{cases} 1&i=1,2\\ F_{i-1}+F_{i-2}& \text{Otherwise.} \end{cases} \]

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

\[Ax=X \]

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

\[\begin{pmatrix} A_{1,1}&A_{1,2}&\dots & A_{1,n}\\ A_{2,1}&\dots&\dots & \dots\\ \dots &\dots &\dots & \dots\\ A_{n,1}&\dots &\dots &A_{n,n} \end{pmatrix} \begin{pmatrix} x_1\\ x_2\\ \dots\\ x_n\\ \end{pmatrix} = \begin{pmatrix} X_1\\ X_2\\ \dots\\ X_n\\ \end{pmatrix} \]

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

\[X_i=\sum_{k=1}^n A_{i,k} \times x_k \]

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

\[\begin{pmatrix} A_{1,1}&A_{1,2}\\ A_{2,1}&A_{2,2}\\ \end{pmatrix} \begin{pmatrix} F_{i-2}\\ F_{i-1} \end{pmatrix} = \begin{pmatrix} F_{i-1}\\ F_i \end{pmatrix} \]

所以我们有:

\[\begin{cases} F_{i-1}&=A_{1,1} F_{i-2}+A_{1,2}F_{i-1}\\ F_{i}&=A_{2,1} F_{i-2}+A_{2,2}F_{i-1}\\ \end{cases} \]

再根据 \(F_{i}=F_{i-1}+F_{i-2}\) 就可以得到:

\[A = \begin{pmatrix} 0&1\\ 1&1 \end{pmatrix} \]

所以我们就有:

\[\begin{pmatrix} 0&1\\ 1&1\\ \end{pmatrix} \begin{pmatrix} F_{i-2}\\ F_{i-1} \end{pmatrix} = \begin{pmatrix} F_{i-1}\\ F_i \end{pmatrix} \]

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

即:

\[\begin{pmatrix} 0&1\\ 1&1\\ \end{pmatrix}^{n-2} \begin{pmatrix} 1\\ 1 \end{pmatrix} = \begin{pmatrix} F_{n-1}\\ F_n \end{pmatrix} \]

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

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

这样递推就是 \(O(\log n)\) 的了。

点击查看代码
#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\) 元一次方程组的流程,本质是消元

以下面的方程组为例:

\[\begin{cases} x_1+x_2+x_3=3\\ x_1+2x_2+3x_3=6\\ x_1-x_2-x_3=-1 \end{cases} \]

很明显,这个方程的解就是 \(x_1=x_2=x_3=1\)

高斯消元会这么做:

把方程组看成一个矩阵:

\[\begin{pmatrix} 1&1&1&3\\ 1&2&3&6\\ 1&-1&-1&-1\\ \end{pmatrix} \]

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

\[\begin{pmatrix} 1&0&0&x_1\\ 0&1&0&x_2\\ 0&0&1&x_3 \end{pmatrix} \]

即单位矩阵。

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

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

\[\begin{pmatrix} 1&1&1&3\\ 1-1&2-1&3-1&6-3\\ 1-1&-1-1&-1-1&-1-3 \end{pmatrix} = \begin{pmatrix} 1&1&1&3\\ 0&1&2&3\\ 0&-2&-2&-4 \end{pmatrix} \]

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

\[\begin{pmatrix} 1-0&1-1&1-2&3-3\\ 0&1&2&3\\ 0-0&-2+2&-2+4&-4+6 \end{pmatrix} = \begin{pmatrix} 1&0&-1&0\\ 0&1&2&3\\ 0&0&2&2 \end{pmatrix} \]

第三行也是同理:

\[\begin{pmatrix} 1&0&-1&0\\ 0&1&2&3\\ 0&0&2&2 \end{pmatrix} = \begin{pmatrix} 1&0&-1&0\\ 0&1&2&3\\ 0&0&1&1 \end{pmatrix} \]

\[\begin{pmatrix} 1-0&0-0&-1+1&0+1\\ 0-0&1-0&2-2&3-2\\ 0&0&1&2 \end{pmatrix} = \begin{pmatrix} 1&0&0&1\\ 0&1&0&1\\ 0&0&1&1 \end{pmatrix} \]

然后就完事儿了。

代码实现起来也非常简单,我们可以用 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 线性相关与线性无关

对于一组向量 \((v_1, v_2, \dots, v_n)\) 如果存在不全为 \(0\) 的实数 \((a_1, a_2, \dots, a_n)\) 满足 \(\sum_{i=1}^n{a_iv_i}\) 为零向量,则称这一组向量线性相关,否则无关。

1.5.3 生成空间

对于向量集合 \(\{v_1, v_2, \dots, v_n\}\),其生成空间是 \(\{\sum_{i=1}^na_iv_i|a_i \in \mathbb{R}\}\)

1.5.4 基和维度

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

1.6 行列式

1.6.1 定义

对于一个 \(n \times n\) 矩阵 \(A\),定义其行列式为:

\[\det(A) = \sum_{\sigma: \pi(n)}A_{1,\pi(1)}A_{2,\pi(2)}\dots A_{n,\pi(n)} sgn(\pi) \]

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

1.6.2 本质

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

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

1.6.3 运算规则

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

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

规则 1: \(A\)\(n\) 个列向量如果有若干个向量线性相关,则行列式为 \(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(A^T)\)\(A^T\) 是转置矩阵,也就是沿着对角线翻折得到。

规则 7(Binet-Cauchy定理): 如果 \(AB\)\(n \times m\) 的矩阵且 \(n \le m\),则 \(\det(AB) = \sum_{S \sube [m],|S| = n}det(A_S)det(B_S)\),其中 \(A_S\) 表示 \(A\) 中只保留 \(S\) 中的列,\(B_S\) 表示只保留 \(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\) 如下:

对于对角线上的每个值 \(Q_{i,i} = \deg_i(G)\)

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

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

2.1.2 证明

证明很重要!

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

于是我们先证明第一步: \(Q = E \times E^T\)

对于 \(Q_{i,i}\),其值等于 \(E\) 的第 \(i\) 行与自己的乘积,于是有多少个 \(\pm1\) 就有多少条边,等于其度数。

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

接下来,不妨设 \(F\) 表示 \(E\) 去掉第一行的矩阵,我们可以推出 \(M_{1,1} = F \times F^T\)

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

\[\det(M_{1,1}) = \det(F \times F^T) = \sum_{S}\det(F_S)det(F^T_S) = \sum_{S}\det(F_S)^2 \]

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

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

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

于是就证明了这个定理。

2.1.3 带权版本

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

在证明过程中,我们把 \(E\) 中设为 \(1\)\(-1\) 改为设成 \(w_e^{0.5}\)\(-w_e^{0.5}\),这样我们依然可以求出 \(F\),然后求出 \(M_{1,1}\) 最后求出行列式即可。证明与上面类似。

具体,\(Q_{i,i}\) 变成所有邻边的权值和,\(Q_{i,j}\) 也是负的边权和。

2.1.4 内向树与外向树

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

外向生成树:对于转置矩阵,只用入点为 \(1\),最后的 \(Q\) 中对角线上只统计入度,\(Q_{i,j}\) 只统计 \(i \to j\) 的边。

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

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

这里给一下 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] 重建

带权版本,我们边权换成 \(\frac{p_i}{1 - p_i}\),然后最后答案再乘上 \((1-p_i)\) 的积即可。

CF917D Stranger Trees

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

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

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

时间复杂度 \(O(n^4)\)

牛客网 计数

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

还是需要借助多项式,将两个值变成 \(\pm x^{\frac{w_e}{2}}\),最后看所有 \(k\) 的倍数的系数之和。

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

我们发现,如果答案是 \(f(x) = \sum_{i=0}^na_ix^i\),那么我们构造 \(F(x)\) 使得 \(F(x) = \sum_{i=0}^{k - 1}\sum_{i + jk \le n}a_{i + jk} x^i\),对余数分类,这样我们就可以只求常数项。

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

P4336 [SHOI2016] 黑暗前的幻想乡

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

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

CF578F Mirror Box

神题。

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

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

posted @ 2022-12-04 10:23  rlc202204  阅读(73)  评论(0编辑  收藏  举报