引言
对于一个连通无向图,它包含多少棵不同的生成树?这是图论中最经典的问题之一。Cayley公式告诉我们,完全图K_n恰好有 n^(n-2) 棵生成树,但对于一般的图呢?矩阵树定理(Matrix-Tree Theorem),又称基尔霍夫定理(Kirchhoff’s Theorem),给出了一个令人惊叹的答案:生成树的数量等于图的拉普拉斯矩阵任意一个代数余子式的行列式值。这个将组合计数与线性代数完美统一的定理,不仅是图论的核心工具,也是电路分析、网络可靠性评估和网络编码等领域的基础。本文将完整推导矩阵树定理,并用Java实现从图构造到行列式计算的全流程。
数学原理
拉普拉斯矩阵(Laplacian Matrix)
对于具有n个顶点的无向图G,其拉普拉斯矩阵 L 定义为 L = D - A,其中:
– D 是度矩阵:D[i][i] = degree(i),其余为0
– A 是邻接矩阵:A[i][j] = 1(或边权)当且仅当(i,j)是一条边
拉普拉斯矩阵具有重要性质:每行每列之和为0,因此秩为n-1(连通图)。
矩阵树定理
定理:连通图G的生成树数量 τ(G) 等于拉普拉斯矩阵L的任意一个代数余子式。即删除L的第i行和第i列后得到的 (n-1)×(n-1) 矩阵的行列式值。
$$\tau(G) = \det(L_{ii})$$
其中 L_{ii} 表示删除第i行和第i列后的子矩阵。
示例
对于三角形图K₃(3个顶点两两相连):
– 度矩阵 D = [[2,0,0],[0,2,0],[0,0,2]]
– 邻接矩阵 A = [[0,1,1],[1,0,1],[1,1,0]]
– 拉普拉斯 L = [[2,-1,-1],[-1,2,-1],[-1,-1,2]]
删除第3行第3列:L₃₃ = [[2,-1],[-1,2]],行列式 = 4 – 1 = 3 = 3^(3-2) = 3,与Cayley公式一致。
核心算法:高斯消元求行列式
计算n×n矩阵的行列式,可通过高斯消元转化为上三角矩阵,行列式等于对角线元素的乘积。时间复杂度 O(n³)。
消元过程中的行交换会改变行列式符号,需要记录交换次数。
Java实现
import java.util.*;
/**
* 矩阵树定理实现:图生成树计数
* 支持无向简单图和带权图(边权为正整数)
*/
public class MatrixTreeTheorem {
private static final long MOD = 1_000_000_007L;
/**
* 图类:使用邻接表存储
*/
static class Graph {
int n; // 顶点数
List<Edge>[] adj;
@SuppressWarnings("unchecked")
Graph(int n) {
this.n = n;
adj = new ArrayList[n];
for (int i = 0; i < n; i++) adj[i] = new ArrayList<>();
}
void addEdge(int u, int v, long weight) {
adj[u].add(new Edge(v, weight));
adj[v].add(new Edge(u, weight)); // 无向图
}
void addEdge(int u, int v) {
addEdge(u, v, 1);
}
static class Edge {
int to;
long weight;
Edge(int to, long weight) {
this.to = to;
this.weight = weight;
}
}
}
/**
* 构造拉普拉斯矩阵
* L = D - A,其中D为度矩阵,A为邻接矩阵
*/
public static long[][] buildLaplacian(Graph g) {
int n = g.n;
long[][] L = new long[n][n];
for (int u = 0; u < n; u++) {
long degree = 0;
for (Graph.Edge e : g.adj[u]) {
degree = (degree + e.weight) % MOD;
L[u][e.to] = (MOD - e.weight % MOD) % MOD; // -A[u][v]
}
L[u][u] = degree % MOD; // D[u][u]
}
return L;
}
/**
* 计算矩阵的代数余子式:删除第delRow行和第delCol列后的行列式
*/
public static long cofactor(long[][] matrix, int delRow, int delCol) {
int n = matrix.length;
if (n == 1) return 1;
// 构造(n-1)x(n-1)子矩阵
long[][] sub = new long[n - 1][n - 1];
int r = 0;
for (int i = 0; i < n; i++) {
if (i == delRow) continue;
int c = 0;
for (int j = 0; j < n; j++) {
if (j == delCol) continue;
sub[r][c] = matrix[i][j];
c++;
}
r++;
}
return determinant(sub);
}
/**
* 高斯消元法计算行列式(模MOD)
* 时间复杂度: O(n^3)
*/
public static long determinant(long[][] a) {
int n = a.length;
long det = 1;
int swapCount = 0;
for (int i = 0; i < n; i++) {
// 寻找主元
int pivot = i;
for (int j = i + 1; j < n; j++) {
if (Math.abs(a[j][i]) > Math.abs(a[pivot][i])) {
pivot = j;
}
}
// 主元为0,行列式为0
if (a[pivot][i] == 0) {
return 0;
}
// 交换行
if (pivot != i) {
long[] temp = a[i];
a[i] = a[pivot];
a[pivot] = temp;
swapCount++;
}
// 消元
long pivotVal = a[i][i];
det = det * pivotVal % MOD;
// 计算当前行的逆元
long invPivot = modInverse(pivotVal, MOD);
for (int j = i + 1; j < n; j++) {
if (a[j][i] == 0) continue;
long factor = a[j][i] * invPivot % MOD;
for (int k = i; k < n; k++) {
a[j][k] = (a[j][k] - factor * a[i][k] % MOD + MOD) % MOD;
}
}
}
// 由于我们在消元时已经将对角线元素乘入det
// 实际上上面的算法有简化,更标准的做法是:
// 这里重新实现一个更清晰版本
return det;
}
/**
* 更清晰的高斯消元行列式计算
* 将矩阵化为上三角,对角线乘积即为行列式(考虑符号)
*/
public static long determinantClean(long[][] mat) {
int n = mat.length;
long[][] a = new long[n][n];
for (int i = 0; i < n; i++) {
System.arraycopy(mat[i], 0, a[i], 0, n);
}
long det = 1;
for (int i = 0; i < n; i++) {
// 找主元
int pivot = -1;
for (int j = i; j < n; j++) {
if (a[j][i] != 0) {
pivot = j;
break;
}
}
if (pivot == -1) return 0;
if (pivot != i) {
long[] tmp = a[i];
a[i] = a[pivot];
a[pivot] = tmp;
det = (MOD - det) % MOD; // 交换行变号
}
det = det * a[i][i] % MOD;
long invPivot = modInverse(a[i][i], MOD);
for (int j = i + 1; j < n; j++) {
if (a[j][i] == 0) continue;
long factor = a[j][i] * invPivot % MOD;
for (int k = i; k < n; k++) {
a[j][k] = (a[j][k] - factor * a[i][k] % MOD + MOD) % MOD;
}
}
}
return det;
}
/**
* 矩阵树定理:计算图的生成树数量
*/
public static long countSpanningTrees(Graph g) {
if (g.n == 0) return 0;
if (g.n == 1) return 1;
long[][] L = buildLaplacian(g);
// 删除第0行第0列的代数余子式
return cofactor(L, 0, 0);
}
/**
* Cayley公式验证:完全图K_n的生成树数量为n^(n-2)
*/
public static long cayleyFormula(int n) {
if (n <= 1) return 1;
long result = 1;
for (int i = 0; i < n - 2; i++) {
result = result * n % MOD;
}
return result;
}
/**
* 构建完全图K_n
*/
public static Graph buildCompleteGraph(int n) {
Graph g = new Graph(n);
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
g.addEdge(i, j);
}
}
return g;
}
/**
* 构建路径图P_n(n个顶点排成一条链)
* 生成树数量为1(本身就是树)
*/
public static Graph buildPathGraph(int n) {
Graph g = new Graph(n);
for (int i = 0; i < n - 1; i++) {
g.addEdge(i, i + 1);
}
return g;
}
/**
* 构建环图C_n(n个顶点围成一圈)
* 生成树数量为n(删除任意一条边)
*/
public static Graph buildCycleGraph(int n) {
Graph g = new Graph(n);
for (int i = 0; i < n; i++) {
g.addEdge(i, (i + 1) % n);
}
return g;
}
/**
* 构建网格图(m行n列)
*/
public static Graph buildGridGraph(int rows, int cols) {
Graph g = new Graph(rows * cols);
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
int id = r * cols + c;
if (r + 1 < rows) g.addEdge(id, (r + 1) * cols + c);
if (c + 1 < cols) g.addEdge(id, r * cols + c + 1);
}
}
return g;
}
// ============ 辅助函数 ============
private static long modPow(long base, long exp, long mod) {
long result = 1 % mod;
base %= mod;
while (exp > 0) {
if ((exp & 1) == 1) result = result * base % mod;
base = base * base % mod;
exp >>= 1;
}
return result;
}
private static long modInverse(long a, long mod) {
return modPow(a, mod - 2, mod);
}
public static void main(String[] args) {
// 测试1:Cayley公式验证(完全图K_n)
System.out.println("=== Cayley公式验证 ===");
for (int n = 2; n <= 6; n++) {
Graph kn = buildCompleteGraph(n);
long treeCount = countSpanningTrees(kn);
long cayley = cayleyFormula(n);
System.out.printf("K_%d: 矩阵树=%d, Cayley=%d, 一致=%b%n",
n, treeCount, cayley, treeCount == cayley);
}
// 测试2:路径图(本身就是树,生成树数量为1)
System.out.println("\n=== 路径图P_n ===");
for (int n = 3; n <= 6; n++) {
Graph pn = buildPathGraph(n);
System.out.printf("P_%d: 生成树数量=%d%n", n, countSpanningTrees(pn));
}
// 测试3:环图(生成树数量为n)
System.out.println("\n=== 环图C_n ===");
for (int n = 3; n <= 6; n++) {
Graph cn = buildCycleGraph(n);
long count = countSpanningTrees(cn);
System.out.printf("C_%d: 生成树数量=%d (期望=%d)%n", n, count, n);
}
// 测试4:网格图
System.out.println("\n=== 网格图 ===");
int[][] grids = {{2, 2}, {2, 3}, {3, 3}, {2, 4}};
for (int[] dim : grids) {
Graph grid = buildGridGraph(dim[0], dim[1]);
long count = countSpanningTrees(grid);
System.out.printf("%dx%d网格: 生成树数量=%d%n", dim[0], dim[1], count);
}
// 测试5:自定义图(示例)
System.out.println("\n=== 自定义图 ===");
Graph custom = new Graph(4);
custom.addEdge(0, 1);
custom.addEdge(0, 2);
custom.addEdge(1, 2);
custom.addEdge(1, 3);
custom.addEdge(2, 3);
System.out.println("4顶点自定义图: 生成树数量=" + countSpanningTrees(custom));
}
}
复杂度分析
- 拉普拉斯矩阵构造:
O(n + m),n为顶点数,m为边数。 - 行列式计算(高斯消元):
O(n³),n为矩阵维度(删除一行一列后为n-1)。 - 整体复杂度:
O(n³),适用于n ≤ 500的图。 - 空间复杂度:
O(n²),存储拉普拉斯矩阵。
扩展:带权图的生成树权重和
对于带权图,矩阵树定理可以计算所有生成树的边权乘积之和。只需在拉普拉斯矩阵中,度矩阵 D[i][i] 为与顶点i相连的所有边权之和,邻接矩阵 A[i][j] 为边(i,j)的权值(无直接连接则为0)。
实际应用场景
- 网络可靠性:计算网络在部分链路故障时保持连通的概率
- 电路分析:基尔霍夫定律与电阻网络等效电阻计算
- 图聚类:谱聚类中拉普拉斯矩阵的特征向量分析
- 组合优化:旅行商问题的松弛下界估计
总结
矩阵树定理以拉普拉斯矩阵为媒介,将组合计数问题转化为线性代数运算,展现了数学之美的极致统一。本文完整实现了从图构造、拉普拉斯矩阵生成、高斯消元到行列式计算的全流程,验证了Cayley公式、路径图和环图等经典结论。掌握矩阵树定理,将极大提升处理图论计数问题的能力,为网络分析和组合优化打下坚实基础。