学习笔记:Prufer 编码
Prufer 编码可以将无根树与序列之间进行转化。
一个 \(n\) 个点、区分编号的无向图 和 Prufer 序列一定是一一对应的,下面会给出映射方式。
借此可以证明 Cayley 定理: \(n\) 个点的无根、区分编号生成树个数为 \(n ^ {n-2}\)
无根树转序列
设一棵 \(n\) 个节点的无根树,写出转化为 Prufer 序列的步骤:
- 找到编号最小的叶节点 \(x\),把 \(x\) 相邻的点加入序列,然后,删掉 \(x\)
- 当点数 \(= 2\) 时,终止(若不终止这次输出的一定是 \(n\) 所以是确定信息 # 无区分信息),否则继续迭代。
所以 Prufer 序列长度为 \(n - 2\)。
依此我们可以看出一个性质
考虑 \(x\) 在 Prufer 序列中出现的次数 + 1: \(cnt_x + 1\) 即 \(x\) 的度数
显然可以 \(O(n \log n)\) 用堆做。
也有线性 \(O(n)\) 的做法:
从小到大枚举哪个选择的点 \(j\)
每次删除,至多会多出来一个待选叶子点 \(x\)。
若没有多出来叶子点 / \(x > j\),那么继续增大 \(j\)
否则,递归继续删除 \(x\) 即可。
序列转无根树
步骤:
考虑 \(x\) 在序列中出现的次数 + 1: \(cnt_x + 1\) 即 \(x\) 的度数,所有 \(cnt_x = 1\) 的 \(x\) 加入备选集合。
- 按顺序考虑序列每一项 \(x\)
- 从备选集合中找到编号最小的 \(y\),连边 \((x, y)\)。
- 减少 \(cnt_x \gets cnt_x - 1\),若减到 \(1\),把 \(x\) 加入备选集合。
复杂度也可以做到 \(O(n)\),和上述方法类似。
想法
后来突然有一个疑问:为什么转化后的 Prufer 是唯一的呢?
后来百思不得其解后发现自己钻牛角尖了,如上两步,我们给出了:
- Prufer 序列转为唯一确定形态无根树的方法
- 无根树转化为唯一确定 Prufer 序列的方法
那么无根树和 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;
}
例题:光之大陆
做完这题就感觉计数特别玄学,问题出在网上所有题解都认为旋转算不同方案,需要 \(\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\)。
\]
这个 \(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 编码的更多相关文章
- MySQL学习笔记5——编码
MySQL学习笔记5之编码 编码 1.查看MySQL数据库编码 *SHOW VARIABLES LIK 'char%'; 2.编码解释 *character_set_client:MySQL使用该编码 ...
- [学习笔记]prufer序列
前言 PKUWC和NOIWC都考察了prufer序列,结果统统爆零 prufer序列就是有标号生成树对序列的映射 prufer序列生成 每次选择编号最小的叶子删掉,把叶子的父亲加入prufer序列,直 ...
- python学习笔记09-python编码与解码
二进制编码: --->ASCII:只能存英文和拉丁字符 一个字符占一个字节:8位 ------>gb2312:只能存6700多个中文: 1980年发表 ----------->gbk ...
- Swift学习笔记 - URL编码encode与解码decode
使用swift有一段时间了,api的变换造成了很多困扰,下面是关于url编码和解码问题的解决方案 在Swift中URL编码 在Swift中URL编码用到的是String的方法 func addingP ...
- IntelliJ IDEA 学习笔记 - 修改编码
感谢原文作者:codeke 原文链接:https://blog.csdn.net/cgl125167016/article/details/78666432 仓库:https://github.com ...
- 学习笔记_Java_day14—编码实战___一个注册页面的完整流程
- 图论:Prufer编码
BZOJ1211:使用prufer编码解决限定结点度数的树的计数问题 首先学习一下prufer编码是干什么用的 prufer编码可以与无根树形成一一对应的关系 一种无根树就对应了一种prufer编码 ...
- prufer编码学习笔记
prufer 编码 对于一个无根树,他的 prufer 编码是这样确定的: 每次找到编号最小的一个叶子节点,也就是度数为\(1\)的节点,把和它相连的点,加入 prufer 编码序列的末尾,然后把这个 ...
- [原创]java WEB学习笔记45:自定义HttpFilter类,理解多个Filter 代码的执行顺序,Filterdemo:禁用浏览器缓存的Filter,字符编码的Filter,检查用户是否登陆过的Filter
本博客为原创:综合 尚硅谷(http://www.atguigu.com)的系统教程(深表感谢)和 网络上的现有资源(博客,文档,图书等),资源的出处我会标明 本博客的目的:①总结自己的学习过程,相当 ...
随机推荐
- MYSQL 存储引擎(面)
存储引擎是MySQL的组件,用于处理不同表类型的SQL操作.不同的存储引擎提供不同的存储机制.索引技巧.锁定水平等功能,使用不同的存储引擎,还可以获得特定的功能. 使用哪一种引擎可以灵活选择,一个数据 ...
- rgw的rgw_thread_pool_size配置调整
前言 在比对rgw的不同前端的区别的时候,官方说civetweb是通过线程池来控制连接的,beast是后面加入了流控相关的,这块一直也没有调整过相关的参数,然后通过ab压测了一下,还是有很明显的区别的 ...
- JS控制Video播放器(快进、后退、播放、暂停、音量大小)
思路: 一.首先监听触发事件. 比如:向上键对应的keyCode为38,向下键对应的keyCode为40,向左键对应的keyCode为37,向右键对应的keyCode为39,空格键对应的keyCode ...
- 渗透测试神器Cobalt Strike使用教程
Cobalt Strike是一款渗透测试神器,常被业界人称为CS神器.Cobalt Strike已经不再使用MSF而是作为单独的平台使用,它分为客户端与服务端,服务端是一个,客户端可以有多个,可被团队 ...
- Angular 富文本编辑之路的探索
作者:杨振兴Worktile 前端工程师,PingCode Wiki 产品技术负责人 PingCode Wiki 提供结构化知识库来记载信息和知识,便于团队沉淀经验.共享资源,欢迎大家注册试用 本文主 ...
- 一次看完28个关于ES的性能调优技巧,很赞,值得收藏!
因为总是看到很多同学在说Elasticsearch性能不够好.集群不够稳定,询问关于Elasticsearch的调优,但是每次都是一个个点的单独讲,很多时候都是case by case的解答,本文简单 ...
- 新鲜出炉!花了三天整理的JVM复习知识点,面试突击必备!
此次JVM知识点包含以下几个部分 1.类加载机制 2.jvm运行时数据区 3.java对象内存布局 4.jvm内存模型 5.垃圾回收机制 6.垃圾收集器 7.问题排查 一 类加载机制 主要说的部分是这 ...
- try-with-resources和multi-catch的使用
1.首先说一下以前开发中我们在处理异常时,我们会使用try-catch-finally来处理异常. //使用try-catch-finallypublic static void main(Strin ...
- Spring Boot中的配置
一.首先使用idea中的Spring Initializr快速创建一个SpringBoot应用,idea会联网自动创建,创建好的结构如下(一些没必要的文件都删了): 其中说一下几个文件夹和文件 sta ...
- Vue—新版本router-view 与 keep-alive 的互动
1. <keep-alive> 直接嵌套到 <router-view> 上会失效,正确写法: <router-view #="{ Component }&quo ...