《训练指南》中的第二种算法,其实本质上就是个背包。d[i][j]表示,在子树的节点数最大为i的情况下,j个节点的解。当之前的i-1,i-2,....0的结果都已知的时候,d[i][j]自然可根据下式求解:
d[i][j]=sum{C(f(i)+p-1,p)*d[i-1][j-p*i] | p*i<=j}
其中f(i)表示恰好有i个节点的子树的数量。而C(f(i)+p-1,p)则表示有p棵i节点子树形成的组合数。然后运用乘法原理,剩余的节点数为j-p*i且包含的子树最多只有i-1个节点。继而从i=2时开始遍历,将所有的d[i][j]求出,而f(i)=d[i-1][i]。最后的解f(n)自然等于d[n-1][n]。
还要注意的一点是边界的问题。当d[i][j]中的j=0时,则d[i][0]=1,这与求解背包时的边界相似。d[0][1]=1,表示叶子节点。而其他的d[0][i]则都为0,因为有i(i>1)个节点,但没有子树的最大节点数为0,显然是不可能的。最后,在输出结果时,f(1)始终为1,但其他的f(i)要乘以2,因为正如书中所说,根节点为并联节点或串联节点将决定两个不同的序列。
注:关于组合数的求解为什么能这样做,希望有高手能解答!
#include <iostream>
#include <cstdio>
#define MAX 30+2
using namespace std;
long long d[MAX][MAX];
long long f[MAX];
long long C(long long n,long long m)//求解组合数C(n,m)
{
double ans=1;
for(int i=0;i<m;++i) ans*=n-i;
for(int i=1;i<=m;++i) ans/=i;
return (long long)(ans+0.5);
}
int main()
{
//freopen("data.txt","r",stdin);
for(int i=0;i<MAX;++i) d[i][0]=d[1][i]=1;//注意边界
for(int i=2;i<MAX;++i) d[0][i]=0;
d[0][1]=1;
for(int i=2;i<MAX;++i){
f[i]=d[i-1][i];
for(int j=1;j<i;++j) d[i][j]=d[i-1][j];//最大子树的节点数大于总节点数,则继承
for(int j=i;j<MAX;++j)
for(int p=0;p*i<=j;++p)
d[i][j]+=C(f[i]+p-1,p)*d[i-1][j-p*i];
}
f[1]=1;
for(int i=2;i<MAX;++i) f[i]*=2;//修改f(i)
int N;
while(cin>>N&&N){
cout<<f[N]<<endl;
}
return 0;
}