矩阵树定理
矩阵树定理
用于解决一张图的生成树计数问题。(默认图可以存在重边,但不能存在自环)
前置芝士:行列式
\(N \times N\) 的矩阵的行列式,可以理解为 \(n\) 个列向量所夹几何体的有向体积。
例如,2 阶行列式即为两个列向量的叉积。
行列式有公式
其中 \(S_n\) 是指长度为 \(n\) 的全排列的集合,\(\sigma\) 就是一个全排列,如果 \(\sigma\) 的逆序对对数为偶数,则 \(\operatorname{sgn}(\sigma)=1\),否则\(\operatorname{sgn}(\sigma)=-1\)。
行列式有如下性质:
- 两行交换,行列式变为相反数。
- 一行 加上 另一行乘某个常数的积,行列式不变。
- 三角矩阵的行列式等于对角线之积。
因为行列式符合上述性质,所以可以直接通过高斯消元计算。
当需要模上一个数时,若模数为质数,算乘积时则可直接使用逆元,否则可以使用辗转相除法。
无向图
设度数矩阵 \(D\),\(D_{i,i}=\operatorname{deg}(i),\ D_{i,j}=0, i \neq j\)。
设邻接矩阵 \(A\),\(A_{i,j}=1\),\(i, j\) 之间有连边。
设拉普拉斯矩阵 \(L=D-A\),所以,
- \(L_{i,i}\) 为 \(i\) 点度数。
- \(L_{i,j}\) 为 \(i, j\) 间的边数的相反数。
图 \(G\) 的生成树个数即为拉普拉斯矩阵去掉任意第 \(k\) 行,第 \(k\) 列(行、列序号相等)后的行列式。
有向图
有向图分为内向树和外向树。
设入度矩阵 \(D^{in}\),\(D_{i,i}=\operatorname{deg^{in}}(i),\ D_{i,j}=0, i \neq j\)。
类似定义出度矩阵\(D^{out}\)。
设邻接矩阵 \(A\),\(A_{i,j}=1\),\(i\) 到 \(j\) 之间有连边。
设拉普拉斯矩阵 \(L^{in}=D^{in}-A\)。
类似定义 \(L^{out}\)。
设 \(t^{root}{G, r}\) 表示图 \(G\) 中,以 \(r\) 为根,所有边指向父亲的生成树。
设 \(t^{leaf}{G, r}\) 表示图 \(G\) 中,以 \(r\) 为根,所有边指向儿子的生成树。
\(t^{root}{G,r}\) 的值为 \(L^{out}\) 去掉第 \(r\) 行,第 \(r\) 列后的行列式。因为根更好向外生长,所以根向生成树对应出度矩阵(雾
\(t^{leaf}{G,r}\) 的值为 \(L^{in}\) 去掉第 \(r\) 行,第 \(r\) 列后的行列式。
拓展
当原图有边权时:
- \(L_{i,i}\) 的值为与 \(i\) 点有关的边的权值和。如,入度矩阵就统计入边的权值和。
- \(L_{i,j}\) 的值为 \(i\) 到 \(j\) 的边权和。
此时求出来的值为
\(S\) 为图 \(G\) 的生成树集合。
P4111 [HEOI2015]小 Z 的房间
模板题,求无向图的生成树个数,需要用辗转相除法。
#include<cstdio>
#include<iostream>
using namespace std;
const int MOD = 1e9;
int n, m, cnt, mp[15][15];
int a[105][105];
char s[15];
int main() {
scanf("%d%d",&n, &m);
for(int i = 1; i <= n; i++) {
scanf("%s",s + 1);
for(int j = 1; j <= m; j++)
if(s[j] == '.') mp[i][j] = ++cnt;
}
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
if(mp[i][j]) {
if(mp[i - 1][j]) ++a[mp[i][j]][mp[i][j]], --a[mp[i][j]][mp[i - 1][j]];
if(mp[i + 1][j]) ++a[mp[i][j]][mp[i][j]], --a[mp[i][j]][mp[i + 1][j]];
if(mp[i][j - 1]) ++a[mp[i][j]][mp[i][j]], --a[mp[i][j]][mp[i][j - 1]];
if(mp[i][j + 1]) ++a[mp[i][j]][mp[i][j]], --a[mp[i][j]][mp[i][j + 1]];
}
--cnt; //去除最后一行和列
int res = 1;
for(int i = 1; i <= cnt; i++)
for(int j = i + 1; j <= cnt; j++)
while(a[j][i]) {
//辗转相除
int r = a[i][i] / a[j][i];
for(int k = i; k <= cnt; k++) a[i][k] = (a[i][k] - 1ll * r * a[j][k] % MOD + MOD) % MOD;
swap(a[i], a[j]); res *= -1;
}
for(int i = 1; i <= cnt; i++) res = 1ll * res * a[i][i] % MOD;
printf("%d\n",(res + MOD) % MOD);
return 0;
}
P6178 【模板】Matrix-Tree 定理
模板题,带边权,求无向图与以 1 为根的外向树。
#include<cstdio>
#include<iostream>
#define LL long long
using namespace std;
inline int read() {
int res = 0, f = 1; char ch;
while((ch = getchar()) && (ch < '0' || ch > '9') && ch != '-');
(ch == '-') ? f = -1 : res = ch - '0';
while((ch = getchar()) && ch >= '0' && ch <= '9') res = (res << 3) + (res << 1) + ch - '0';
return res * f;
}
const int MAXN = 305;
const LL MOD = 1e9 + 7;
int n, m, t;
LL a[MAXN][MAXN];
inline LL expow(LL x, LL y) {
LL res = 1;
while(y) {
if(y & 1) res = res * x % MOD;
y >>= 1, x = x * x % MOD;
}
return res;
}
void solve1() {
for(int i = 1, x, y, z; i <= m; i++) {
x = read(); y = read(); z = read();
a[x][x] = (a[x][x] + z) % MOD;
a[y][y] = (a[y][y] + z) % MOD;
a[x][y] = (a[x][y] - z + MOD) % MOD;
a[y][x] = (a[y][x] - z + MOD) % MOD;
}
--n;
LL res = 1;
for(int i = 1; i <= n; i++) {
for(int j = i; j <= n; j++)
if(a[j][i]) {
if(i != j) swap(a[i], a[j]), res *= -1;
break;
}
LL inv = expow(a[i][i], MOD - 2);
for(int j = i + 1; j <= n; j++) {
LL r = a[j][i] * inv % MOD;
for(int k = i; k <= n; k++)
a[j][k] = (a[j][k] - a[i][k] * r % MOD + MOD) % MOD;
}
}
for(int i = 1; i <= n; i++) res = res * a[i][i] % MOD;
printf("%lld\n",(res + MOD) % MOD);
}
void solve2() {
for(int i = 1, x, y, z; i <= m; i++) {
x = read(); y = read(); z = read();
a[y][y] = (a[y][y] + z) % MOD;
a[x][y] = (a[x][y] - z + MOD) % MOD;
}
LL res = 1;
for(int i = 2; i <= n; i++) {
for(int j = i; j <= n; j++)
if(a[j][i]) {
if(i != j) swap(a[i], a[j]), res *= -1;
break;
}
LL inv = expow(a[i][i], MOD - 2);
for(int j = i + 1; j <= n; j++) {
LL r = a[j][i] * inv % MOD;
for(int k = i; k <= n; k++)
a[j][k] = (a[j][k] - a[i][k] * r % MOD + MOD) % MOD;
}
}
for(int i = 2; i <= n; i++) res = res * a[i][i] % MOD;
printf("%lld\n",(res + MOD) % MOD);
}
int main() {
n = read(); m = read(); t = read();
if(t == 0) solve1();
else if(t == 1) solve2();
return 0;
}
P4336 [SHOI2016]黑暗前的幻想乡
题目大意
有 \(n\) 个城市和 \(m\) 条双向道路,现在需要维修其中恰好 \(n-1\) 条,使任意两个城市能通过维修的道路互相到达。有 \(n-1\) 个工人,每个工人可以修他愿意修的若干条道路。要求每个工人恰好修一条路,求方案数。
Solution
考虑容斥,枚举 \(k\) 个工人参加,加入这 \(k\) 个工人愿意修的边。最后的答案乘上系数 \((-1)^{n-k-1}\)。
P3317 [SDOI2014]重建
题目大意
有 \(n\) 个城市和 \(m\) 条双向道路,有 \(p_i\) 的概率第 \(i\) 条边损害。求最后剩下恰好 \(n-1\) 条边且城市可以两两互相到达的概率。
Solution
考虑带边权的矩阵树定理求出的值为
而本题要求的是
简单化一下式子
P4455 [CQOI2018]社交网络
题目大意
有 \(n\) 个朋友存在单向的认识关系。每当一个人发出消息,所有认识他的人都会看到他的消息。一个人可能同时看到多个人的消息,但他只能转发一个人的消息。1 号会发出一条消息,要使每个人都能看到这条消息,转发方案一共有多少种。
Solution
即求有向图以 1 为根的外向树或内向树。(只能统计一种)
Code
#include<cstdio>
#include<iostream>
using namespace std;
inline int read() {
int res = 0, f = 1; char ch;
while((ch = getchar()) && (ch < '0' || ch > '9') && ch != '-');
(ch == '-') ? f = -1 : res = ch - '0';
while((ch = getchar()) && ch >= '0' && ch <= '9') res = (res << 3) + (res << 1) + ch - '0';
return res * f;
}
const int MAXN = 255, MOD = 1e4 + 7;
int n, m, a[MAXN][MAXN];
inline int expow(int x, int y) {
int res = 1;
while(y) {
if(y & 1) res = res * x % MOD;
y >>= 1, x = x * x % MOD;
}
return res;
}
int main() {
n = read(); m = read();
for(int i = 1, x, y; i <= m; i++) {
x = read(); y = read();
//y->x
a[x][x] = (a[x][x] + 1) % MOD;
a[y][x] = (a[y][x] - 1 + MOD) % MOD;
}
int res = 1;
for(int i = 2; i <= n; i++) {
for(int j = i; j <= n; j++)
if(a[i][j]) {
if(i != j) swap(a[i], a[j]), res *= -1;
break;
}
int inv = expow(a[i][i], MOD - 2);
for(int j = i + 1; j <= n; j++) {
int r = a[j][i] * inv % MOD;
for(int k = i; k <= n; k++)
a[j][k] = (a[j][k] - a[i][k] * r % MOD + MOD) % MOD;
}
}
for(int i = 2; i <= n; i++) res = res * a[i][i] % MOD;
printf("%d\n",(res + MOD) % MOD);
return 0;
}