[学习笔记]矩阵树定理

〇、重刊序言

曾经学过一次矩阵树定理,但是介于并不知道它到底有什么用,以及对于基尔霍夫矩阵的理解不够到位,只是记住了 \(基尔霍夫矩阵=邻接矩阵-度数矩阵\) 这样一个结论,所以对于无向图的一些问题我可以直接使用模板,但是在有向图上我就只能对于系数进行乱尝试了,并且这样忘记得很快......时隔半年 可能还没有半年 就忘记了,故重开一篇,对矩阵树定理进行更深入的理解。

矩阵树定理,它的目的其实是求出一张 有向/无向 图的生成树的种类有多少,但是介于网上对于该定理的证明偏少,以及大多数介绍实际上都侧重在介绍行列式相关知识,故而用此篇对于矩阵树定理进行深入探讨,而非侧重于介绍行列式(如果不会行列式或者是忘记了,请复习后再来),以及对于该算法的一个简要证明。

壹、算法内容

要说这个算法内容,实际上是很简单的 不然我也背不到结论,简而言之就是这几句话:

\(D\) 为图的度数矩阵(即只有 \(d_{i,i}\) 是有值,且值为点的度数),\(E\) 为图的邻接矩阵(即如果 \(e_{i,j}=1\),则图上存在 \(u\rightarrow v\) 的一条边),那么,对于基尔霍夫(Kirchhoff)矩阵 \(K\),有

\[K=E-D \]

对于图的生成树个数,即为 \(\det K\).

上述情况成立,当原图是无向图的时候,但是如果图是有向图呢?

做一个较小的变化,设 \(D\) 为图的入度矩阵,\(E\) 为图的邻接矩阵(满足 \(e_{i,j}=1\) 当且仅当 \(j\rightarrow i\) 是一条边),然后继续使用上述定理即可。

算法本身时间复杂度 \(\mathcal O(n^3)\),卡在用类似高斯消元思想求 \(\det K\) 上。

贰、算法证明

其实该定理的本质是容斥,先考虑在一张无向图上,如果我们对于所有的 \(n\) 个点都随便选,选 \(n-1\) 条边,可能会出现环,然后我们再对于环的个数进行容斥,枚举环个数,再钦定几个点形成固定的环,剩下的点仍然随便选,然后我们就有形似下面的容斥:

\[Ans=\sum_{i=0}(-1)^i枚举 i 个环\prod_{j不在环上}d_j \]

但是肉眼可见的,这个容斥并不容易实现,容斥的时候我们还要同时枚举环,并且环还得长得不一样......这完全不可做。

想到行列式的定理,实际上就是我们随便取出一个排列出来,然后求积再算上 \(\pm\) 即可,这个 \(\pm\) 取决于这个排列的逆序对个数,但是这个逆序对个数比较奇怪,和环没有任何关系。但是我们考虑一个排列的置换环,一个排列可能会形成很多的置换环,如果我们要将一个置换环变得有序,显然,如果我们记置换环大小为 \(s_i\),那么需要的步数就是 \(s_i-1\),同理,如果我们要将整个排列变得有序,即变成 \(1234...n\) 的形式,需要的是 \(\sum (s_i-1)=n-置换环个数\),这其实和所谓的 \(逆序对个数\) 在奇偶性上是等价的,因为符号肯定都是固定的嘛,所以,我们可以将行列式最后乘上的系数理解为 \((-1)^{n-这个排列形成的置换环个数}\),但由于一个排列的置换环对应的是我们原图上的一个环(例如我们这一次排列选择了 \(e_{i,j}\) 即表示我们选择了 \(i-j\) 的一条边),所以我们最后实际上乘上的,是 \((-1)^{这个排列形成的置换环个数}\),但是由于 \((-1)^{-这个排列形成的置换环个数}\) 在符号上不影响,所以我们最后差的只是一个 \((-1)^n\),所以我们最后是用 \(E-D\) 作为 \(K\),即对角线上的 \(n\) 歌元素由 \(+\)\(-\),刚好相当于乘上 \((-1)^n\).

但是我们为什么要去掉一行一列呢?因为我们要选择一个点当根让他没有入边(所谓的入边感性理解一下),这也是为什么无向图可以任意去掉一行一列计算杭行列式的原因——每个点都可以成为这个无向图的根。

对于容斥过程,如果我们某一次排列选择的恰好是 \(e_{i,i}\)(对角线元素)该如何理解?即我们让这个点任意选择连向它的边。

基于这个容斥过程,为什么有向图是那样构造 \(K\) 也可以解释了,此处不再赘述。

至于原图如果带权,其实也可以分析出来,此处亦不作过多解释。

叁、模板

[SPOJ-HIGH]highway 为例,行列式的消元,由于为了避免小数,使用了类似辗转相除的原理。

using namespace Elaina;

const int maxn=12;

ll a[maxn+5][maxn+5];

int n, m, T;

