[BZOJ 1005] 明明的烦恼 && [BZOJ 1211] 树的计数【组合数学】

—————————————————————————————————
HNOI 2008 明明的烦恼

Description
自从明明学了树的结构,就对奇怪的树产生了兴趣...... 给出标号为1到N的点,以及某些点最终的度数,允许在任意两点间连线,可产生多少棵度数满足要求的树?



Input
第一行为N(0 < N < = 1000),接下来N行,第i+1行给出第i个节点的度数Di,如果对度数不要求,则输入-1

Output
一个整数,表示不同的满足要求的树的个数,无解输出0

Sample Input
3
1
-1
-1

Sample Output
2

HINT 两棵树分别为1-2-3;1-3-2
—————————————————————————————————
一道比较简单的组合数学题。

首先我们要了解一下Prufer Sequence(普吕弗序列)(内容摘自zyj PPT).

Prufer Sequence是一个和某棵结点数为n的树唯一对应的一个长度为n-2的整数序列。定义如下:

假定已知的n个顶点标志为1,2,...,n,假定T是其中的一棵树。设a1为叶节点中有标号最小者,与a1连接的点为b1,从T中消去点a1和边(a1,b1),再从余下的数T1中寻找标号最小的叶节点,设为a2,a2的邻接点为b2,从从T1中消去点a2和边(a2,b2)。如此步骤n-2次,直到最后剩下一条边为止。于是一棵树T对应一序列:b1,b2,...,bn-2
这些数是1~n中的数,并且允许重复。

相反地,用b1,b2...bn-2可以恢复树T本身,因为消去的是树叶中标号最小的,而且它和b1是邻接的。
即给出一序列b1,b2,...,bn-2,其中1<=bi<=n,i=1,2,...,n-2.可恢复与之对应的树,方法如下:
有两个序列,一个是(1)1,2,....,n,另一个是(2)b1,b2,...,bn-2
在序列(1)中找出第一个不出现在序列(2)b1,b2,...,bn-2中的数,这个数显然便是a1,同时形成边(a1,b1),并从(1)中消去a1,从(2)中消去b1。在余下的(1)和(2)中继续以上的步骤n-2次,直到序列(2)为空集为止。这时序列(1)中剩下的两个数x,y,边(x,y)就是树T的最后一条边。

看不懂? 来看个栗子

树T


找编号最小的节点,为2,与他相连的为3,将3加入序列


消掉点2和边(2,3),找到当前编号最小的节点,为3,与他相连的为1,将1加入序列



重复这个操作




... 直到最后得到这个序列



同样,我们用这个序列恢复这棵树

现有(1,7)之前并未消去


开始操作












这样很容易看出,每一个Prufer Sequence对应一颗唯一的树(一一对应)。

—————————————————————————————————

回到题目中来。

我们发现,树中结点的度和结点号在此序列中出现的次数有关(结点i在序列中出现的次数=degree[i]-1).

由题意,有题目给出度数的限制,同时也就限制了序列的一些条件。树的所有可能情况也就是序列可能的情况。

设度数没有限制的点的数量为num,有限制的点在序列中出现的总次数为sum

可知,n-num个没有限制的点在序列中的排列总情况为
(从n-2个位置选sum个位置填充这些数*排列方式为多重集的全排列方案数)

剩下n-sum-2 个位置可由num个数填充,方案数为



所以答案为



因为可能爆long long,用分解质因数求以下就可以了。

然后答案也比较畸形,要用高精度,好像要开3000位,不然就神作了。

注意当任意degree=0时是无解的。

另外,当sum>n-2同样也是无解的。

注意特判n=1的情况,if(degree[1]=0) ans=1, else ans=0;

然后就可以愉快的AC辣--

附一段代码



#include<cstdio>
#include<cctype>

inline int readint(){
    int x=0; char c=getchar();  bool minu=false;
    while(!isdigit(c) && c!= '-') c=getchar(); 
    if(c=='-') minu=true; else x=c-'0';
    while(isdigit(c=getchar())) x=(x*10)+c-'0';
    return minu?-x:x;
}

const int maxn=1000+10;
int degree[maxn];

int form[maxn],prime[maxn],facnum[maxn],cnt;
void MakePrimeList(int n){                          
    for(int i=2;i<=n;i++) if(!form[i]){
        prime[cnt++]=i;
        for(int j=i*i;j<=n;j+=i) form[j]=true;
    }
}

long long pow(long long a,int n){                       //递推快速幂
    long long ans=1;
    while(n){
        if(n&1) ans*=a;
        a*=a; n>>=1;
    }
    return ans;
}
void fac(int x,int t){                                  //分解质因数
    if(!x || !t) return;
    for(int i=0;i<cnt;i++)
        while(!(x%prime[i])) facnum[i]+=t,x/=prime[i];
}

long long ans[3000]={1},digit=1;
void count(){
    for(int i=0;i<cnt;i++) if(facnum[i]){
        int x=0;
        for(int k=0;k<facnum[i];k++){
            for(int j=0;j<digit;j++) ans[j]*=prime[i];
            for(int j=0;j<digit;j++){
                ans[j]+=x;
                x=ans[j]/10;
                ans[j]%=10;
            }
        while(x) ans[digit++]=x%10,x/=10;
        }
    }
}

int main(){
    int n=readint(),sum=0,num=0;
    MakePrimeList(maxn);
    if(n==1){
        if(readint()) putchar('0');  else putchar('1');
        return 0;
    }
    for(int i=0;i<n;i++){
        degree[i]=readint();
        if(!degree[i]) { printf("0"); return 0; }
        else if(degree[i]==-1) num++;
        else sum+=degree[i]-1;
    }
    if(sum>n-2) { printf("0"); return 0; }
    fac(num,n-sum-2);
    for(int i=n-sum-1;i<=n-2;i++) fac(i,1);
    for(int i=0;i<n;i++) if(degree[i]!=1)
        for(int j=2;j<degree[i];j++) fac(j,-1);
    count();
    for(int i=digit-1;i>=0;i--) putchar(ans[i]+'0');
    return 0;
}


然后1211就是简化了的题目了(建议先完成)

--By Foggy
2015.6.22

此博客中的热门博文

图论专题总结