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. pandas.DataFarme内置的绘图功能参数说明

    可视化是数据探索性分析及结果表达的一种非常重要的形式,因此打算写一个python绘图系列,本文是第一篇,先说一下pandas.DataFrame.plot()绘图功能. pandas.DataFram ...

  2. MySQL全面瓦解12:连接查询的原理和应用

    概述 MySQL最强大的功能之一就是能在数据检索的执行中连接(join)表.大部分的单表数据查询并不能满足我们的需求,这时候我们就需要连接一个或者多个表,并通过一些条件过滤筛选出我们需要的数据. 了解 ...

  3. matlab 向量操作作业

    写出下列语句的计算结果及作用 clear    清除所有变量 clc    清屏 A = [2 5 7 1 3 4];    创建行向量并赋值 odds = 1:2:length(A);    冒号操 ...

  4. [LeetCode题解]141. 环形链表 | 快慢指针

    题目描述 给定一个链表,判断链表中是否有环. 如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环. 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的 ...

  5. Perfview 分析进程性能

    PerfView 概述: PerfView是一个可以帮助你分析CPU和内存问题的工具软件.它非常轻量级也不会入侵诊断的程序,在诊断过程中对诊断的程序影响甚微. Visual Studio自带的性能分析 ...

  6. bootstrap-datetimepicker的两种版本

    1.引入js/css <link rel="stylesheet" th:href="@{/plugin/bootstrap-datetimepicker/boot ...

  7. 面试官:别的我不管,这个JVM虚拟机内存模型你必须知道

    前言 说jvm的内存模型前先了解一下物理计算机的内存处理. 物理计算器上用户磁盘和cpu的交互,由于cpu读写速度速度远远大于磁盘的读写速度速度,所以有了内存(高速缓存区).但是随着cpu的发展,内存 ...

  8. 深度分析:Java并发编程之线程池技术,看完面试这个再也不慌了!

    线程池的好处 Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池.在开发过程中,合理地使用线程池,相对于单线程串行处理(Serial Processing ...

  9. 教你在CorelDRAW中制作水印

    水印是一种数字保护的手段,在图像上添加水印即能证明本人的版权,还能对版权的保护做出贡献.也就是在图片上打上半透明的标记,因其具有透明和阴影的特性,使之不管在较为阴暗或明亮的图片上都能完美使用,嵌入的水 ...

  10. 安装git和lsof

    yum install git yum install lsof 查看80端口 lsof -i:80