View on GitHub

Wenchong Huang

旧博客,文章搬迁完后删除

返回首页 返回专题

Matrix-Tree定理 学习笔记

提出问题

给定一张n个点m条边的无向连通图,求它的生成树个数。

简单想法

那还不简单?深搜!反正我只要从任意一个点出发,不重不漏地把每个点搜一遍,就可以得到一颗生成树。然后我把生成树的数量加起来不就行了?

额,等等……

这玩意儿怎么写深搜,我好像不会耶……

搜一条链很简单,但搜出一棵树……哎呀妈呀怎么写啊!

就算搜出来了,怎么判重啊……

这令我想起了化学老师手动暴搜同分异构的情境,并且信誓旦旦地告诉我们,同分异构只能一个一个罗列。嗯,OIer当然要表示不服

好吧扯远了,我们回到正题

算了不写了,有兴趣的同学自己写写看吧

矩阵树定理

度数矩阵D

我们定义度数矩阵D的大小为n*n

度数矩阵里面的元素是这样的:

对于D[i][j],其中i≠j,有D[i][j]=0

对于D[i][j],其中i=j,有D[i][j]=图上i号点的度数(也就是与之相连的边的数量)

邻接矩阵A

我们定义领接矩阵A的大小为n*n

邻接矩阵里面的元素是这样的:

若i与j有一条边相连,则A[i][j]=A[j][i]=1

对于A[i][j],其中i=j,有A[i][j]=0

Kichhoff矩阵C

我们定义C=D-A,减法定义为两矩阵中位置相同的元素相减

最终答案

删去拉C的任意一行以及任意一列,得到矩阵Ans

Ans的行列式的绝对值就是答案

问题转移

现在问题变成了怎样快速求一个行列式的值

翻看数学奥赛的一些矩阵相关书籍,发现上面介绍的行列式求值法都是所谓的“代数余子式”

这种方法是把n阶行列式拆成n个n-1阶行列式,然后再对每个拆出来的行列式进行同样的操作,直到拆成1阶,不难发现这种方法的时间复杂度是阶乘级别的,也就是O(n!)

我们不妨从一个简单的问题着手,思考如何优化行列式求值算法

试着写出一个“上三角行列式”,也就是左上-右下对角线的左下方元素均为0的行列式,用“代数余子式”法进行求值

发现了什么?答案等于左上-右下对角线上的元素之积!

其实很容易验证这个发现,只要每次都用最下面那行的元素来进行“代数余子式”中的降阶操作,就会发现一切

问题再转移

现在问题变成了怎样把一个普通行列式转化为“上三角行列式”

这其实就是高斯消元求线性方程组解的第一步——消去

那么问题就简单了,执行高斯消元第一步,然后将对角线元素相乘,就是答案

高斯消元法嘛,emm,有空再说

double G[105][105];
const double eps=1e-9;
int dcmp(double x)
{
	if(fabs(x)<=eps) return 0;
	else if(x>0) return 1;
	else return -1;
}

double Gauss(int n)
{
	n--;
	for(int i=1;i<=n;i++)
	{
		int pos=i;
		for(int j=i+1;j<=n;j++)
			if(dcmp(G[j][i]-G[pos][i])>0) pos=j;
		if(dcmp(G[pos][i])==0) return 0;
		if(pos!=i)
			for(int j=1;j<=n+1;j++) swap(G[i][j],G[pos][j]);
		for(int j=i+1;j<=n;j++)
		{
			if(dcmp(G[j][i])==0) continue;
			double t=G[j][i]/G[i][i];
			for(int k=1;k<=n+1;k++)
				G[j][k]-=t*G[i][k];
		}
	}
	double ans=1;
	for(int i=1;i<=n;i++) ans*=G[i][i];
	return fabs(ans);
}

int main()
{
	int T;
	for(cin>>T;T;T--)
	{
		int n,m,a,b;
		cin>>n>>m;
		memset(G,0,sizeof(G));
		for(int i=1;i<=m;i++)
		{
			scanf("%d%d",&a,&b);
			G[a][a]++;
			G[b][b]++;
			G[a][b]--;
			G[b][a]--;
		}
		printf("%.0lf\n",Gauss(n));
	}
	return 0;
}