首页 > 其他分享 >Prufer 序列学习笔记

Prufer 序列学习笔记

时间:2023-04-21 09:01:27浏览次数:62  
标签:limits 个数 笔记 根树 序列 Prufer 节点

一、前言

感觉它本身没有什么用。主要是用于计数问题。
前置知识:树的定义。

二、定义

对于一棵有 \(n\) 个节点的无根树 \(T\),定义其 Prufer 序列为执行以下操作 \(n-2\) 次所形成的长度为 \(n-2\) 的正整数序列。
·选择其编号最小的度数为 \(1\) 的节点,输出唯一与其相邻的节点的编号。
·删除这个节点以及以其为端点的唯一边。
然后这个东西有一些性质:
1.容易知道最后树上一定是只留下 \(2\) 个度数为 \(1\) 的节点以及连着它们的边,其中一个编号为 \(n\)。证明:将无根树视作以 \(n\) 为根的有根树,容易知道由于树没有环,所以当点的个数 \(>1\) 时叶子结点的个数 \(\ge 2\)。如果树上节点个数 \(>1\),存在一个叶子节点 \(y\ne n\),删除节点 \(y\),一定比删除节点 \(n\) 更优。□
2.这个东西可以与一棵有编号的无根树一一对应。
这个性质可以通过给出一种一一对应的映射方式来证明,见下文。
3.每个节点在 Prufer 序列里出现的次数是它在无根树上的度数减一,特别地,一开始的所有叶子节点不会出现在 Prufer 序列里。
证明:每往 Prufer 序列中加一个节点,就会删掉一条边。最后每个节点都至多只有一条边。我们知道因为每次删的都是叶子节点,所以每删去一个节点以及以其为端点的唯一边后,无根树仍连通。具体来说,如果我们将树视为以 \(n\) 为根的有根树,最后剩下的 \(2\) 个节点中一定有根 \(n\),而且另外一个是 \(n\) 的一个子节点。而且容易知道一个节点 \(x\) 的所有子节点被删时,标在 Prufer 序列里的数都是 \(x\),而一个节点 \(y\) 被删时,表在 Prufer 序列里的数是它的父亲节点的编号。那么除了 \(n\) 以外每个节点的子节点都被删光了,所以每个节点都被标了它的子节点的个数次,所以是 \(d_i-1\) 次。而对于节点 \(n\),它的子节点个数是 \(d_i\),它有一个儿子没被删,所以它也被标了 \(d_i-1\) 次。□

三、用途

1.可以知道有 \(n\) 个节点的不同有标号无根树一共有 \(n^{n-2}\) 棵,即有 \(n\) 个点的完全图的不同生成树一共有 \(n^{n-2}\)(Cayley 定理)。这是因为它的长为 \(n-2\) 的 Prufer 序列的每一位都可以填 \(1\) 到 \(n\) 的任意正整数。有 \(n\) 个节点的不同有标号有根树一共有 \(n^{n-1}\) 棵,因为处理出无根树之后,\(n\) 个点都可以做根。
2.可以知道有 \(n\) 个节点,其中编号为 \(i\) 的节点的度数为 \(d_i\),并且满足 \(\sum\limits_{i=1}\limits^n{d_i}=2\times n-2\) 的不同有标号无根树有 \(\frac{(n-2)!}{\prod\limits_{i=1}\limits^n{(d_i-1)!}}\) 个。注意分母是先做阶乘再连乘。这是因为这棵无根树的 Prufer 序列可看作 \(d_1-1\) 个 \(1\)、\(d_2-1\) 个 \(2\)、……、\(d_n-1\) 个 \(n\) 所组成的,长度为 \(n-2\) 的可重复元素的全排列。
3.在造数据时,如果把树的每个节点的父亲节点标为一个随机的树,树的期望高度是 \(\sqrt n\),而随机出 Prufer 序列再转换成树,树的期望高度是 \(\log n\) 的。

四、例题

例1.P6086 【模板】Prufer 序列

这一题是要实现 \(O(n)\) 的 Prufer 序列和有标号无根树的互相转换。
出于方便考虑,将无根树视作以 \(n\) 为根的有根树(事实上,根据定义,这不影响结果,而且只是为了在表述上更方便大家理解),于是下面将所有节点往 \(n\) 号节点方向非自身的最近点称为其父亲节点。

\(O(n\log n)\) 做法

首先实现父亲序列转 Prufer。一个明显的想法是记录所有点的子节点个数,维护一个小根堆(优先队列),把所有子节点个数为 \(0\) 的点扔进这个小根堆。然后每次取队头出队,然后删掉它,并且将它的父亲节点的度数减一,然后记录在 Prufer 序列里。接下来检查它父亲节点的子节点个数,如果降为 \(0\) 就入队。然后我们就求出了 Prufer 序列。时间复杂度 \(O(n\log n)\),且常数较大。
然后就是 Prufer 序列转父亲序列,还是找出每一个节点的子节点数数(即在序列中出现的次数),然后找到所有子节点数为 \(0\) 的点,加入优先队列。每次取出队头,然后删掉它,并且我们可以知道第 \(i\) 次出队的点的父亲的编号实际上就是 Prufer 序列里第 \(i\) 个数,然后将其父亲节点的度数减一。最后就可以找到 \(n-2\) 个节点的父亲节点,然后 \(n\) 没有父亲节点,然后剩下一个节点的父亲节点为 \(n\),然后我们就求出了父亲序列。时间复杂度 \(O(n\log n)\),且常数较大。
所以就可以实现在 \(O(n\log n)\) 的时间内互相转换,同时也证明了 Prufer 序列与一棵有标号无根树一一对应。所以以上所说的所有计数问题成立。

