矩陣樹定理淺談


矩陣樹定理淺談

一、前置知識

  在學習矩陣樹定理之前,要知道什么是生成樹,知道怎么運用高斯消元求一個矩陣的行列式。

二、定理內容

  這個定理共分為三個部分:1.給出無向圖,求這個圖的生成樹個數。2.給出有向圖和其中的一個點,求以這個點為根的生成外向樹個數。3.給出有向圖和其中一個點,求以這個點為根的生成內向樹個數。

  部分一:我們對這個圖構造兩個矩陣,分別是這個圖的連通矩陣和度數矩陣。連通矩陣$S_1$的第$i$行第$j$列上的數字表示原無向圖中編號為$i$和編號為$j$的兩個點之間的邊的條數。度數矩陣$S_2$只有斜對角線上有數字,即只有第$i$行第$i$列上有數字,表示編號為$i$的點的度數是多少。我們將兩個矩陣相減,即$S_2-S_1$,我們記得到的矩陣為$T$,我們將矩陣$T$去掉任意一行和一列(一般情況去掉最后一行和最后一列的寫法比較多)得到$T'$,最后生成樹的個數就是這個矩陣$T' $的行列式。

  部分二:我們對這個圖構造兩個矩陣,分別是這個圖的連通矩陣和度數矩陣。連通矩陣$S_1$的第$i$行第$j$列上的數字表示原無向圖中編號為$i$和編號為$j$的兩個點之間編號$i$的點指向編號為$j$的點的條數。度數矩陣$S_2$只有斜對角線上有數字,即只有第$i$行第$i$列上有數字,表示編號為$i$的點的入度是多少。我們將兩個矩陣相減,即$S_2-S_1$,我們記得到的矩陣為$T$,我們將矩陣$T$去掉根所在行和根所在列得到$T'$,最后生成樹的個數就是這個矩陣$T' $的行列式。

  部分三:我們對這個圖構造兩個矩陣,分別是這個圖的連通矩陣和度數矩陣。連通矩陣$S_1$的第$i$行第$j$列上的數字表示原無向圖中編號為$i$和編號為$j$的兩個點之間編號$i$的點指向編號為$j$的點的條數。度數矩陣$S_2$只有斜對角線上有數字,即只有第$i$行第$i$列上有數字,表示編號為$i$的點的出度是多少。我們將兩個矩陣相減,即$S_2-S_1$,我們記得到的矩陣為$T$,我們將矩陣$T$去掉根所在行和根所在列得到$T'$,最后生成樹的個數就是這個矩陣$T' $的行列式。

三、例題

  例一:bzoj4031小z的房間

  題目描述:你突然有了一個大房子,房子里面有一些房間。事實上,你的房子可以看做是一個包含n*m個格子的格狀矩形,每個格子是一個房間或者是一個柱子。在一開始的時候,相鄰的格子之間都有牆隔着。

  你想要打通一些相鄰房間的牆,使得所有房間能夠互相到達。在此過程中,你不能把房子給打穿,或者打通柱子(以及柱子旁邊的牆)。同時,你不希望在房子中有小偷的時候會很難抓,所以你希望任意兩個房間之間都只有一條通路。現在,你希望統計一共有多少種可行的方案。

  題目講解:這是一道十分簡單的矩陣樹定理的題目,我們將每一個房子看做一個節點,對於隔開兩個房子的一堵牆看做連接兩個點的一條無向邊,這樣就是無向圖求生成樹個數。

