1. 程式人生 > >【BZOJ1494】【NOI2007】生成樹計數(動態規劃,矩陣快速冪)

【BZOJ1494】【NOI2007】生成樹計數(動態規劃,矩陣快速冪)

題面

Description

最近,小棟在無向連通圖的生成樹個數計算方面有了驚人的進展,他發現:

·n個結點的環的生成樹個數為n。

·n個結點的完全圖的生成樹個數為n^(n-2)。這兩個發現讓小棟欣喜若狂,由此更加堅定了他繼續計算生成樹個數的

想法,他要計算出各種各樣圖的生成樹數目。一天,小棟和同學聚會,大家圍坐在一張大圓桌周圍。小棟看了看,

馬上想到了生成樹問題。如果把每個同學看成一個結點,鄰座(結點間距離為1)的同學間連一條邊,就變成了一

個環。可是,小棟對環的計數已經十分嫻熟且不再感興趣。於是,小棟又把圖變了一下:不僅把鄰座的同學之間連

一條邊,還把相隔一個座位(結點間距離為2)的同學之間也連一條邊,將結點間有邊直接相連的這兩種情況統稱

為有邊相連,如圖1所示。

img

img

小棟以前沒有計算過這類圖的生成樹個數,但是,他想起了老師講過的計算任意圖的生成樹個數的一種通用方法:

構造一個n×n的矩陣A={aij},其中

img

img

img

img

其中di表示結點i的度數。與圖1相應的A矩陣如下所示。為了計算圖1所對應的生成數的個數,只要去掉矩陣A的最

後一行和最後一列,得到一個(n-1)×(n-1)的矩陣B,計算出矩陣B的行列式的值便可得到圖1的生成樹的個數所以

生成樹的個數為|B|=3528。小棟發現利用通用方法,因計算過於複雜而很難算出來,而且用其他方法也難以找到更

簡便的公式進行計算。於是,他將圖做了簡化,從一個地方將圓桌斷開,這樣所有的同學形成了一條鏈,連線距離

為1和距離為2的點。例如八個點的情形如下:

img

img

這樣生成樹的總數就減少了很多。小棟不停的思考,一直到聚會結束,終於找到了一種快捷的方法計算出這個圖的

生成樹個數。可是,如果把距離為3的點也連起來,小棟就不知道如何快捷計算了。現在,請你幫助小棟計算這類

圖的生成樹的數目。

Input

包含兩個整數k,n,由一個空格分隔。k表示要將所有距離不超過k(含k)的結點連線起來,n表示有n個結點。

Output

輸出一個整數,表示生成樹的個數。由於答案可能比較大,所以你 只要輸出答案除65521 的餘數即可。

Sample Input

3 5

Sample Output

75

HINT

img img

題解

這是一道神仙題啊。
鑑於我自己在網上研究了很久各種題解才知道怎麼做。
我還是打算好好地把這道題目從頭到尾寫一寫。
從部分分開始吧。

Task160pts:k5,n100
送分的一檔
直接暴力構圖,矩陣樹定理直接算就好了
時間複雜度O(n3)

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<set>
#include<map>
#include<vector>
#include<queue>
using namespace std;
#define ll long long
#define RG register
#define MOD 65521
#define MAX 111
inline int read()
{
    RG int x=0,t=1;RG char ch=getchar();
    while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
    if(ch=='-')t=-1,ch=getchar();
    while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
    return x*t;
}
int n,K,c[MAX][MAX],ans=1;
int main()
{
    K=read();n=read();
    for(int i=1;i<=n;++i)
        for(int j=i+1;j<=min(n,i+K);++j)
            c[i][i]++,c[j][j]++,c[i][j]--,c[j][i]--;
    for(int i=2;i<=n;++i)
        for(int j=i+1;j<=n;++j)
            while(c[j][i])
            {
                int t=c[i][i]/c[j][i];
                for(int k=i;k<=n;++k)c[i][k]=(c[i][k]-1ll*c[j][k]*t%MOD)%MOD,swap(c[i][k],c[j][k]);
                ans=-ans;
            }
    for(int i=2;i<=n;++i)ans=1ll*ans*c[i][i]%MOD;
    printf("%d\n",ans);
    return 0;
}

Task280pts:k5,n10000
這檔分其實寫出來基本就會滿分了
發現k的範圍十分的小,也就是每個點的邊並不會很多
換而言之,如果一個點要和前面的所有點處在一個生成樹中
意味著它必須和前面k個點中的至少一個處在同一個聯通塊中
所以,我們只需要考慮當前點和它前面K個點的狀態就行了

那麼,我們的狀態是什麼?
我們思考一下,對於生成樹而言,我們比較在意的只有兩個方面:聯通塊和邊
對於相鄰的K個點,單獨拿出來顯然是一個完全圖,因此不需要考慮邊的問題
所以,我們的狀態和聯通塊有關。
現在的問題又變成了如何記錄前面k個點的聯通塊呢?
我們可以按照順序編號,保證任意一個編號的聯通塊在出現之前,所有小於它的編號都已經出現過。(最小表示法吼啊)
(不一定要這樣,只需要一種能夠保證所有相同的聯通塊方案只會被算一次就行了)
編完號之後,我們可以把前面k個點所在的聯通塊給壓成十進位制
因為k5,就只需要三個二進位制位,所以可以用八進位制來把聯通塊的情況給壓成十進位制位。
那麼,不同的聯通塊方案數有多少呢?
相當於把n個球放進任意數量個集合,也就是Bell數,算出來是52