点击查看代码
int main(){
	n=read();m=read();
	if(m==1){
		for(int i=1;i<=n-1;i++)d[f[i]=read()]++;
		for(int i=1;i<=n;i++)if(!d[i])q.push(i);
		for(int i=1;i<=n-2;i++){
			p[i]=f[q.top()];q.pop();
			if(!--d[p[i]])q.push(p[i]);
		}
		for(int i=1;i<=n-2;i++)ans^=1ll*i*p[i];
	}else{
		for(int i=1;i<=n-2;i++)d[p[i]=read()]++;
		for(int i=1;i<=n-1;i++)f[i]=n;
		for(int i=1;i<=n;i++)if(!d[i])q.push(i);
		for(int i=1;i<=n-2;i++){
			f[q.top()]=p[i];q.pop();
			if(!--d[p[i]])q.push(p[i]);
		}
		for(int i=1;i<=n-1;i++)ans^=1ll*i*f[i];
	}
	cout<<ans<<'\n';
	return 0;
}

AC记录,通过了就很神奇,直接 \(n\log n\) 过 \(5000000\) 是吧。

\(O(n)\) 做法

但是这一题的正解是 \(O(n)\) 的。
我们可以发现,这一次删了一个点,那么下一次删哪个点。首先,有可能是原本就是度数为 \(1\) 的节点。其次,它还有可能是当前节点的父亲节点。
所以我们可以维护一个指针,初始时为 \(1\),然后每次遇到一个节点 \(y\),如果它的子节点被删光,那么就把它的父亲节点 \(x\) 记录进 Prufer 序列里,然后把 \(x\) 的残存的子节点数减一,如果 \(x\) 的子节点数因此变为 \(0\),分类讨论:如果 \(x>y\),则不用管它,继续自增指针;如果 \(x<y\),那么要另外处理 \(x\),删除它,继续记录 \(x\) 的父亲节点,然后继续判断……直到这个连锁反应停止为止。因为每个节点只会处理 \(1\) 次,所以时间复杂度 \(O(n)\)。
把 Prufer 序列转换成父亲序列同理。

点击查看代码
int main(){
	n=read();m=read();
	if(m==1){
		for(int i=1;i<=n-1;i++)d[f[i]=read()]++;
		for(int i=1,j=1,x;i<=n-2;i++,j++){
			while(d[j])j++;p[i]=f[x=j];
			while(i<=n-2&&!--d[p[i]]&&p[i]<j)p[++i]=f[x=f[x]];
		}
		for(int i=1;i<=n-2;i++)ans^=1ll*i*p[i];
	}else{
		for(int i=1;i<=n-2;i++)d[p[i]=read()]++;p[n-1]=n;
		for(int i=1,j=1,x;i<=n-1;i++,j++){
			while(d[j])j++;f[x=j]=p[i];
			while(i<=n-1&&!--d[p[i]]&&p[i]<j)f[x=f[x]]=p[++i];
		}
		for(int i=1;i<=n-1;i++)ans^=1ll*i*f[i];
	}
	cout<<ans<<'\n';
	return 0;
}

AC记录
然后这个东西有什么用呢?基本没用,毕竟现有的大部分 OI 题只考计数,不考求 Prufer 序列。但是万一考了呢?学有余力的话还是学一学吧。

例2.P4981 父子

这一题就是求一个有 \(n\) 个点的完全图的生成树有几个。输出 $n^{n-2} $即可。

例3.P4430 小猴打架

这一题就是求一个有 \(n\) 个点的完全图的生成树的边共能形成多少种不同的排列。
注意到两棵不同的生成树至少有一个点不同,所以边形成的排列一定不同,所以答案就是生成树的棵数乘上树的边数的全排列的方法数,即 \(n^{n-2}\times (n-1)!\)。

例4.P2290 [HNOI2004]树的计数

使用上面(“用途”中)所讲的公式,可以知道是 \(\frac{(n-2)!}{\prod\limits_{i=1}\limits^n{(d_i-1)!}}\)。注意特判 \(\sum\limits_{i=1}\limits^n{d_i}\ne 2\times n-2\) 的情况(此时组不成一棵树,应该输出 0)以及图不连通的情况。

例5.P2624 [HNOI2008]明明的烦恼

