「Codeforces 724F」Uniformly Branched Trees


題目大意

如果兩棵樹可以通過重標號后變為完全相同,那么它們就是同構的。

將中間節點定義為度數大於 \(1\) 的節點。計算由 \(n\) 個節點,其中所有的中間節點度數都為 \(d\) 的互不同構的樹的數量。

答案對大質數取模。\(1\leq n\leq 1000,2\leq d \leq 10,10^{8}\leq \text{mod} \leq 10^9\)。

Solution

Part 1

先來思考一個組合問題:在 \(x\) 個方案中不分順序地選 \(t\) 種,可重復。求方案數。

這里給出一種非插板法的組合意義的解釋:問題在於如何處理“可重復”這個條件。考慮在 \(x\) 種方案后面額外添加進去 \(t-1\) 種方案。前面 \(x\) 種方案中的第 \(i\) 種方案,表示選擇了第 \(i\) 種方案。后面的 \(t-1\) 種方案中的第 \(i\) 種方案,表示 選出的 \(i+1\) 種方案與第 \(i\) 種一樣。可以發現,在這 \(x+t-1\) 種方案中任選 \(t\) 個的方案數就是答案。所以方案數為 \(\dbinom{x+t-1}{t}\)

Part 2

然后再來看這道題。

如何判斷兩棵無根樹是否同構呢?由於無根樹是可以重標號(換根)的,所以我們需要對於每棵無根樹找出一個特殊的根,將無根樹轉化為有根樹,才能方便比較同構。

對於無根樹而言,能夠確定的一個點就是 重心。重心有一個特殊的性質:一棵具有 \(n\) 個節點的無根樹,若以該樹的重心作為整棵樹的根,則任意子樹大小都小於 \(\frac{n}{2}\)。有兩種情況:單重心 與 雙重心

\(dp_{i,j,k}\) 表示節點數為 \(i\),有 \(j\) 棵子樹,子樹大小都不超過 \(k\) 的有根樹數量。

有兩種情況:

  • 所有子樹大小都不超過 \(k\)\(dp_{i,j,k}←dp_{i,j,k-1}\)

  • 不滿足“所有子樹大小都不超過 \(k\)”:不滿足“所有子樹大小都不超過 \(k\)”意味着至少有一棵子樹的大小是 \(k\)。考慮枚舉子樹大小等於 \(k\) 的子樹個數。假設有 \(t\) 棵子樹大小等於 \(k\)。由於子樹之間是可以相同的(即 可重復),所以這 \(t\) 棵子樹的方案數就是從 \(dp_{k,d-1,k-1}\)\(d\) 就是題目中的 \(d\))種方案中不分順序地選 \(t\) 種並且可重復的方案數。實際上就是我們最開始思考的那個組合問題。所以方案數為 \(\dbinom{dp_{k,d-1,k-1}+t-1}{t}\)。則 \(dp_{i,j,k}←dp_{i-t\times k,j-t,k-1}\times \dbinom{dp_{k,d-1,k-1}+t-1}{t}\)。

若只考慮單重心,那么答案為 \(dp_{n,d,\lfloor \frac{n}{2}\rfloor}\)

Part 3

然后考慮雙重心的情況。出現了雙重心,那么肯定是一條邊連接的兩個點分別掛着兩棵大小相等的子樹。大概長這樣:

顯然只有 \(n\) 是 偶數 時才會出現雙重心。如圖所示,把整棵樹拆成兩部分,就轉化成了兩個單重心。對於其中一部分的方案數為 \(dp_{\frac{n}{2},d-1,\lfloor \frac{n}{2}\rfloor -1}\)。所以雙重心的情況的個數為,從 \(dp_{\frac{n}{2},d-1,\lfloor \frac{n}{2}\rfloor -1}\) 種方案中選出 \(2\) 種的方案數,則方案數為 \(\dbinom{dp_{\frac{n}{2},d-1,\lfloor \frac{n}{2}\rfloor -1}}{2}\)

Part 4

綜上所述:

  • \(n\) 是奇數時,答案為 \(dp_{n,d,\lfloor \frac{n}{2}\rfloor}\)
  • \(n\) 是偶數時,答案為 \(dp_{n,d,\lfloor \frac{n}{2}\rfloor}-\dbinom{dp_{\frac{n}{2},d-1,\lfloor \frac{n}{2}\rfloor -1}}{2}\)。(為什么是單重心的情況個數減雙重心的情況個數呢?因為每個雙重心的情況都在前面算單重心的情況中算了兩遍,一遍是以其中一個重心為根,一遍是以另一個重心為根。所以去掉重復的部分就是答案。)

Code

注意到要求的組合數 \(\binom{n}{m}\)\(n\) 都比較大,\(m\) 都比較小。

\(C_n^m=\frac{n!}{m!(n-m)!}\) ,因為 \(m\) 非常小,所以 \(m!\) 可以很快被計算出來。

再考慮 \(\frac{n!}{(n-m)!}\)。\(\frac{n!}{(n-m)!}=\frac{n\times (n-1)\times ...\times (n-m+1)\times (n-m)\times (n-m-1)\times ...\times 2\times 1}{(n-m)\times (n-m-1)\times ...\times 2\times 1}=n\times (n-1)\times ...\times (n-m+1)\)。只有 \(m\) 項。也可以暴力計算。

代碼里是預處理出 \(\frac{1}{i!}\),然后用這樣的方法計算。

另外 \(n\leq 2\) 的時候要特判,dp 邊界條件也要注意。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e3+5,M=20;
int n,d,mod,dp[N][M][N],f[M],g[M],ans; 
int mul(int x,int n,int mod){
    int ans=mod!=1;
    for(x%=mod;n;n>>=1,x=x*x%mod)
        if(n&1) ans=ans*x%mod;
    return ans;
}
void init(){ 
    int n=10;
    f[0]=g[0]=1;
    for(int i=1;i<=n;i++)
        f[i]=f[i-1]*i%mod; 
    g[n]=mul(f[n],mod-2,mod);
    for(int i=n-1;i;i--)
        g[i]=g[i+1]*(i+1)%mod;    //計算 1/(i!) 
} 
int C(int n,int m){    //計算組合數 
    if(n<m) return 0;
    int ans=1;
    for(int i=1;i<=m;i++)
        ans=ans*(n-i+1)%mod;
    return ans*g[m]%mod;
} 
signed main(){
    scanf("%lld%lld%lld",&n,&d,&mod),init();
    if(n<=2) puts("1"),exit(0);    //特判 
    for(int i=0;i<=n;i++) dp[1][0][i]=1;
    for(int i=2;i<=n;i++)    //枚舉節點數 
        for(int j=1;j<=min(i-1,d);j++)    //枚舉子樹個數 
            for(int k=1;k<=n;k++){    //枚舉子樹大小的限制 
                dp[i][j][k]=dp[i][j][k-1];    //所有子樹大小都不超過 k 的情況 
                for(int t=1;i-t*k>0&&j-t>=0;t++){    //t 棵子樹大小等於 k 
                    if(k!=1) dp[i][j][k]=(dp[i][j][k]+dp[i-t*k][j-t][k-1]*C(dp[k][d-1][k-1]+t-1,t)%mod)%mod;
                    else dp[i][j][k]=(dp[i][j][k]+dp[i-t*k][j-t][k-1]*C(dp[k][0][k-1]+t-1,t)%mod)%mod;
                }
            }
    if(n&1) ans=dp[n][d][n/2];    //n 為奇數 
    else ans=(dp[n][d][n/2]-C(dp[n/2][d-1][n/2-1],2)+mod)%mod;    //n 為偶數 
    printf("%lld\n",ans);
    return 0;
}


免責聲明!

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



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