計樹問題小結
標題並沒有打錯字
前言
某個當時對生成樹一竅不通的蒟蒻在\(WC2019T1\)看到了“\(n\)個點的無根樹一共有\(n^{n-2}\)種”時感到十分詭異,於是惡補了相關知識但是並沒有總結,正好和最近的無標號樹的計數問題合在一起
\(Prufer\)序列
Prufer數列是無根樹的一種數列。在組合數學中,Prufer數列由有一個對於頂點標過號的樹轉化來的數列,點數為n的樹轉化來的Prufer數列長度為n-2。它可以通過簡單的迭代方法計算出來。
——百度百科
下面通過給出構造方法來得到這樣一個結論:無根樹和\(Prufer\)序列是一一對應的關系
無根樹轉\(Prufer\)序列
1)找到編號最小的葉子結點(度數為1的節點)
2)刪除該節點並在\(Prufer\)序列中添加與其有邊相連的點(也就是它的父親)
3)回到操作1直到樹中只剩下\(2\)個節點
顯然樹中一定最后會剩下\(2\)個節點(其實這就是一個類似\(toposort\)的過程),這樣就說明了一棵無根樹僅對應着一個\(Prufer\)序列
\(Prufer\)序列轉無根樹
1)構建一個含有\(n\)個點的編號的集合\(G\)
2)取出\(Prufer\)序列的第一個數\(u\)和\(G\)中未在\(Prufer\)序列出現的編號最小的點\(v\)(沒有在\(Prufer\)序列出現說明它的度數為\(1\))
3)在\(u\)和\(v\)之間連一條邊\((u,v)\),並刪去\(u\)和\(v\)
4)重復上述步驟直到\(G\)中只剩兩個點,將其連一條邊
不難得到這其實是給每一個點找它在樹上的\(fa\)的過程(雖然是一棵無根樹),於是\(Prufer\)序列也僅對應這一棵無根樹
以上我們就得到了這兩者之間的一一對應關系,利用這一條性質可以解決一類計數問題
性質
1、\(n\)個點的無根樹一共有\(n^{n-2}\)種,有根樹有\(n^{n-1}\)種
將每一棵無根樹都映射到其對應的\(Prufer\)序列上,由於\(Prufer\)序列的每一位都有\(n\)種取值,於是就一共有\(n^{n-2}\)種;有根樹的話再乘上根的種數\(n\)即可
2、設編號\(u\)在\(Prufer\)序列出現過\(d_u\)次,那么編號為\(u\)的節點在樹上的度數為\(d_u+1\)
依然把無根樹當做有根樹來看待,這樣的話每個點\(u\)都有一個父親\(fa_u\),它必須在\(u\)刪去之后才有可能被刪去,類似的,\(u\)的所有兒子會在\(u\)之前被刪去,每一次刪去\(u\)的兒子時都會將\(u\)放入\(Prufer\)序列中,所以\(u\)一共有\(d_u\)個兒子,再算上它的父親就一共有\(d_u+1\)個點與之相連
3、記點\(1,2,\cdots,n\)的度數分別為\(d_1,d_2,\cdots d_n\),那么無根樹一共有\(\frac{(n-2)!}{\prod_{i=1}^n(d_i-1)!}\)種
由性質\(2\)知,點\(u\)在\(Prufer\)序列中的出現次數為\(d_u-1\),於是問題變成了重復排列的計算
例題:HNOI2004 樹的計數:模板題
HNOI2008 明明的煩惱:需要推式子
無標號有根樹的計數
看起來很高級,說個簡單的例子:烷基計數
這個題有一個弱化版本和一個強化版本,強化版本需要用到生成函數和\(Burnside\)引理,在此按下不表
弱化版本的話直接記\(f_i\)為有\(i\)個頂點的樹有多少種,枚舉三個兒子的大小進行轉移
接下來假設問題是這樣的:求\(n\)個點,每個點的度數限制為\(m\)的無根樹的方案數
先簡單提一下一個引理:\(n\)個元素的\(m\)元素可重集合(集合中的元素可以重復)的種數為\(\dbinom{n+m-1}{n-1}=\dbinom{n+m-1}{m}\)
證明的話可以考慮隔板法,我們可以看成是\(m\)個物品中間放\(n-1\)個隔板分成\(n\)組,每組對應着原來的\(n\)個元素的一個,物品對應着選出來的元素,每一組中的元素表示這幾個物品所代表的原來的集合中的元素是這一組所代表的原來的元素,直接做組合即可(注意這里物品是無標號的所以是組合)
記\(f_{i,j}\)為\(i\)個點,根節點度數為\(j\)時的方案數,顯然\(Ans=\sum_{i=0}^mf_{n,i}\)
為了方便轉移,再設\(a_i=\sum_{j=0}^{m-1}f_{i,j}\),表示了一棵\(i\)個點的子樹的方案數
我們枚舉度數和每個子樹的大小,記當前有\(k_s\)個大小為\(s\)的子樹,於是就有下面的轉移
后面那個組合數就是說有\(k_s\)個大小為\(s\)的子樹的方案數,這就是上面的可重元素的集合,注意每個子樹的根的度數要先減去與\(i\)相連的那個\(1\)
這樣直接\(dp\)顯然是不行的,我們考慮枚舉轉移時出現的最大子樹\(mx\),然后倒序枚舉\(i\),順序枚舉\(j\),以此確定\(mx\)的出現次數\(k\),具體的有
倒序枚舉\(i\)是為了在調用更小的下標是還未計算\(mx\)對其的貢獻
復雜度似乎是\(O(n^2mlogm)\),但是在烷基計數時\(m=4\),於是時間復雜度確定為\(O(n^2)\),可以通過
#include<iostream>
#include<string.h>
#include<string>
#include<stdio.h>
#include<algorithm>
#include<math.h>
#include<vector>
#include<queue>
#include<map>
#include<set>
using namespace std;
#define lowbit(x) (x)&(-x)
#define sqr(x) (x)*(x)
#define fir first
#define sec second
#define rep(i,a,b) for (register int i=a;i<=b;i++)
#define per(i,a,b) for (register int i=a;i>=b;i--)
#define maxd 1000000007
#define eps 1e-6
typedef long long ll;
const int N=5000;
const double pi=acos(-1.0);
int n;
ll fac[5050],invfac[5050],f[5050][5];
int read()
{
int x=0,f=1;char ch=getchar();
while ((ch<'0') || (ch>'9')) {if (ch=='-') f=-1;ch=getchar();}
while ((ch>='0') && (ch<='9')) {x=x*10+(ch-'0');ch=getchar();}
return x*f;
}
ll qpow(ll x,int y)
{
ll ans=1;
while (y)
{
if (y&1) ans=ans*x%maxd;
x=x*x%maxd;
y>>=1;
}
return ans;
}
int main()
{
fac[0]=1;invfac[0]=1;
rep(i,1,N) fac[i]=fac[i-1]*i%maxd;
invfac[N]=qpow(fac[N],maxd-2);
per(i,N-1,1) invfac[i]=invfac[i+1]*(i+1)%maxd;
n=read();f[1][0]=1;
rep(mx,1,n-1)
{
ll a=0;
rep(i,0,3) a=(a+f[mx][i])%maxd;
per(i,n,mx+1)
{
rep(j,1,4)
{
ll tmp=a;int k;
for (k=1;k<=j && mx*k<i;k++)
{
f[i][j]=(f[i][j]+f[i-k*mx][j-k]*tmp%maxd*invfac[k]%maxd)%maxd;
tmp=tmp*(a+k)%maxd;
}
}
}
}
ll ans=0;
rep(i,0,3) ans=(ans+f[n][i])%maxd;
printf("%lld",ans);
return 0;
}
無標號樹無根樹的計數
對沒錯它是烷烴計數
那么我們顯然會考慮以將一個點看做根,然后將其轉化成有根樹的計數
注意到樹的重心的優越性質:子樹的大小不超過\(\lceil\frac{n}{2}\rceil\),於是考慮將重心看做根,這樣枚舉上界變成了\(\frac{n}{2}\)
但是還要注意一個問題:當\(n\)為偶數的時候,有可能會有兩個重心的情況出現(比如一條鏈),對於這種情況,我們可以考慮將兩個大小為\(\frac{n}{2}\)且根節點度數不超過\(m-1\)的兩棵樹合並起來,於是方案數相比原來還要加上\(\dbinom{a_{\frac{n}{2}+2-1}}{2}\)
上面那道題的代碼如下,為了不寫高精度於是用了python
dp=[[0 for i in range(5)]for j in range(1010)]
dp[1][0]=1
n=int(input())
for mx in range(1,(n+1)//2):
a=0
for i in range(0,4):
a+=dp[mx][i]
for i in range(n,mx,-1):
for j in range(1,5):
tmp=a
fac=1
for k in range(1,j+1):
if (k*mx<i):
dp[i][j]+=dp[i-k*mx][j-k]*tmp//fac
fac*=(k+1)
tmp*=(a+k)
ans=0
for i in range(0,5) :
ans+=dp[n][i]
if n%2==0:
tmp=0
for i in range(0,4):
tmp+=dp[n//2][i]
ans+=(tmp+1)*tmp//2
print(ans)
參考資料:
prufer序列筆記(自為風月馬前卒):https://www.cnblogs.com/zwfymqz/p/8869956.html
無標號樹的計數原理(組合計數,背包問題,隔板法,樹的重心)(FlashHu):https://www.cnblogs.com/flashhu/p/9457830.html