考虑 Prufer 序列。令 \(s=\sum\limits_{d_i>0}^n{(d_i-1)}\),\(k=\sum\limits_{d_i>0}^n{1}\),\(q_i\) 代表第 \(i\) 个满足 \(d_r>0\) 的 \(r\),那么我们知道 Prufer 序列里面有 \(s\) 个数是固定的,分别是 \(d_{q_i}-1\) 个 \(q_i\),以及 Prufer 序列中剩下 \(n-2-s\) 个数,每一个数都可以是节点编号中的剩下 \(n-k\) 个数的任何一个。所以答案就是所有 \(d_{q_i}-1\) 个 \(q_i\) 的可重全排列乘上 \(n-2\) 个位置中选 \(s\) 个方案数乘上 \(n-k\) 的 \(n-2-s\) 次方,即 \(\frac{s!} {\prod\limits_{d_i>0}\limits^n{(d_i-1)!}}\times C_{n-2}^s\times (n-k)^{n-2-s}\)。要实现高精度乘除。

标签:limits,个数,笔记,根树,序列,Prufer,节点
From: https://www.cnblogs.com/qwq-qaq-tat/p/17337712.html

相关文章

  • 分布滞后线性和非线性模型(DLNM)分析空气污染(臭氧)、温度对死亡率时间序列数据的影响|附
    全文下载链接 http://tecdat.cn/?p=23947 最近我们被客户要求撰写关于分布滞后线性和非线性模型的研究报告,包括一些图形和统计输出。分布滞后非线性模型(DLNM)表示一个建模框架,可以灵活地描述在时间序列数据中显示潜在非线性和滞后影响的关联。该方法论基于交叉基的定义,交叉基是......
  • C语言基础知识(不想写笔记啦,就把它打出来)
    scanf()函数的使用:操作系统接收数据时其实都是当作字符来接收的。scanf()函数的两种用法:用法一:scanf("输入控制符",输入参数);功能:将从键盘输入的字符转化成输入控制符所规定格式的数据,然后存入以输入参数的值为地址的变量中。用法二:scanf("非输入控制符输入控制符"......
  • 【进阶15】【自学笔记】Python运行cmd命令的几种方式
    一、pathlib的简单介绍pathlib是Python3.4及更高版本中内置的标准库,提供了一种面向对象的方式来处理文件系统路径。它为不同操作系统提供了合适的路径语义,并支持常见的文件和目录操作,比如判断路径是否存在、获取路径的各个部分、创建/删除目录等操作。二、基本操作1、获取......
  • 微信小程序开发笔记 基础篇③——自定义数据dataset,事件触发携带额外信息
    文章目录一、前言二、视频演示三、原理和流程四、注意事项五、全部源码六、参考一、前言微信小程序开发笔记——导读想要实现一个电费充值界面。多个不同金额的充值按钮,每个按钮都携带自定义数据(金额)点击不同金额的充值按钮,就会上传对应的数据(金额)。所以,本文主要使用到了微信小程......
  • JavaScript学习笔记
    SassSASS官网世界上最成熟、最稳定、最强大的专业级CSS扩展语言!sass是一个css的预编译工具也就是能够更优雅的书写csssass写出来的东西浏览器不认识依旧是要转换成css在浏览器中运行变量定义一个变量,在后面的代码中使用使用$来定义变量//定义一个$c作为变量,值是红......
  • 【进阶14】【自学笔记】Python运行cmd命令的几种方式
    1、使用os.system()函数importos#运行cmd命令os.system('dir')2、使用subprocess模块importsubprocess#运行cmd命令subprocess.run(['dir'],shell=True)3、使用os.popen()函数importos#运行cmd命令result=os.popen('dir')print(result.read......
  • Django笔记二十六之数据库函数之数学公式函数
    本文首发于公众号:Hunter后端原文链接:Django笔记二十六之数据库函数之数学公式函数这一篇来介绍一下公式函数,主要是数学公式。其中sin,cos这种大多数情况下用不上的就不介绍了,主要介绍下面几种:Abs()绝对值Ceil()向上取整Floor()向下取整Mod()取余Power()乘方Roun......
  • 程序员修炼之道阅读笔记
    第16节强力编辑器1、我们认为你最好是精通一种编辑器,并将其用于所有编辑任务:代码、文档、备忘录、系统管理等等。进行编辑活动时,你不必停下来思考怎样完成文本操作,编辑器将成为你双手的延伸,键会在滑过文本和思想时歌唱起来。这就是我们的目标。2、好的编辑器应该具有这些特性......
  • 人月神话读书笔记02
    我过去是怎么做的:单纯把编程作为工作这样做为什么不好:没有乐趣就没有动力解决办法:第一章焦油坑编程系统产品只有编程系统产品才是真正有用的产品,是大多数系统开发的目标。职业的乐趣创建事物的纯粹快乐;eg:当自己写完第一个helloworld时候的欣喜来源于开发对......
  • 人月神话读书笔记03
    本次阅读第七章 我过去是怎么做的在编程之前没有清晰的目标,写到什么就去做什么这种做法为什么不好思路不够清晰,导致编程没有逻辑性如何解决:7.为什么巴比伦塔会失败?关于巴比伦塔的故事:维基百科TowerofBabel7.1巴比伦塔的管理教训据《创世纪》记载,巴比伦塔是人类继......