inline void input(){
	n=readin(1), m=readin(1);
	for(int i=0; i<=n; ++i)
		for(int j=0; j<=n; ++j)
			a[i][j]=0;
	int u, v;
	for(int i=1; i<=m; ++i){
		u=readin(1), v=readin(1);
		++a[u][v], ++a[v][u];
		--a[u][u], --a[v][v];
	}
}

inline ll det(){
	ll ret=1;
	for(int i=2; i<=n; ++i){
		for(int j=i+1; j<=n; ++j){
			while(a[j][i]){
				ll t=a[i][i]/a[j][i];
				for(int k=i; k<=n; ++k)
					a[i][k]=a[i][k]-a[j][k]*t;
				for(int k=i; k<=n; ++k) swap(a[i][k], a[j][k]);
				ret=-ret;
			}
		}
		if(!a[i][i])return 0;
		ret=ret*a[i][i];
	}
	return ret<0?-ret:ret;
}

signed main(){
	T=readin(1);
	while(T--){
		input();
		printf("%lld\n", det());
	}
	return 0;
}

肆、例题

[UVA10766]Organising the Organisation

[HDU5304]Eastest Magical Day Seep Group's Summer

[HDU4305]Lightning

[LOJ2532][CQOI2018]社交网络

伍、更加深入

深入x1

如果我们将问题变换为:

给一个 \(n\)\(m\) 边的无向图,每条边有一个权值,求这张图所有生成树的边权乘积之和,具体来说,就是求

\[\sum_{T}\text{T is a generating tree}\prod_{e\in T}w_e \]

保证 \(w\le 10^9,n\le 50\).

我们需要对矩阵树定理有更深的了解,矩阵树定理使用行列式完成容斥,每次选择 \(e_{i,j}=1\) 表示选择这条边,但是最终我们是将所有选择的 \(e_{i,j}\) 乘起来,是否可以利用这个原理进行一些改造?

考虑构建以下的矩阵:对角线上是所有点向这个点连接的所有边的边权值之和,其他地方还是一样的邻接矩阵(肯定是边权值),然后将对角线上的值添上一个负号(为了满足容斥原理),这就得到了我们想要的邻接矩阵。

这样的矩阵构造还是相似,如果都是对角线上的,则说明我们都是任意选择边,如果选择的非对角线上的边,则说明我们强制选择了边。

深入x2

在 “深入x1” 的基础上,我们继续变难它:

给一个 \(n\)\(m\) 边的无向图,每条边有一个权值,求这张图所有生成树的边权乘积之和,具体来说,就是求

\[\sum_{T}\text{T is a generating tree}\sum_{e\in T}w_e \]

保证 \(w\le 10^9\)\(n\le 50\).

感觉要使用矩阵树定理的性质就比较困难了,因为我们最终计算的是边乘积而非加和。

但是我们能否进行一些变更,让乘法变成加法?

这里想到了一个东西,如果我们让 \(e_{i,j}=x^{w_e}\),采用类似生成函数的方式,那么我们最后求的就是 \(F'(1)\),具体来说,就是记 \(f_{i,j}=x^{w_{e_{i,j}}}\),对角线上放 \(x^{w_1}+x^{w_2}+...\),最后的行列式就会是一个多项式 \(F(x)\),最后我们要求的实际上是指数,所以就是 \(F'(1)\). 这样的复杂度至少会是 \(\mathcal O(w_e)\),还要算上一些多项式计算的复杂度以及求行列式的复杂度......不错,这和网络流差不多了,\(n^2\) 过百万已经变成了一个垃圾,我直接 \(\mathcal O(10^9)\) 起手了......

上面的做法是可行的,但是这样好像会上天,我们得另外考虑一种方式,我们怎么样才能保证我们构造的俩东西乘起来会出现边权之和,并且会出现类似结构?

考虑这样进行设计,如果存在一条边 \(e=\lang u,v,w\rang\),那么 \(e_{u,v}=wx+1\),考虑两个这样的东东乘起来会是什么?

\[(w_1x+1)(w_2x+1)=w_1w_2x^2+(w_1+w_2)x+1 \]

那么我们最后求的就是 \(x\) 的一次项的系数。

但是如果直接进行维护,我们则要将矩阵中每个元素变成多项式,乘法的时候还需要考虑是否需要 \(\tt FFT\),并且求行列式的时候复杂度还无法进行精确计算(但是肯定超时了)。

同样,对角线上放 \((w_1x+1)+(w_2x+1)+...=(w_1+w_2+w_3+...)x+1+1+1+...\),然后填上负号即可。

只不过我们最后只需要 \(x\) 的一次项,我们完全可以维护 \(x\)\(0,1\) 次项的系数,对于产生的高次项扔掉,然后同样做行列式以及乘法就可以了,时间复杂度预估在 \(\mathcal O(n^3)\) 左右,但是肯定比这更大。

posted @ 2021-02-20 15:23  Arextre  阅读(253)  评论(0编辑  收藏  举报