#include <cmath>
#include <cstdio>
#include <algorithm>
using namespace std;
#define N 20
#define eps 1e-10
#define mod 1000000000
int n,m,num[N][N],cnt;
int dicx[5]={0,0,0,1,-1},dicy[5]={0,1,-1,0,0};
bool map[N][N];char str[N];
long long ans=1,square[N*N][N*N];
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%s",str+1);
        for(int j=1;j<=m;j++) map[i][j]=(str[j]=='.');
    }
    for(int i=1;i<=n;i++) for(int j=1;j<=m;j++)
        if(map[i][j]) num[i][j]=++cnt;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            for(int k=1;k<=4;k++)
        		if(map[i+dicx[k]][j+dicy[k]]&&map[i][j])
                {
                    square[num[i][j]][num[i][j]]++,
            		square[num[i][j]][num[i+dicx[k]][j+dicy[k]]]--;
                }
    for(int i=1;i<cnt;i++)
    {
        int j;
        for(j=i;j<cnt;j++)
            if(square[j][i]) break;
        if(j==cnt) continue;
        if(j!=i)
        {
            for(int k=i;k<cnt;k++)
                swap(square[i][k],square[j][k]);
            ans*=-1;
        }
        for(j=i+1;j<cnt;j++)
        {
            while(square[j][i])
            {
                int t=square[j][i]/square[i][i];
                for(int k=i;k<cnt;k++)
                    square[j][k]=(square[j][k]-square[i][k]*t%mod+mod)%mod;
                if(!square[j][i]) break;
                for(int k=i;k<cnt;k++)
                    swap(square[i][k],square[j][k]);
                ans*=-1;
            }
        }
    }
    for(int i=1;i<cnt;i++)
        ans*=square[i][i],ans%=mod;
    printf("%lld\n",(ans%mod+mod)%mod);
}

  例二:bzoj1002輪狀病毒

  題目描述:輪狀病毒有很多變種。許多輪狀病毒都是由一個輪狀基產生。一個n輪狀基由圓環上n個不同的基原子和圓心的一個核原子構成。2個原子之間的邊表示這2個原子之間的信息通道,如圖1。

  n輪狀病毒的產生規律是在n輪狀基中刪除若干邊,使各原子之間有唯一一條信息通道。例如,共有16個不同的3輪狀病毒,入圖2所示。

  給定n(N<=100),編程計算有多少個不同的n輪狀病毒。

  題目講解:我們發現這是一道無向圖的生成樹計數問題,我們考慮一個$n$輪狀基得到的行列式是(下圖所示)。

  對於上方的行列式一共有$n$行$n$列,我們通過找規律(推式子)可以發現,這個行列式可以遞推,我們設$f[i]$表示$i$輪狀基的行列式值,則$f[i]=3\times f[i-1]-f[i-2]+2$。最后用高精度算一下就好了。

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define mod 100000000
#define N 1000
int n;
struct Num
{
	long long num[N];
	Num() {memset(num,0,sizeof(num));}
    void print()
    {
        for(int i=num[0];i;i--)
            (i==num[0])?printf("%lld",num[i]):printf("%08lld",num[i]);
        printf("\n");
    }
	Num operator + (const Num &a) const
	{
		Num ans;ans.num[0]=max(a.num[0],num[0]);long long tmp=0;
		for(int i=1;i<=ans.num[0];i++)
			ans.num[i]=num[i]+a.num[i]+tmp,tmp=ans.num[i]/mod,ans.num[i]%=mod;
		while(tmp) ans.num[++ans.num[0]]=tmp%mod,tmp/=mod;
		return ans;
	}
	Num operator - (const Num &a) const
	{
		Num ans;ans.num[0]=num[0];long long tmp=0;
		for(int i=1;i<=ans.num[0];i++)
		{
			if(num[i]-tmp-a.num[i]<0) ans.num[i]=num[i]-tmp-a.num[i]+mod,tmp=1;
			else ans.num[i]=num[i]-tmp-a.num[i],tmp=0;
		}
		while(ans.num[ans.num[0]]==0) ans.num[0]--;
		return ans;
	}
	Num operator * (const Num &a) const
	{
		Num ans,tmp;long long tmp1=0,tmp2;
		for(int i=1;i<=a.num[0];i++)
		{
			tmp2=a.num[i],tmp.num[0]=i-1,tmp1=0,tmp.num[tmp.num[0]]=0;
			for(int j=1;j<=num[0];j++)
			{
				tmp.num[++tmp.num[0]]=num[j]*tmp2+tmp1;
				tmp1=tmp.num[tmp.num[0]]/mod,tmp.num[tmp.num[0]]%=mod;
			}
			while(tmp1) tmp.num[++tmp.num[0]]=tmp1%mod,tmp1/=mod;
			ans=ans+tmp;
		}return ans;
	}
	Num operator ^ (const int &a) const
	{
		Num ans,x;int y=a;ans.num[0]=ans.num[1]=1;
		for(int i=0;i<=num[0];i++) x.num[i]=num[i];
		while(y) {if(y&1) ans=ans*x;x=x*x,y>>=1;} return ans;
	}
}ans[200],tmp,tmp1;
void solve()
{
    tmp.num[1]=3,tmp1.num[1]=2,tmp1.num[0]=tmp.num[0]=ans[1].num[0]=ans[1].num[1]=1;
    for(int i=2;i<=n;i++) ans[i]=ans[i-1]*tmp-ans[i-2]+tmp1;
}
int main() {scanf("%d",&n),solve(),ans[n].print();}

  例三:bzoj2467生成樹

 

  題目描述:有一種圖形叫做五角形圈。一個五角形圈的中心有1個由n個頂點和n條邊組成的圈。在中心的這個n邊圈的每一條邊同時也是某一個五角形的一條邊,一共有n個不同的五角形。這些五角形只在五角形圈的中心的圈上有公共的頂點。如圖0所示是一個4-五角形圈。

  現在給定一個n五角形圈,你的任務就是求出n五角形圈的不同生成樹的數目。還記得什么是圖的生成樹嗎?一個圖的生成樹是保留原圖的所有頂點以及頂點的數目減去一這么多條邊,從而生成的一棵樹。

  注意:在給定的n五角形圈中所有頂點均視為不同的頂點。

  題目講解:這顯然是一道優秀的矩陣樹定理裸題,值得注意的是這道題目當$n==2$時需要特判。這還有一種組合數的做法,本人寫的是組合數的方法。

  例四:bzoj4894天賦

  題目描述:小明有許多潛在的天賦,他希望學習這些天賦來變得更強。正如許多游戲中一樣,小明也有n種潛在的天賦,但有一些天賦必須是要有前置天賦才能夠學習得到的。也就是說,有一些天賦必須是要在學習了另一個天賦的條件下才能學習的。比如,要想學會"開炮",必須先學會"開槍"。一項天賦可能有多個前置天賦,但只需習得其中一個就可以學習這一項天賦。上帝不想為難小明,於是小明天生就已經習得了1號天賦-----"打架"。於是小明想知道學習完這n種天賦的方案數,答案對1,000,000,007取模。

  題目講解:我們看這道題目,顯然是一道有向圖,題目中要我們求的就是以$1$為根的外向生成樹個數,這個就是我們在第二大點中講述的第二部分。

