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;
}

学习笔记:Prufer 编码的更多相关文章

  1. MySQL学习笔记5——编码

    MySQL学习笔记5之编码 编码 1.查看MySQL数据库编码 *SHOW VARIABLES LIK 'char%'; 2.编码解释 *character_set_client:MySQL使用该编码 ...

  2. [学习笔记]prufer序列

    前言 PKUWC和NOIWC都考察了prufer序列,结果统统爆零 prufer序列就是有标号生成树对序列的映射 prufer序列生成 每次选择编号最小的叶子删掉,把叶子的父亲加入prufer序列,直 ...

  3. python学习笔记09-python编码与解码

    二进制编码: --->ASCII:只能存英文和拉丁字符 一个字符占一个字节:8位 ------>gb2312:只能存6700多个中文: 1980年发表 ----------->gbk ...

  4. Swift学习笔记 - URL编码encode与解码decode

    使用swift有一段时间了,api的变换造成了很多困扰,下面是关于url编码和解码问题的解决方案 在Swift中URL编码 在Swift中URL编码用到的是String的方法 func addingP ...

  5. IntelliJ IDEA 学习笔记 - 修改编码

    感谢原文作者:codeke 原文链接:https://blog.csdn.net/cgl125167016/article/details/78666432 仓库:https://github.com ...

  6. 学习笔记_Java_day14—编码实战___一个注册页面的完整流程

  7. 图论:Prufer编码

    BZOJ1211:使用prufer编码解决限定结点度数的树的计数问题 首先学习一下prufer编码是干什么用的 prufer编码可以与无根树形成一一对应的关系 一种无根树就对应了一种prufer编码 ...

  8. prufer编码学习笔记

    prufer 编码 对于一个无根树,他的 prufer 编码是这样确定的: 每次找到编号最小的一个叶子节点,也就是度数为\(1\)的节点,把和它相连的点,加入 prufer 编码序列的末尾,然后把这个 ...

  9. [原创]java WEB学习笔记45:自定义HttpFilter类,理解多个Filter 代码的执行顺序,Filterdemo:禁用浏览器缓存的Filter,字符编码的Filter,检查用户是否登陆过的Filter

    本博客为原创:综合 尚硅谷(http://www.atguigu.com)的系统教程(深表感谢)和 网络上的现有资源(博客,文档,图书等),资源的出处我会标明 本博客的目的:①总结自己的学习过程,相当 ...

随机推荐

  1. 流编辑器:sed

    一 简介:sed是一个精简的.非交互式的流式编辑器,它在命令行中输入编辑命令和指定文件名,然后在屏幕上查看输出.逐行读取文件内容到临时缓冲区,称为模式空间.接着用sed命令处理缓冲区内容,处理完之后, ...

  2. 信号发送接收函数:sigqueue/sigaction

    信号是一种古老的进程间通信方式,下面的例子利用sigqueue发送信号并附带数据:sigaction函数接受信号并且处理时接受数据. 1.sigqueue: 新的信号发送函数,比kill()函数传递了 ...

  3. 'sortbitwise'是什么意思

    问题 flag sortbitwise 在ceph中是什么意思,在Jewel版本下可以看到多了这个flags [root@lab8106 current]# ceph -s cluster ffe7a ...

  4. [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain (_ssl.c:1076)'))) - skipping

    C:\Users>pip listPackage Version------------ -------behave 1.2.6configparser 3.7.4ddt 1.2.1parse ...

  5. [原题复现][极客大挑战 2019]BuyFlag

    简介  原题复现:[极客大挑战 2019]BuyFlag  考察知识点:php函数特性(is_numeric().strcmp函数())  线上平台:https://buuoj.cn(北京联合大学公开 ...

  6. 给PDF文件创建书签,实现快速导航

    当文档中的页码比较多的情况下,使用目录进行导航是一个很好用的方法,为文档内容制作目录,方便快速查找目标内容.除了内容的快速导航,书签还能指明不同书签的层级关系,展现文档的结构. 图1:书签的功能 一. ...

  7. 如何利用FL Studio进行音乐合并

    FL Studio20是Fruity Loops Studio的简称,也叫做水果音乐制作软件.它是一款功能十分强大的音乐制作软件,将作曲.编曲.混音.录音.大碟等功能集合一体,外接MIDI即可成为一个 ...

  8. 教您使用OCR编辑器复制文档内容

    ABBYY FineReader 15允许用户复制图像或者扫描页面上的内容,可复制其中的文本.图片和表格的信息.在复制过程中,用户无需将图像或扫描页面转换为可编辑的格式,可以直接在ABBYY Fine ...

  9. 在Mac上也能轻松拥有Windows应用程序的简便方法

    一般而言,如果我们想要在Windows的环境下下载一款软件那是件很方便的事情.只要我们登陆软件的官网进行下载即可.但是如果我们使用的是Mac OS系统,很多用户就会发现,许多软件会出现不兼容的情况. ...

  10. 牛逼哄哄的PageHelper分页插件到底是怎么实现的?网友:给我10分钟,给你写一个~

    Hi,各位读者们 PageHelper是一款好用的开源免费的Mybatis第三方物理分页插件,其实我并不想加上好用两个字,但是为了表扬插件作者开源免费的崇高精神,我毫不犹豫的加上了好用一词作为赞美. ...