我們不關心每個聯通塊之間的連線方案,我們只關心如何對於兩種聯通塊之間進行轉移
所以我們可以列舉當前點和前面k個點的連邊方案(最多就k條邊)
然後暴力(用並查集)判斷是否成環,
同時,最前面的那個點也必須和當前這k個點中的一個在同一個聯通塊中
這樣就可以轉移到另外一個聯通塊的情況
再用上面的最小表示法把它的編號還原出來
這樣證明可以從當前狀態向後面的狀態進行轉移。

dp要的是初始狀態和轉移。
顯然搞出來了轉移,考慮初始狀態。
顯然不能一開始不滿k個點
所以直接從k個點開始計算
因為每個聯通塊之間是完全圖,所以可以暴力計算聯通塊大小為x時的方案數
那麼k個點時,某個聯通塊情況的方案數就是numsize
聯通塊大小為1,2時,有1種方法
聯通塊大小為33
聯通塊大小為416
聯通塊大小為5125種。

這樣子,我們就可以O(5225K2+n522)轉移了
這樣子可以過80pts

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<set>
#include<map>
#include<vector>
#include<queue>
using namespace std;
#define ll long long
#define RG register
#define MOD 65521
#define MAX 55
inline ll read()
{
    RG ll x=0,t=1;RG char ch=getchar();
    while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
    if(ch=='-')t=-1,ch=getchar();
    while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
    return x*t;
}
ll n;int K,cnt;
int p[1<<20],st[MAX];
bool check(int t)//檢查一個狀態是否合法
{
    int tmp=1<<1;//因為第一個點一定屬於一號聯通快,所以先把一號聯通快放進去檢查
    for(int i=3;i<K+K+K;i+=3)
    {
        for(int j=1;j<((t>>i)&7);++j)//檢查比當前編號小的所有編號是否都已經出現過
            if(!(tmp&(1<<j)))return false;
        tmp|=1<<((t>>i)&7);//將當前編號也給放進來
    }
    return true;
}
void dfs(int x,int t)//暴力找出所有狀態,每個編號利用3個二進位制位存
{
    if(x==K){if(check(t))p[t]=++cnt,st[cnt]=t;return;}
    for(int i=1;i<=K;++i)dfs(x+1,t|(i<<(x+x+x)));
}
int fa[MAX],a[MAX];
int getf(int x){return x==fa[x]?x:fa[x]=getf(fa[x]);}
int f[11111][MAX],g[MAX][MAX];
int main()
{
    K=read();n=read();dfs(1,1);
    for(int i=1;i<=cnt;++i)
    {
        f[K][i]=1;memset(a,0,sizeof(a));
        for(int j=0;j<K;++j)++a[(st[i]>>(j*3))&7];
        for(int j=1;j<=K;++j)
            if(a[j]==3)f[K][i]=3;
            else if(a[j]==4)f[K][i]=16;
            else if(a[j]==5)f[K][i]=125;
        int t=st[i];
        for(int s=0;s<(1<<K);++s)//暴力列舉當前點對於前面幾個點的連邊狀態
        {
            for(int j=0;j<=K;++j)fa[j]=j;
            for(int j=0;j<K;++j)//利用並查集維護聯通性
                for(int k=j+1;k<K;++k)
                    if(((t>>(3*j))&7)==((t>>(3*k))&7))
                        fa[getf(j)]=getf(k);
            bool cir=false;
            for(int j=0;j<K;++j)//檢查當前點的連邊
                if(s&(1<<j))
                {
                    if(getf(K)==getf(j)){cir=true;break;}//出現了環
                    fa[getf(K)]=getf(j);
                }
            if(cir)continue;//連邊不合法
            for(int j=1;j<=K;++j)//最前面的點必須和後面的一個點聯通,否則就無法聯通了
                if(getf(0)==getf(j)){cir=true;break;}
            if(!cir)continue;
            int now=0,used=0;
            for(int j=0;j<K;++j)//當前存在合法的聯通方案,因此當前的狀態可以轉移到另外的一個狀態上去
                if(!(now&(7<<(j*3))))//當前點不在任意一個聯通塊中
                {
                    now|=++used<<(j*3);//新的聯通塊
                    for(int k=j+1;k<K;++k)//把所有在一個聯通塊裡的點丟到狀態裡去
                        if(getf(j+1)==getf(k+1))
                            now|=used<<(k*3);
                }
            g[i][p[now]]++;
        }
    }
    for(int i=K+1;i<=n;++i)
        for(int j=1;j<=cnt;++j)
            for(int k=1;k<=cnt;++k)
                if(g[j][k])f[i][k]=(f[i][k]+1ll*g[j][k]*f[i-1][j])%MOD;
    printf("%d\n",f[n][1]);
    return 0;
}

AC:n1015
明擺著log演算法,
發現每次轉移相同,直接矩陣快速冪就行了
時間複雜度