#include <cmath>
#include <cstdio>
#include <algorithm>
using namespace std;
#define N 300
#define eps 1e-10
#define mod 1000000007
int n;long long ans=1,square[N][N];
int main()
{
    scanf("%d",&n),n--;
    for(int i=0;i<=n;i++) for(int j=0,a;j<=n;j++)
    {
        scanf("%1d",&a);
        if(a==1) square[j][j]++,square[i][j]--;
    }
    for(int i=1;i<=n;i++)
    {
        int j;for(j=i;j<=n;j++) if(square[j][i]) break;
        if(j==n+1) continue;
        if(j!=i)
        {
            for(int k=i;k<=n;k++)
                swap(square[i][k],square[j][k]);
            ans*=-1;
        }
        for(j=i+1;j<=n;j++)
        {
            while(square[j][i])
            {
                int t=square[j][i]/square[i][i];
                for(int k=i;k<=n;k++)
                    square[j][k]=(square[j][k]-square[i][k]*t%mod+mod)%mod;
                if(!square[j][i]) break;
                for(int k=i;k<=n;k++)
                    swap(square[i][k],square[j][k]);
                ans*=-1;
            }
        }
    }
    for(int i=1;i<=n;i++) ans*=square[i][i],ans%=mod;
    printf("%lld\n",(ans%mod+mod)%mod);
}

  例五:bzoj5056OI游戲

  題目描述:小Van的CP最喜歡玩與OI有關的游戲啦~小Van為了討好她,於是冥思苦想,終於創造了一個新游戲。下面是小Van的OI游戲規則:給定一個無向連通圖,有N個節點,編號為0~N-1。圖里的每一條邊都有一個正整數權值,邊權在1~9之間。要求從圖里刪掉某些邊(有可能0條),使得剩下的圖滿足以下兩個條件:

  1) 剩下的圖是一棵樹,有N-1條邊。

  2) 對於所有v (0 < v < N),0到v的最短路(也就是樹中唯一路徑長度)和原圖中的最短路長度相同。

  最終要報出有多少種不同的刪法可以滿足上述條件。(兩種刪法不同當且僅當存在兩個點,一種刪法刪完之后這兩個點之間存在邊而另外一種刪法不存在。)由於答案有可能非常大,良心的小Van只需要答案膜1,000,000,007的結果。

  題目講解:我們可以先求出任意兩個點的最短路,這樣我們就可以判斷那些邊可以留。我們現在假設第$i$條邊連接了$x$,$y$兩個點,這兩個點到點$0$的最短路分別為$dis_x$和$dis_y$,若$dis_x+val_i==dis_y$,則$y$的最短路可以由$x$轉移過來,這樣我們保留一條從$x$指向$y$的有向邊,這樣題目就轉化成為$0$為根的外向生成樹的個數。

