关于矩阵树定理,其实没有太多要讲的东西,不过你需要先了解行列式。
行列式
定义
首先要了解行列式是一个函数,并不是一个数。它的自变量是一个方阵 ,它的因变量是一个标量(一个数),记作 或 。例如下图:
行列式最早被用来解线性方程组,一个线性方程组有唯一解当且仅当它对应的行列式不为零。
对于一个 矩阵 ,它的行列式的计算方法如下:
其中 是一个 的排列, 为排列 的逆序对数。
即每行挑一个数乘起来,再用得到的排列的逆序对数有关的东西做系数相加,这样计算复杂度为 。
性质
首先了解行列式的几条基本性质。
- 单位矩阵 的行列式是 ;
- ;
- 有某两行一样或成比例的矩阵,行列式为 ;
- 有某一行或某一列是 的矩阵,行列式为 ;
- 行列互换,行列式不变。
接着了解初等行变换与行列式之间的几条关系。
-
交换矩阵两行,行列式变为其相反数。
交换两行后,只会影响到逆序对系数。易证一个排列中交换两个数对逆序对的影响一定是奇数个,因此行列式要变号。
-
某一行乘以 ,行列式也乘 。
根据公式就可以看出来,那一行的所有数都乘以 ,所以连乘时也会变成原来的 倍。每个不同的排列中,连乘都会变成原来的 倍,故行列式值变为原来 倍。
-
矩阵的一行加上另一行的倍数,行列式不变。
有基本性质第二三四条合起来即可证明。
计算
首先是著名的消元求行列式。
知道初等行变换对行列式的影响后,我们由于消元只用了初等行变换,那么我们在高斯消元的时候只用在每次交换两行时把答案变号,最后消成上三角矩阵后,行列式再乘以对角线乘积,如果消元不出来就返回 。时间复杂度为 。
然后是展开求行列式。依然需要了解一些基本定义。
主子式
在 阶行列式中,选取行号,再选取与行号相同的列号,则行数和列数都为 个的行列式即为 阶行列式的 阶主子式。
余子式
在 阶行列式中,划去元 所在的第 行和第 列,剩下的元不改变原顺序所构成的 阶行列式称为元 的余子式,把元 的余子式也计作 。
代数余子式
将某余子式 的值再乘以 得到一个数 ,它即是元 的代数余子式。
一个行列式按行展开,那么行列式 的计算方式为:
一个行列式按列展开,那么行列式 的计算方式为:
主要用这两种方法来求行列式。对于一些特殊的行列式还有着特殊的求法(行列式的解法)。
接下来才是我们的重头戏——矩阵树定理。
我们先来看最最经典的情况,无向无权图求生成树个数。
我们设 为邻接矩阵(若 之间存在边,那么 ,其他为 ), 为度数矩阵( 为 的度数,其他的为 ),那么它的基尔霍夫() 矩阵 。设 为 的任意一个 阶主子式,那么 就是该图的生成树个数。
然后我们扩展一下,把它变成无向带权图。
求生成树的个数就把边权当成 来处理即可。但如果我们把 的定义改一下,把 变成与 点相连的边的边权之和,同理 也变成 之间的所有边的边权之和。这样得出来的 就是所有生成树边权乘积的总和。
继续扩展至有向图。
这个时候要分两种情况考虑,我们把生成树分成外向树和内向树,顾名思义,外向树就是树上的每条边都是从根向外的,内向树书上每条边都是由外向根的。因此外向树也被称作叶向树,内向树也被称作根向树。
这时候我们把 只算 的有向边即可。对于度数矩阵则要分情况。如果要求外向树,那么 就是从外面到该点的边权总和,即入度边权总和;内向树则是从该点到外面的边权总和,即出度边权总和。同时既然是有向,那么根就是确定的。 必须要是 去掉根那一行那一列的主子式。
例 1
模板题。
/*
* @Author: clorf
* @Date: 2020-09-14 08:50:18
* @Last Modified by: clorf
* @Last Modified time: 2020-09-14 09:16:22
*/
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#include<ctime>
#define INF 1e9
using namespace std;
const int maxn=310;
const int mod=1e9+7;
const double Pi=acos(-1.0);
const double eps=1e-7;
//1.数组开小
//2.没用long long/爆了类型存储范围
//3.写之前式子没推清楚
//4.变量写错(想当然typo/没想清楚含义)
//5.写之前没想清楚复杂度
template<class T>void read(T &x)
{
x=0;int f=0;char ch=getchar();
while(ch<'0'||ch>'9') {f|=(ch=='-');ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
x=f?-x:x;
return;
}
int n,m,t;
long long a[maxn][maxn],ans=1,flag=1;
long long power(long long a,long long b,long long p)
{
long long ans=1%p;
for(;b;b>>=1)
{
if(b&1)
ans=ans*a%p;
a=a*a%p;
}
return ans%p;
}
long long inv(long long x)
{
return power(x,mod-2,mod);
}
inline void gauss()
{
int i=2,j=2;
while(i<=n&&j<=n)
{
int maxx=i;
for(int k=i+1;k<=n;k++)
if(abs(a[k][j])>abs(a[maxx][j]))
maxx=k;
if(abs(a[maxx][j]))
{
if(maxx!=i)
flag*=-1;
swap(a[maxx],a[i]);
for(int k=i+1;k<=n;k++)
{
long long num=(a[k][j]*inv(a[i][j]))%mod;
for(int l=j;l<=n;l++)
a[k][l]=(a[k][l]+(mod-num)*a[i][l])%mod;
}
i++;
}
j++;
}
}
int main()
{
scanf("%d%d%d",&n,&m,&t);
for(int i=1;i<=m;i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
if(!t)
{
(a[x][x]+=z)%=mod;
(a[y][y]+=z)%=mod;
(a[x][y]-=z)%=mod;
(a[y][x]-=z)%=mod;
}
else
{
(a[y][y]+=z)%=mod;
(a[x][y]-=z)%=mod;
}
}
gauss();
for(int i=2;i<=n;i++)
ans=(ans*a[i][i]%mod)%mod;
ans=(ans*flag+mod)%mod;
printf("%lld",ans);
return 0;
}
例 2
另一道模板题。
解析:为了保证每条边只枚举到 次,加边时只枚举某个点上方和左方的点,然后就是跑生成树个数。注意模数是 ,不是质数,不能直接求逆元。这里我们采用类似欧几里得算法,辗转相除的方式,用这行把另一行消后(消元就相当于欧几里得中的取模)就交换两行,直到某一行为 。
/*
* @Author: clorf
* @Date: 2020-09-14 21:58:02
* @Last Modified by: clorf
* @Last Modified time: 2020-09-14 22:13:31
*/
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#include<ctime>
#define INF 1e9
using namespace std;
const int maxn=15;
const int mod=1e9;
const double Pi=acos(-1.0);
const double eps=1e-7;
//1.数组开小
//2.没用long long/爆了类型存储范围
//3.写之前式子没推清楚
//4.变量写错(想当然typo/没想清楚含义)
//5.写之前没想清楚复杂度
template<class T>void read(T &x)
{
x=0;int f=0;char ch=getchar();
while(ch<'0'||ch>'9') {f|=(ch=='-');ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
x=f?-x:x;
return;
}
int n,m,cnt,num[maxn][maxn];
long long ans=1,a[maxn*maxn][maxn*maxn];
char s[maxn][maxn];
inline void add(int u,int v)
{
a[u][u]++;
a[v][v]++;
a[u][v]--;
a[v][u]--;
}
inline void gauss(int m,int n)
{
int i=2,j=2;
while(i<=m&&j<=n)
{
int maxx=i;
for(int k=i+1;k<=m;k++)
if(abs(a[k][j])>abs(a[maxx][j]))
maxx=k;
if(abs(a[maxx][j]))
{
if(maxx!=i)
ans=-ans;
swap(a[maxx],a[i]);
for(int k=i+1;k<=m;k++)
{
while(a[k][j])
{
int num=a[i][j]/a[k][j];
for(int l=j;l<=n;l++)
a[i][l]=(a[i][l]+(mod-num)*a[k][l])%mod;
swap(a[k],a[i]);
ans=-ans;
}//辗转相除消元
}
i++;
}
j++;
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%s",s[i]+1);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
if(s[i][j]=='.')
num[i][j]=++cnt;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
if(num[i][j])
{
if(num[i-1][j])
add(num[i][j],num[i-1][j]);
if(num[i][j-1])
add(num[i][j],num[i][j-1]);
}
gauss(cnt,cnt);
for(int i=2;i<=cnt;i++)
ans=(ans*a[i][i])%mod;
ans=(ans+mod)%mod;
printf("%lld",ans);
return 0;
}
例 3
解析:我们看到 , 阶的行列式肯定要用高精度,加上高斯消元的复杂度后约为 ,需要卡一些常数才能卡过。有没有其他优秀的方法呢(比如能跑过 的那种)?我们先把 矩阵画出来:
删掉第 行第 列得到 :
然后我们把 按第一列展开:
我们把这 个行列式记作 。接下来我们发现 是个具有特殊性质的行列式——三对角行列式,即只有主对角线和主对角线左右的两条对角线,每条对角线上的值都相同,其他地方为 。这种行列式可以通过递推来求解,我们现在开始推 的递推式。
把 按第 行展开:
然后又得到了一个 的主子式,继续按第一列展开它:
如此,我们就得到了 的递推式:
用同样的方法,我们可以得到 与 的关系,不过要注意系数 不要漏掉。
带回去得到:
结合 的递推式,自然而然猜测 也是二阶线性递推,待定系数可得:
于是直接递推即可,注意要写高精度。
/*
* @Author: clorf
* @Date: 2020-09-14 09:17:52
* @Last Modified by: clorf
* @Last Modified time: 2020-09-14 21:54:01
*/
#include<cstdio>
#include<cstring>
#include<iostream>
#include<stdexcept>
#include<cmath>
#define INF 1e9
using namespace std;
const int maxn=100010;
const int M=10000,P=4;
const int mod=1e9+7;
const double Pi=acos(-1.0);
const double eps=1e-7;
//1.数组开小
//2.没用long long/爆了类型存储范围
//3.写之前式子没推清楚
//4.变量写错(想当然typo/没想清楚含义)
//5.写之前没想清楚复杂度
template<class T>void read(T &x)
{
x=0;int f=0;char ch=getchar();
while(ch<'0'||ch>'9') {f|=(ch=='-');ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
x=f?-x:x;
return;
}
int n;
struct bign
{
int x[20001],len;
bign()
{
len=1;
memset(x,0,sizeof(x));
}
bign(int x)
{
*this=x;
}
bign operator =(int n)
{
char s[20001];
sprintf(s,"%d",n);
*this=s;
}
bign operator =(char *s)
{
bign a;
a.len=strlen(s);
for(int i=0;i<a.len;i++)
a.x[a.len-i]=s[i]-'0';
*this=a;
}
friend bool operator <(bign a,bign b)
{
if(a.len<b.len)
return true;
if(a.len==b.len)
{
int i=a.len;
while(a.x[i]==b.x[i]&&i>=1)
i--;
if(a.x[i]<b.x[i])
return true;
}
return false;
}
friend bool operator >(bign a,bign b)
{
return b<a;
}
friend bool operator <=(bign a,bign b)
{
return !(a>b);
}
friend bool operator >=(bign a,bign b)
{
return !(a<b);
}
friend bool operator ==(bign a,bign b)
{
return (a>=b)&&(b>=a);
}
friend bool operator !=(bign a,bign b)
{
return (a<b)||(b<a);
}
friend bign operator +(bign a,bign b)
{
int i=1,jw=0;
bign c,aa=a,bb=b;
while((i<=aa.len+1)||(i<=bb.len+1))
{
c.x[i]=aa.x[i]+bb.x[i]+jw;
jw=c.x[i]/10;
c.x[i]%=10;
i++;
}
c.len=i;
while(c.x[c.len]==0)
c.len--;
return c;
}
friend bign operator -(bign a,bign b)
{
if(a<b)
swap(a,b);
int i=1;
bign c,aa=a,bb=b;
while(i<=aa.len+1||i<=bb.len+1)
{
if(aa.x[i]<bb.x[i])
{
aa.x[i+1]--;
aa.x[i]+=10;
}
c.x[i]=aa.x[i]-bb.x[i];
i++;
}
c.len=i;
while(c.x[c.len]==0)
c.len--;
return c;
}
friend bign operator *(bign a,int b)
{
bign c;
int g=0,l=a.len;
for(int i=1;i<=l;i++)
{
c.x[i]=a.x[i]*b+g;
g=c.x[i]/10;
c.x[i]%=10;
}
while(g>0)
{
l++;
c.x[l]=g%10;
g/=10;
}
while(l>1&&c.x[l]==0)
l--;
c.len=l;
}
};
void cin_bign(bign &a)
{
char s[20001];
scanf("%s",s);
a=s;
}
void cout_bign(bign a)
{
bool ok=false;
for(int i=a.len;i>0;i--)
{
printf("%d",a.x[i]);
ok=1;
}
if(!ok) printf("0");
printf("\n");
}
int main()
{
scanf("%d",&n);
bign x,y,z;
if(n==1)
{
printf("1");
return 0;
}
if(n==2)
{
printf("5");
return 0;
}
x=5;
y=1;
for(int i=3;i<=n;i++)
{
z=x+x+x-y+2;
y=x;
x=z;
}
cout_bign(z);
return 0;
}