树的计数问题-Prufer 序列
问题引入
设一颗树有 n n n个节点,分别是 v 1 , v 2 , … , v n v_{1},v_{2},\ldots,v_{n} v1,v2,…,vn,其度分别为 d 1 , d 2 , … , d n d_{1},d_{2},\ldots,d_{n} d1,d2,…,dn,问构成这样的无根树总共有多少种?
Prufer 序列
很明显,在一颗树上用排列组合定理是相当困难的,我们能不能把一颗无根树转换成对应的唯一一个序列,即让一个序列与一个 带标号的无根树(LUT) 一一对应,构成双射,然后就可以对线性的序列使用排列组合定理,对线性的序列使用排列组合定理这是容易的。答案就是Prufer 序列。
带标号的无根树(LUT):我们对一颗节点数为 n n n的无根树对其节点进行标号,每一个节点都有唯一一个标号(从1-n),叫做带标号的无根树。
无根树转Prufer 序列
我们通过下列几个步骤将一颗无根树转换为Prufer 序列。
- 初始化 Prufer 序列为 { } \{\} {}
- 在树中选择一个节点,其度为 1 1 1,且节点索引尽量小。将这个节点的父节点加入到Prufer 序列中,并且删除这个节点。
- 一直重复下去,直到无根树中还剩两个节点。
此时生成的序列就是这颗无根树的Prufer 序列。并且这个序列的长度是 n − 2 n-2 n−2。
举例来说:
步数 | 选择节点 | Prufer序列 |
---|---|---|
1 | 4 | 2 |
2 | 5 | 2,3 |
3 | 3 | 2,3,1 |
4 | 6 | 2,3,1,2 |
5 | 2 | 2,3,1,2,1 |
最终得到的Prufer序列为 { 2 , 3 , 1 , 2 , 1 } \{2,3,1,2,1\} {2,3,1,2,1},树中的节点剩下1和7。这颗树就和序列 { 2 , 3 , 1 , 2 , 1 } \{2,3,1,2,1\} {2,3,1,2,1}一一对应。
Prufer 序列转无根树
Prufer 序列转无根树的过程类似于上述过程的逆过程,根据Prufer的性质,每个节点的出现次数都是度数-1,我们每次可以选取最小度数为1的节点:
- 初始化点集合 V V V为全体节点
- 在 V V V中取一个点 u u u,满足 u u u不在Prufer 序列中且 u u u的索引最小。与当前Prufer 序列的第一个元素 v v v,在 u u u与 v v v之间添加一条边。并弹出Prufer 序列的首元素,并且在 V V V中删除节点 u u u。
- 重复上述过程,直到Prufer 序列为空,此时 V V V还剩下两个节点,在这两个节点之间添加一条边,此时生成的树就是原来的无根树。
举例来说:
步数 | V V V | u u u | v v v | Prufer序列 |
---|---|---|---|---|
1 | 1,2,3,5,6,7 | 4 | 2 | 3,1,2,1 |
2 | 1,2,3,6,7 | 5 | 3 | 1,2,1 |
3 | 1,2,6,7 | 3 | 1 | 2,1 |
4 | 1,2,7 | 6 | 2 | 1 |
5 | 1,7 | 2 | 1 | ϕ \phi ϕ |
最后添加边 1 → 7 1 \to 7 1→7结束,可见,最后一次添加的边一定是最大的节点和Prufer序列最后一个节点相连的边。
因此我们看,选择最小的元素是为了能和逆过程对应上,我们也可以选择其他条件,只要能和逆过程对应上即可。
模板
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define FR freopen("in.txt", "r", stdin)
#define FW freopen("out.txt", "w", stdout)
int fa[5010110];
int deg[5010110];
int prufer[5010110];
int n, m;
ll read()
{
ll cnt = 0, flag = 1;
char c = getchar();
while (c < '0' || c > '9')
{
if (c == '-')
flag = -1;
c = getchar();
}
while (c >= '0' && c <= '9')
cnt = (cnt << 1) + (cnt << 3) + (c ^ 48), c = getchar();
return flag * cnt;
}
void prufer_code()
{
// count degree
for (int i = 1; i <= n - 1; i++)
{
deg[i]++;
deg[fa[i]]++;
}
int ptr = -1;
// find the node
for (int i = 1; i <= n; i++)
{
if (deg[i] == 1)
{
ptr = i;
break;
}
}
int leaf = ptr;
for (int i = 0; i < n - 2; i++)
{
prufer[i] = fa[leaf];
deg[leaf]--;
deg[fa[leaf]]--;
if (deg[fa[leaf]] == 1 && fa[leaf] < ptr)
{
leaf = fa[leaf];
}
else
{
while (deg[ptr] != 1)
{
ptr++;
}
leaf = ptr;
}
}
}
void prufer_decode()
{
// count degree
for (int i = 0; i < n - 2; i++)
{
deg[prufer[i]]++;
}
for (int i = 1; i <= n; i++)
{
deg[i]++;
}
int ptr = -1;
for (int i = 1; i <= n; i++)
{
if (deg[i] == 1)
{
ptr = i;
break;
}
}
int leaf = ptr;
for (int i = 0; i < n - 2; i++)
{
fa[leaf] = prufer[i];
deg[leaf]--;
deg[prufer[i]]--;
if (deg[prufer[i]] == 1 && prufer[i] < ptr)
{
leaf = prufer[i];
}
else
{
while (deg[ptr] != 1)
{
ptr++;
}
leaf = ptr;
}
}
fa[prufer[n - 3]] = n;
}
int main()
{
n = read();
m = read();
if (m == 1)
{
for (int i = 1; i <= n - 1; i++)
{
fa[i] = read();
}
prufer_code();
long long ans = 0;
for (int i = 0; i < n - 2; i++)
{
ans ^= (i + 1ll) * prufer[i];
}
printf("%lld", ans);
}
else
{
for (int i = 0; i < n - 2; i++)
{
prufer[i] = read();
}
prufer_decode();
long long ans = 0;
for (int i = 1; i <= n - 1; i++)
{
ans ^= ((long long)i) * fa[i];
}
printf("%lld", ans);
}
return 0;
}
Prufer 序列计数
有了Prufer 序列我们就可以排列组合了,我们借助如下几个性质进行计算:
- 无根树和Prufer 序列一一对应,构成双射。
- 度数为 d i d_{i} di的节点会在Prufer 序列出现 d i − 1 d_{i}-1 di−1次,即为节点的子节点的个数。
- Cayley公式:一个完全图有 n n n个节点,其生成树的个数有 n n − 2 n^{n-2} nn−2。Prufer 序列有 n − 2 n-2 n−2个位置,每个位置都有 n n n种可能。
- 对于给定的节点的度,无根树的个数有:
A ( n − 2 ) ∏ i = 1 n A ( d i − 1 ) \frac{A(n-2)}{\prod_{i=1}^{n}A(d_{i}-1)} ∏i=1nA(di−1)A(n−2)
即先对序列进行全排列,然后除以相同元素的全排列即可。
#include <bits/stdc++.h>
#define MAX_SIZE 1000
using namespace std;
typedef long long ll;
struct Decimal
{
int fact[MAX_SIZE];
Decimal()
{
for (int i = 0; i < MAX_SIZE; i++)
fact[i] = 0;
}
void set(int num)
{
int fa = 2;
while (num > 1)
{
int cnt = 0;
while (num % fa == 0)
{
num /= fa;
cnt++;
}
if (cnt != 0)
{
fact[fa] = cnt;
}
fa++;
}
}
void operator*=(const Decimal &a)
{
for (int i = 0; i < MAX_SIZE; i++)
{
fact[i] += a.fact[i];
}
}
void operator/=(const Decimal &a)
{
for (int i = 0; i < MAX_SIZE; i++)
{
fact[i] -= a.fact[i];
}
}
};
ostream &operator<<(ostream &out, const Decimal &d)
{
ll ans = 1;
for (int i = 0; i < MAX_SIZE; i++)
{
for (int cnt = 0; cnt < d.fact[i]; cnt++)
{
ans *= i;
}
}
out << ans;
return out;
}
int main()
{
int n;
cin >> n;
// 特判n=1的情况
if (n == 1)
{
int deg;
cin >> deg;
if (deg == 0)
cout << "1";
else
cout << "0";
return 0;
}
// 阶乘准备
Decimal factorial[151];
for (int i = 1; i <= n; i++)
{
factorial[i].set(i);
factorial[i] *= factorial[i - 1];
}
// 初始化答案为(n-2)!
Decimal ans = factorial[n - 2];
int cnt = 0;
for (int i = 0; i < n; i++)
{
int deg;
cin >> deg;
// 如果度为0,图不连图,不是树,无解
if (deg == 0)
{
cout << "0";
return 0;
}
// 计数,除掉相同元素的排列
cnt += deg - 1;
ans /= factorial[deg - 1];
}
// 判断是否成立
if (cnt == n - 2)
{
cout << ans;
}
else
{
cout << "0";
}
return 0;
}