#include <cstdio>
#include <algorithm>
using namespace std;
#define inf 1000000000
#define mod 1000000007
#define N 51
int n,dis[N][N],dis2[N][N];long long ans=1,squ[N][N];
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++) for(int j=1;j<=n;j++)
    {
        scanf("%1d",&dis[i][j]),dis2[i][j]=dis[i][j];
        if(i!=j&&(!dis[i][j])) dis[i][j]=inf;
    }
    for(int k=1;k<=n;k++) for(int i=1;i<=n;i++) for(int j=1;j<=n;j++)
        dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
    for(int i=1;i<=n;i++) for(int j=1;j<=n;j++)
        if(dis2[i][j]&&dis[1][i]+dis2[i][j]!=dis[1][j]) dis2[i][j]=0;
    for(int i=1;i<=n;i++) for(int j=1;j<=n;j++)
        if(dis2[i][j]) squ[j-1][j-1]++,squ[i-1][j-1]--; n--;
    for(int i=1;i<=n;i++)
    {
        int j;for(j=i;j<=n;j++) if(squ[j][i]) break;
        if(j==n+1) continue;
        if(i!=j) {for(int k=i;k<=n;k++) swap(squ[i][k],squ[j][k]);ans*=-1;}
        for(j=i+1;j<=n;j++)
        {
            while(squ[j][i])
            {
                int t=squ[j][i]/squ[i][i];
                for(int k=i;k<=n;k++)
                    squ[j][k]=(squ[j][k]-squ[i][k]*t%mod+mod)%mod;
                if(!squ[j][i]) break;
                for(int k=i;k<=n;k++)
                    swap(squ[i][k],squ[j][k]);
                ans*=-1;
            }
        }
    }
    for(int i=1;i<=n;i++) (ans*=squ[i][i])%=mod;
    printf("%lld\n",(ans%mod+mod)%mod);
}

  例六:bzoj45496黑暗前的幻想鄉

  題目描述:四年一度的幻想鄉大選開始了,最近幻想鄉最大的問題是很多來歷不明的妖怪涌入了幻想鄉,擾亂了幻想鄉昔日的秩序。但是幻想鄉的建制派妖怪(人類)博麗靈夢和八雲紫等人整日高談所有妖怪平等,幻想鄉多元化等等,對於幻想鄉目前面臨的種種大問題卻給不出合適的解決方案。風間幽香是幻想鄉里少有的意識到了問題的嚴重性的大妖怪。她這次勇敢的站了出來參加幻想鄉大選。提出包括在幻想鄉邊境建牆(並讓人類出錢),大力開展基礎設施建設挽回失業率等一系列方案,成為了大選年出人意料的黑馬並順利的當上了幻想鄉的大統領。

  幽香上台以后,第一項措施就是要修建幻想鄉的公路。幻想鄉有 N 個城市,之間原來沒有任何路。幽香向選民承諾要減稅,所以她打算只修 N- 1 條路將這些城市連接起來。但是幻想鄉有正好 N- 1 個建築公司,每個建築公司都想在修路的過程中獲得一些好處。雖然這些建築公司在選舉前沒有給幽香錢,幽香還是打算和他們搞好關系,因為她還指望他們幫她建牆。所以她打算讓每個建築公司都負責一條路來修。每個建築公司都告訴了幽香自己有能力負責修建的路是哪些城市之間的。所以幽香打算選擇 N-1 條能夠連接幻想鄉所有城市的邊,然后每條邊都交給一個能夠負責該邊的建築公司修建,並且每個建築公司都恰好修一條邊。幽香現在想要知道一共有多少種可能的方案呢?兩個方案不同當且僅當它們要么修的邊的集合不同,要么邊的分配方式不同。

  題目講解:我們看$n$十分小,我們可以考慮容斥原理,我們容斥一下,每一次都用矩陣樹定理來做,我們設$calc(i)$為假設欽定$i$個建築公司不選,其他的隨意的方案數,最后的答案即為:$ans=calc(0)-calc(1)+calc(2)-……$。

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define mod 1000000007
#define N 70
int n,many[N],from[N][N],to[N][N];long long ans,squ[N][N];
long long calc()
{
    long long tmp=1;
    for(int i=1;i<n;i++)
    {
        int j;for(j=i;j<n;j++) if(squ[j][i]) break;
        if(j==n) continue;
        if(j!=i) {for(int k=i;k<n;k++) swap(squ[i][k],squ[j][k]);tmp*=-1;}
        for(j=i+1;j<n;j++)
        {
            while(squ[j][i])
            {
                long long t=squ[j][i]/squ[i][i];
                for(int k=i;k<n;k++)
                    squ[j][k]=(squ[j][k]-squ[i][k]*t%mod+mod)%mod;
                if(!squ[j][i]) break;
                for(int k=i;k<n;k++)
                    swap(squ[i][k],squ[j][k]);
                tmp*=-1;
            }
        }
    }
    for(int i=1;i<n;i++) (tmp*=squ[i][i])%=mod; return tmp;
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<n;i++) {scanf("%d",&many[i]);
        for(int j=1;j<=many[i];j++) scanf("%d%d",&from[i][j],&to[i][j]);}
    for(int i=0;i<(1<<(n-1));i++)
    {
        int times=n-1;memset(squ,0,sizeof squ);
        for(int j=1;j<n;j++) if(i>>(j-1)&1) times--;
        for(int j=1;j<n;j++) if(i>>(j-1)&1)
            for(int k=1;k<=many[j];k++)
                squ[from[j][k]][from[j][k]]++,squ[to[j][k]][to[j][k]]++,
                squ[from[j][k]][to[j][k]]--,squ[to[j][k]][from[j][k]]--;
        long long tmp=calc();if(times&1) tmp*=-1; (ans+=tmp)%=mod;
    } printf("%lld\n",(ans%mod+mod)%mod);
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM