0. 前言
邦邦卡邦!又学会了新的双射方式!这次是关于树的双射内容!
1. 定义与构建
1.1 定义
prufer 序列,又叫做 prüfer 序列,因为键盘平时不太好打出来 ü 所以一般叫做 prufer 序列。他的作用就是可以将一个有标号的n 个点的树映射成一个由n−2 个在[1,n] 范围内的数所组成的序列,同时这个序列也会唯一对应一个树。也就是说,prufer 序列和树结构构成双射。
1.2 构建
钦定n 为树的根节点,因为这个树我没有说这是有根树,所以不会影响。
每次选择编号最小的叶子结点并删掉它,再把它的父亲的编号加入序列中。重复此操作,直到树上只有两个点为止。显然,这两个点中必有一个是编号最大的点 n。为什么是两个点呢?假设我们再进行一次操作,那么进入序列的必然是 n。这是没有意义的操作。而如果我们进行了这次操作,末尾不为 n 的 prufer 序列将失去对应一个树的优秀性质。
以下是一个n=7 的例子:

最终的序列就是{2,2,3,3,2}。
1.3 还原
那么可以看得出来,经过此操作,每个树都对应了一个 prufer 序列。如果我么能从 prufer 序列还原出树,那么就证明了树和 prufer 序列是一一对应的。
prufer 序列有一个性质如下:
- 树上每个点的度数等于在 prufer 序列上出现的次数加1。
这个性质很好想,因为度数要么来自儿子要么来自父亲贡献,没删掉一个儿子,这个点就会在 prufer 序列上出现一次。而众所周知的是树上一个节点有且仅有一个父亲,所以出现次数加1 就是度数。而对于根节点为n 则不一样,它在 prufer 序列中出现的次数为它的儿子数减1,即它的度数减1。
有了这个性质,我们就可以得知没有出现在 prüfer 序列上的点,一定是叶子结点。
这样我们轻易就能得到树上最小的叶子结点的编号,就是没有在 prüfer 序列上出现的点。我们将这个叶子结点与 prüfer 序列上的第一个数连边,然后删除这个点和 prüfer 序列上的第一个数。如果将编号大于该叶子结点的编号减一,我们就得到了一个长度为n−3 的 prüfer 序列,对应一个大小为 n−1 的树。因此可以使用同样的方法重复操作,再把最后剩下的点连向 n,就可以得到原树。
以下为{2,2,3,3,2} 的构造:
- 度数:dg[1,7]={1,4,3,1,1,1,1}。
- 取出1,令1→2,dg[1,7]={0,3,3,1,1,1,1}。
- 取出4,令4→2,dg[1,7]={0,2,3,0,1,1,1}。
- 取出5,令5→3,dg[1,7]={0,2,2,0,0,1,1}。
- 取出6,令6→3,dg[1,7]={0,2,1,0,0,0,1}。
- 取出3,令3→2,dg[1,7]={0,1,0,0,0,0,1}。
- prufer 序列遍历完,还剩下n=7 和2 号点,连接即可。
总连边:
可以自行验证,有结果:

