題目大意
如果兩棵樹可以通過重標號后變為完全相同,那么它們就是同構的。
將中間節點定義為度數大於 \(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; }