BZOJ 1005/1211 【组合数学】

HNOI2008 明明的烦恼

[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

[Solution]

一道比较简单的组合数学题。

prufer编码详解

首先我们要了解一下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个没有限制的点在序列中的排列总情况为

$$C(n-2,sum)\frac{sum!}{\prod_{i=1}^{n-sum}(degree[i-1])!}$$

(从n-2个位置选sum个位置填充这些数*排列方式为多重集的全排列方案数)
剩下n-sum-2 个位置可由num个数填充,方案数为

$$num^{n-sum-2}$$

所以答案为
$$C(n-2,sum)\frac{sum!}{\prod_{i=1}^{n-sum}(degree[i-1])!}num^{n-sum-2} $$

$$ =\frac{(n-2)!}{sum!(n-sum-2)!}\frac{sum!}{\prod_{i=1}^{n-sum}(degree[i-1])}num^{n-sum-2} $$

$$= \frac{\prod_{i=n-sum-1}^{n-2}i}{\prod^{i=1\ to\ n-num}(degree[i]-1)!}num^{n-sum-2}$$

因为可能爆long long,用分解质因数求以下就可以了。
然后答案也比较畸形,要用高精度,好像要开3000位,不然就神作了。
注意当任意degree=0时是无解的。
另外,当sum>n-2同样也是无解的。
注意特判n=1的情况,if(degree[1]=0) ans=1, else ans=0;
然后就可以愉快的AC辣–
附一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#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;
}

BZOJ1211 树的计数

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