故证明成立。
1.4 线性时间做到构造与还原
显然可以使用堆做到O(nlogn) 的复杂度,但其实有更优的做法。
维护一个下标p,初始值为最小的叶结点编号。重复以下操作:
- 删除编号为p 的结点,并检查是否使其父亲成为叶结点。
- 设其父亲的编号为x。先将x 加入序列。若x 成为了新的叶子结点,判断其与p 的大小关系。若x<p,则立即删除x,然后重复判断x 的父亲;否则不管。
- 使p 自增,直到p 指向一个叶子结点为止。
因为每条边最多被访问一次(在删点的时候访问父亲),指针最多遍历每个点一次,所以复杂度是O(n) 的。
这有点像可删堆的操作,可以结合理解。
对于还原类似,用同样的方法寻找编号最小的叶结点删除即可。
以下为 P6086 【模板】Prüfer 序列 的代码:
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
| #include<bits/stdc++.h> #define int long long using namespace std; constexpr int MN=5e6+15; int fa[MN],dg[MN],p[MN],n,m,ans;
void TtoP(){ for(int i=1;i<n;i++){ cin>>fa[i]; dg[fa[i]]++; } for(int i=1,x,j=1;i<=n-2;i++,j++){ while(dg[j]) j++; p[i]=fa[x=j]; while(i<=n-2&&!--dg[p[i]]&&p[i]<j){ p[++i]=fa[x=fa[x]]; } } for(int i=1;i<=n-2;i++){ ans^=1ll*i*p[i]; } }
void PtoT(){ for(int i=1;i<=n-2;i++){ cin>>p[i]; dg[p[i]]++; p[n-1]=n; } for(int i=1,x,j=1;i<=n-1;i++,j++){ while(dg[j]) j++; fa[x=j]=p[i]; while(i<=n-1&&!--dg[p[i]]&&p[i]<j) fa[x=fa[x]]=p[++i]; } for(int i=1;i<=n-1;i++){ ans^=1ll*i*fa[i]; } }
signed main(){ cin>>n>>m; if(m==1){ TtoP(); }else PtoT(); cout<<ans; return 0; }
|
1.5 性质总结
说了这么多,我们来总结几个关键的性质来辅助做题。
- Prufer 序列与树构成唯一双射,在计数问题中对一般树计数可以考虑直接对 prufer 序列计数。
- 树上每个点的度数等于在 prufer 序列上出现的次数加1。
- 所有没在序列里出现过的点,一定是树中的叶子。
- 对于完全图Kn 有nn−2 颗生成树。(任意一个长度为n−2 的值域[1,n] 的序列计数)
我们对第四个结论进行推广:
- n 个点形成有k 颗树的有标号无根树森林,使得给定的k 个点两两不属于同一棵树,此时的方案总数为 k⋅nn−k−1。
- 指定点度数的生成树方案为∏i=1n(di−1)(n−2)!。
- 若n 个点已经被连成大小为{si}i=1k 的k 个连通块,则在这些连通块之间加边构成生成树的方案数为nk−2∏i=1ksi。
推广 1 的证明:给定n 个点和k 个指定点,两两不在同一棵树的无根森林数:
- 每个指定点作为不同树的根,有根森林数nn−k。
- 无根森林对应有根森林,每棵树根可选择k 种,调整得到:
k⋅nn−k−1.
推广 2 的证明:指定n 点度数{di} 的生成树数:
- Prüfer 序列长度n−2。
- 每点vi 出现di−1 次。
- 多重排列计数:
∏i=1n(di−1)!(n−2)!.
推广 3 在后面例题会证明。
2. 例题
显然。
设si 为第i 个连通块的点数,di 表示连通块在树上的度数。
那么有 Prufer 序列方案数:
(d1−1)!(d2−1)!…(dn−1)!(k−2)!
而一个连通块连接边的方案为∏i=1ksidi。那么总方案数枚举di乘起来即为:
di>1,i=1∑kdi=2k−2∑(d1−1)!(d2−1)!…(dn−1)!(k−2)!i=1∏ksidi
设ei=di−1,有
ei>0,i=1∑kei=k−2∑e1!e2!…ek!(k−2)!i=1∏ksiei+1
考虑多元二项式定理:
(x1+⋯+xm)p=ci≥0,∑i=1mci=p∑(c1,c2,⋯,cmp)⋅i=1∏mxici
原式变为:
(s1+s2+⋯+sk)k−2⋅i=1∏ksi
即:
nk−2⋅i=1∏ksi
只给了一些点的度数,对于给定度数点的排列方案数也是可以算出来的,记sum=i=1∑n(di−1),令cnt 表示已知度数的点的个数。那么由上述推论 2 有:
(sumn−2)×∏i=1cnt(di−1)sum!
然后剩下的n−cnt 个数任意插在(n−sum−2) 的位置上即可,即(n−cnt)n−sum−2。答案就是乘起来即可,但是不给模数是有什么心事吗?我直接 python 喵了。
等会,题面这个图还有题目背景,莫非是?

咳咳,回到正题,首先看到 “恰好” 直接哈气。用二项式反演反演成至少,有:gk=i=k∑n(ki)fi⇔fk=i=k∑n(−1)i−k(ki)gi。其中f 为答案,g 为至少k 条边相同的方案数。
然后考虑g 怎么算,发现这玩意我们把钦定的i 条边断开,会形成n−i 个连通块,而这些连通块都是独立树计数的。根据 Prufer 定理有任意连边方案数:
nm−2i=1∏msi
m 为连通块个数,其中si 还是连通块大小。
然后考虑如何快速计算,发现如果直接做因为边不确定状压直接爆炸。数据范围O(n3),考虑发掘性质。
我们考虑把gk 的组合意义拆分,前式子可以随便计算,而后面却要求我们快速不用状压计算。考虑 DP,设f(i,j,k) 表示i 子树内,分成了j 个连通块,当前i 所在连通块大小为k 的方案数。时间复杂度O(n3) 可以O(1) 转移但是我认为是O(n4)→O(n5) 就很难泵。
但是我们显然有更好的做法,考虑我们只是在计算∏i=1msi,考虑复杂度瓶颈就是在于这个k 这一维度让我们的优化没有前途。考虑切换组合意义,发现∏i=1msi 的本质就是给每个连通块内部任意定根的方案数,把根是否确定放进状态中即可。
那么这很好考虑设f(i,j,0/1) 表示i 子树内,分成了j 个连通块,i 所在连通块是否定根的方案数,转移用背包对子树合并即可,时间复杂度O(n2)。