Prufer 编码可以将无根树与序列之间进行转化。


一个 \(n\) 个点、区分编号的无向图 和 Prufer 序列一定是一一对应的,下面会给出映射方式。

借此可以证明 Cayley 定理: \(n\) 个点的无根、区分编号生成树个数为 \(n ^ {n-2}\)

无根树转序列

设一棵 \(n\) 个节点的无根树,写出转化为 Prufer 序列的步骤:

  1. 找到编号最小的叶节点 \(x\),把 \(x\) 相邻的点加入序列,然后,删掉 \(x\)
  2. 当点数 \(= 2\) 时,终止(若不终止这次输出的一定是 \(n\) 所以是确定信息 # 无区分信息),否则继续迭代。

所以 Prufer 序列长度为 \(n - 2\)。

依此我们可以看出一个性质

考虑 \(x\) 在 Prufer 序列中出现的次数 + 1: \(cnt_x + 1\) 即 \(x\) 的度数


显然可以 \(O(n \log n)\) 用堆做。

也有线性 \(O(n)\) 的做法:

  1. 从小到大枚举哪个选择的点 \(j\)

  2. 每次删除,至多会多出来一个待选叶子点 \(x\)。

    • 若没有多出来叶子点 / \(x > j\),那么继续增大 \(j\)

    • 否则,递归继续删除 \(x\) 即可。

序列转无根树

步骤:

考虑 \(x\) 在序列中出现的次数 + 1: \(cnt_x + 1\) 即 \(x\) 的度数,所有 \(cnt_x = 1\) 的 \(x\) 加入备选集合。

  1. 按顺序考虑序列每一项 \(x\)
  2. 从备选集合中找到编号最小的 \(y\),连边 \((x, y)\)。
  3. 减少 \(cnt_x \gets cnt_x - 1\),若减到 \(1\),把 \(x\) 加入备选集合。

复杂度也可以做到 \(O(n)\),和上述方法类似。

想法

后来突然有一个疑问:为什么转化后的 Prufer 是唯一的呢?

后来百思不得其解后发现自己钻牛角尖了,如上两步,我们给出了:

  • Prufer 序列转为唯一确定形态无根树的方法
  • 无根树转化为唯一确定 Prufer 序列的方法

那么无根树和 Prufer 序列就是一一对应的了。

例题题

模板题

AcWing 2419. prufer序列

注意,这里规定了 \(n\) 为根,那么显然更方便了一点,一定是从叶子往上删:

  • \(cnt_x\) 为 \(x\) 的儿子数
  • \(cnt_x = 0\) 加入备选集合

就可以了。

#include <iostream>
#include <cstdio> using namespace std; const int N = 100005; int n, m, f[N], p[N], d[N]; void inline fToP() {
for (int i = 1; i < n; i++) d[f[i]]++;
for (int i = 1, j = 1; i <= n - 2; j++) {
while (d[j]) j++;
p[i++] = f[j];
while (i <= n - 2 && --d[p[i - 1]] == 0 && p[i - 1] < j) p[i++] = f[p[i - 1]];
}
} void inline pToF() {
for (int i = 1; i <= n - 2; i++) d[p[i]]++;
p[n - 1] = n;
for (int i = 1, j = 1; i < n; i++, j++) {
while (d[j]) j++;
f[j] = p[i];
while (i < n - 1 && --d[p[i]] == 0 && p[i] < j) f[p[i]] = p[i + 1], ++i;
}
} int main() {
scanf("%d%d", &n, &m);
if (m == 1) {
for (int i = 1; i < n; i++) scanf("%d", f + i);
fToP();
for (int i = 1; i <= n - 2; i++) printf("%d ", p[i]);
} else {
for (int i = 1; i <= n - 2; i++) scanf("%d", p + i);
pToF();
for (int i = 1; i < n; i++) printf("%d ", f[i]);
}
return 0;
}

例题:光之大陆

AcWing 2418. / BZOJ2873 光之大陆

做完这题就感觉计数特别玄学,问题出在网上所有题解都认为旋转算不同方案,需要 \(\times k\)。但我觉得不需要,因为映射的时候已经选择了对应的编号,想了 3 天,发现这一点网上的题解的理解好像都是错的。。

遂发题解。

或者说,我们做题的都是自己构造了一个自己认为正确的“广义” Prufer 序列,但并没有考虑具体映射关系,编号和缩点后编号的是否映射到位,所以计数不可避免的出现一些重复、缺漏的部分,但是阴差阳错的就对了。。

这题就是求 \(n\) 个点的区分编号、联通点仙人掌图数量。

我们可以考虑把 \(n\) 分成若干简单环,然后考虑把他们连起来的方案数。

考虑把每个简单环缩点,设缩点后有 \(j\) 个点,设 \(c[x]\) 为 \(x\) 缩点后属于的编号。

那么对于 \(j\) 个缩点的图,有多少种生成树个数呢?

考虑 Prufer 编码,稍稍做一点改动,考虑每次选择的点是 \(n\) 个,需要连 \(j - 1\) 条边,所以是 \(n ^ {j - 2}\)。这样为什么是正确的呢?考虑构造一个类似的映射方式,在标准 Prufer 编码映射上的改动:

  • 在有关度数的所有操作,把 \(x\) 当作 \(c[x]\) 进行相关操作
  • 在有关生成 Prufer 序列,连接父亲儿子边这些操作,用原始编号。

这样的话我们发现映射的时候,如果当作一个以 \(n\) 为根的有根树,对于一条边而言,映射出了这条边父亲的缩点前的具体编号与儿子缩点后的具体编号(这个可以考虑 Prufer 映射的过程,连边在这种特殊的映射中 \((x, y)\), \(y\) 实际上是缩点后的编号),所以对于每条边,我们还要计数选择这条边儿子连接的具体是 \(y\) 这个缩的点连接的是这个环中具体的哪个点(选择一个接口),即除了 \(n\) 所在的那个环,其他的都要从中选一个点作为接口 。并且,我们发现 Prufer 编码没有确定最后一条边,即父亲为 \(n\) 缩点后所在的的那条边的父亲的具体编号(原始的 Prufer 编码缩点后的节点是根不需要连父亲边,但考虑到我们特殊的 Prufer,最后只能保证从 \(n\) 号节点连边,没有选择 \(n\) 号节点缩点后的点对应着缩点前的哪个点,所以 \(n\) 对应的环也要从中选一个),设每个缩点的环大小是 \(sz\),答案应该是 \(n^{j - 2} \times \prod sz\)

后面 \(sz\) 的乘积,我们可以在把分成环的时候把贡献送进去。

所以 \(f_{i, j}\) 实际上是把 \(i\) 个点的仙人掌缩点后分成 \(j\) 个点,再从每个环里面选出一个点作为接口连接父亲边,的方案数。

所以这个 \(\times k\) 实际上并不是网上流传的朝向本质不同,而是广义 Prufer 计数留下的历史遗漏问题,缩点后编号和原始编号映射没有映射全,需要额外增加计数导致的。

答案就是 \(\sum_{i=1}^n f_{n, i} \times n^{i - 2}\)。

考虑求解 \(f_{i, j}\),可以类比 AcWing 307. 连通图 的方式,枚举基准点 \(i\) 的联通块大小 \(k\)。

\[f_{i, j} = \sum_{k=1}^i f_{i-k, j - 1} \times C_{i - 1}^{k - 1} \times g_k \times k
\]

这个 \(g_i\) 表示大小为 \(i\) 的环的方案数:

\(g_i = \begin{cases} 0,\ i=2 \\ \frac{(i-1)!}{2}, \text{otherwise} \end{cases}\)

不存在大小为 \(2\) 的环,\(i=1\) 默认是一个点,在这个题里环旋转、翻转本质相同。

注意 \(i = 1\) 的时候特判,贡献即 \(\frac{(n-1)!}{2}\)

这个东西在预处理组合数后可以 \(O(n ^3)\) 做,这题就做完了。

#include <iostream>
#include <cstdio> using namespace std; const int N = 205; typedef long long LL; int n, P, f[N][N], fact[N], C[N][N]; int main() {
scanf("%d%d", &n, &P);
f[0][0] = C[0][0] = 1;
fact[1] = 1, fact[3] = 3;
for (int i = 4; i <= n; i++) fact[i] = fact[i - 1] * i % P;
for (int i = 1; i <= n; i++) {
C[i][0] = 1;
for (int j = 1; j <= i; j++)
C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % P;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) {
for (int k = 1; k <= i; k++) {
f[i][j] = (f[i][j] + (LL)f[i - k][j - 1] * C[i - 1][k - 1] % P * fact[k]) % P;
}
}
}
int ans = fact[n - 1], s = 1;
for (int i = 2; i <= n; i++, s = s * n % P) ans = (ans + (LL)f[n][i] * s) % P;
printf("%d\n", ans);
return 0;
}

最新文章

  1. iOS RESideMenu 侧滑 第三方类库
  2. 微信小程序 不在以下合法域名列表中,请参考文档:https://mp.weixin.qq.com/debug/wxadoc/dev/api/network-request.html
  3. 搭建CnetOS6.5x64最小系统及在线yum源的配置
  4. [HTML5]a标签禁止嵌套使用
  5. 基于tiny4412的Linux内核移植 -- 设备树的展开
  6. 制作支持UEFI PC的Server2008 R2系统安装U盘
  7. jQuery选择器之全面总结
  8. php提取背景图片
  9. 经典union的使用
  10. DirectX:函数连接两个随机filter
  11. 怎么给当前点击的a标签添加一个样式(跳转页面后)
  12. c++中string的用法
  13. .net 正则获取url参数
  14. 痞子衡嵌入式:如果i.MX RT是一匹悍马,征服它时别忘了用马镫MCUBootUtility
  15. 数据库——MongoDB的安装
  16. 【二次开发】shopxo商城
  17. Android学习之基础知识一
  18. HDU 4292 Food (网络流,最大流)
  19. winform 中 给DataGridView的表头添加CheckBox
  20. Cordova 微信分享插件,安卓亲测可用

热门文章

  1. Golang 接口型函数和http.Handler接口
  2. 重构rbd镜像的元数据
  3. 老板让只懂Java基本语法的我,基于AQS实现一个锁
  4. vue 常见记录
  5. 记php多张图片 合并生成竖列 纵向长图(可用于商品详情图合并下载)
  6. Guitar Pro7应该怎么添加音色
  7. 关于ABBYY的常见问题与解答
  8. 利用perspective 和 transform 里面的几个参数来实现旋转照片墙
  9. php-fpm和nginx配置
  10. Sysbench对Mysql进行基准测试