首页 > 其他分享 >Prufer序列

Prufer序列

时间:2024-08-11 09:04:58浏览次数:13  
标签:cur int Prufer tot 序列 prufer 节点 deg

Prufer 序列

Prufer 序列可以将一个带标号 \(n\) 个结点的树用 \([1, n]\) 中的 \(n - 2\) 个整数表示,也可以理解为完全图的生成树与数列之间的双射。

建立过程:每次选择编号最小的叶子节点并删掉,然后在序列中记录它连接的节点标号,重复 \(n - 2\) 次后结束。

不难发现:

  • 构造完 Prufer 序列后原树中肯定会剩下两个节点,其中必然有一个节点为 \(n\) 。
    • 因为一棵无根树至少有两个叶子节点,\(n\) 肯定不会成为编号最小的叶子节点,故不可能被删去。
  • 每个节点在序列中出现的次数就是其度数 \(-1\) 。
    • 每个节点在被删除前其度数 \(-1\) 个叶子节点一定被删完,序列中就会出现度数 \(-1\) 次其编号。

为了方便,规定树的根为 \(n\) ,因为 \(n\) 一定不会被删掉。

线性建立 Prufer 序列

考虑维护一个指向编号最小的叶子节点的指针 \(p\) 初值为 \(1\) 。同时我们维护每个节点的度数以维护新产生的叶子节点。过程如下:

  • 删除 \(p\) 指向的节点,更新 Prufer 序列。
  • 若删去 \(p\) 后其父亲为新的叶子节点且编号 \(< p\) ,立即删除这个节点。重复此操作直到不满足条件。
  • 让 \(p\) 自增直到 \(p\) 指向一个未被删除的叶子节点,跳转至步骤一。

每个点在遍历指针时最多被访问一次,每条边在更新度数时最多被删除一次,因此时间复杂度时 \(O(n)\) 的。

inline void BuildPrufer() {
	for (int i = 1; i < n; ++i)
		++deg[fa[i]], ++deg[i];

	for (int cur = 1, tot = 0; tot < n - 2; ++cur) {
		while (deg[cur] > 1)
			++cur;

		--deg[prufer[++tot] = fa[cur]];

		while (tot < n - 2 && deg[prufer[tot]] == 1 && prufer[tot] < cur)
			++tot, --deg[prufer[tot] = fa[prufer[tot - 1]]];
	}
}

线性重建树

由 Prufer 序列,可以得到原树上每个节点的度数和最小的叶子节点编号,这个节点肯定和 Prufer 序列的第一个数连接。

类似的,我们同样维护一个指针 \(p\) ,重建过程中同样会不断产生新的叶子节点,再连边即可。

inline void BuildTree() {
	fill(deg + 1, deg + 1 + n, 1);

	for (int i = 1; i <= n - 2; ++i)
		++deg[prufer[i]];

	for (int cur = 1, tot = 0; tot < n - 2; ++cur) {
		while (deg[cur] > 1)
			++cur;

		--deg[fa[cur] = prufer[++tot]];

		while (tot < n - 2 && deg[prufer[tot]] == 1 && prufer[tot] < cur)
			++tot, --deg[fa[prufer[tot - 1]] = prufer[tot]];
	}

	fa[prufer[n - 2]] = n;
}

Cayley 公式

\(n\) 个点组成的无向完全图有 \(n^{n - 2}\) 棵生成树。使用 Prufer 序列可以很简单地证明。

图连通方案数

一个 \(n\) 个点 \(m\) 条边的带标号无向图有 \(k\) 个连通块,添加 $ k - 1$ 条边使得整个图连通,求方案数。

建立新图并将每个联通块视为点 \(A_{1 \sim k}\) ,令 \(s_i\) 为每个连通块中点的数量, \(d_i\) 为 \(A_i\) 的度数(不是连通块内部的度数)

因为度数和为边数的两倍,所以有 \(\sum_{i = 1}^k d_i = 2k - 2\) 。

由于 Prufer 序列中每个节点出现的次数就是其度数 \(-1\) ,所以对于给定的序列 \(d\) 在新图中的 Prufer 序列的方案数为:

\[\dbinom{k - 2}{d_1 - 1, d_2 - 1, \cdots, d_k - 1} = \cfrac{(k - 2)!}{(d_1 - 1)! (d_2 - 1)! \cdots (d_k - 1)!} \]

对于第 \(i\) 个连通块,它有 \(s_i^{d_i}\) 种连接方式,因此对于给定 \(d\) 序列使图联通的方案数是:

\[\dbinom{k - 2}{d_1 - 1, d_2 - 1, \cdots, d_k - 1} \cdot \prod_{i = 1}^k s_i^{d_i} \]

枚举所有的 \(d\) 序列,总方案数即为:

\[\sum_{d_i \geq 1, \sum_{i = 1}^k d_i = 2k - 2} \dbinom{k - 2}{d_1 - 1, d_2 - 1, \cdots, d_k - 1} \cdot \prod_{i = 1}^k s_i^{d_i} \]

有多元二项式定理:

\[(x_1 + \cdots + x_m)^p = \sum_{c_i \geq 0, \sum_{i = 1}^m c_i = p} \dbinom{p}{c_1, c_2, \cdots, c_m} \cdot \prod_{i = 1}^m x_i^{c_i} \]

尝试对原式换元,令 \(e_i = d_i - 1\) ,显然 \(\sum_{i = 1}^k e_i = k - 2\) ,于是原式转化为:

\[\begin{aligned} & \ \sum_{e_i \geq 0, \sum_{i = 1}^k e_i = k - 2} \dbinom{k - 2}{e_1, e_2, \cdots, e_k} \cdot \prod_{i = 1}^k s_i^{e_i + 1} \\ =& \ (s_1 + s_2 + \cdots + s_k)^{k - 2} \cdot \prod_{i = 1}^k s_i \\ =& \ n^{k -2} \cdot \prod_{i = 1}^k s_i \end{aligned} \]

应用

P2290 [HNOI2004]树的计数

给出每个点的度数,求满足条件的树的方案数。

\(n \leq 150\)

\[\dbinom{k - 2}{d_1 - 1, d_2 - 1, \cdots, d_k - 1} = \cfrac{(k - 2)!}{(d_1 - 1)! (d_2 - 1)! \cdots (d_k - 1)!} \]

直接求会炸,注意到其等价于:

\[\prod_{i = 1}^{k} \dbinom{\sum_{j = i}^k (d_i - 1)}{d_i - 1} \]

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1.5e2 + 7;

ll C[N][N];
int deg[N];

int n, sum;

signed main() {
	scanf("%d", &n);
	
	if (n == 1) {
		scanf("%d", deg + 1);
		putchar(deg[1] ? '0' : '1');
		return 0;
	}
	
	for (int i = 1; i <= n; ++i) {
		scanf("%d", deg + i);
		sum += deg[i] - 1;
		
		if (!deg[i])
			return putchar('0'), 0;
	}
	
	if (sum != n - 2)
		return putchar('0'), 0;
	
	C[0][0] = 1;
	
	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] + C[i - 1][j - 1];
	}

	ll ans = 1;
	
	for (int i = 1; i <= n; ++i)
		ans *= C[sum][deg[i] - 1], sum -= deg[i] - 1;
	
	printf("%lld", ans);
	return 0;
}

P5454 [THUPC2018]城市地铁规划

给出一个 \(k\) 次多项式 \(F(x) \bmod 59393\) ,构造一棵 \(n\) 个点的树,记 \(d_i\) 为每个点的度数,求 \(\max \sum_{i = 1}^n F(d_i)\) ,输出这个最大值并给出一组方案。

\(n \leq 3000, k \leq 10\)

不难发现求出一组 \(d_i\) 后即可用 Prufer 序列重建树即可。

先给每个节点分配 \(1\) 的度数,于是总度数和变为 \(n - 2\) 。

设 \(f_i\) 表示 Prufer 序列中分配完 \(i\) 位可获得的最大权值和,有:

\[f_j = \max (f_{j - i + 1} + F(i) - F(1)) \\ f_0 = n \times F(1) \]

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int Mod = 59393;
const int N = 3e3 + 7, K = 1e1 + 7;

int prufer[N], deg[N], fa[N];
int a[K], w[N], f[N], g[N];

int n, k;

inline void prework() {
	for (int i = 0; i <= n; ++i)
		for (int j = k; ~j; --j)
			w[i] = (w[i] * i % Mod + a[j]) % Mod;
}

inline void BuildTree() {
	fill(deg + 1, deg + 1 + n, 1);

	for (int i = 1; i <= n - 2; ++i)
		++deg[prufer[i]];

	for (int cur = 1, tot = 0; tot < n - 2; ++cur) {
		while (deg[cur] > 1)
			++cur;

		--deg[fa[cur] = prufer[++tot]];

		while (tot < n - 2 && deg[prufer[tot]] == 1 && prufer[tot] < cur)
			++tot, --deg[fa[prufer[tot - 1]] = prufer[tot]];
	}

	fa[prufer[n - 2]] = n;
}

signed main() {
	scanf("%d%d", &n, &k);
	
	for (int i = 0; i <= k; ++i)
		scanf("%d", a + i);
	
	prework();
	printf("%d ", n - 1);
	
	if (n == 1)
		return printf("%d", w[0]), 0;
	else if (n == 2)
		return printf("%d\n1 2", w[1]), 0;
	
	f[0] = n * w[1];
	
	for (int i = 2; i <= n; ++i)
		for (int j = i - 1; j <= n - 2; ++j)
			if (f[j - i + 1] + w[i] - w[1] > f[j])
				f[j] = f[j - i + 1] + w[i] - w[1], g[j] = j - i + 1;
	
	printf("%d\n", f[n - 2]);
	
	for (int i = n - 2, tot = 0, cur = 1; i; i = g[i], ++cur)
		for (int j = 1; j <= i - g[i]; ++j)
			prufer[++tot] = cur;
	
	BuildTree();
	
	for (int i = 1; i < n; ++i)
		printf("%d %d\n", i, fa[i]);
	
	return 0;
}

标签:cur,int,Prufer,tot,序列,prufer,节点,deg
From: https://www.cnblogs.com/wshcl/p/18353085/prufer

相关文章

  • web渗透-反序列化
    一:概念1、序列化:将变量转化为可保存或者可以传输的字符串的过程;实现函数是serialize()函数(变量转化成字符串)2、反序列化:把这个字符串在转化成原来变量使用;就是序列化的逆过程;实现函数是unserialize()函数(字符串转换成变量)3、示例<?phpclassStudent{ public$name="admin";......
  • wechat crawler url拼接 url解析 微信爬虫 json序列化 反序列化
    WechatPublicRequest\Program.csusingSystem.Collections.Specialized;usingSystem.Diagnostics;usingSystem.Web;usingNewtonsoft.Json;classProgram{staticasyncTaskMain(){varlatestTxtFilePath=GetLatestTxtFilePath();......
  • 如何在Java项目中使用自定义序列化器处理URL
    如何在Java项目中使用自定义序列化器处理URL在Java开发中,处理和序列化URL是一个常见的需求,尤其是在涉及到图像资源时。如果项目需要根据特定条件处理图像URL(如添加前缀),可以自定义一个序列化器来简化这一过程。本文将介绍如何创建一个自定义的ImgJsonSerializer类,处理单个URL和UR......
  • 代码随想录day25 || 491 递增子序列,46 全排列, 47 全排列2
    491递增子序列funcfindSubsequences(nums[]int)[][]int{ //思路,在原数组上面找寻递增子序列,所以不能改变顺序, varpath[]int varres[][]int //nums=quicksort(nums) backtracking(nums,&path,&res,-200)//范围是【-100,100】,传入一个不在区间的数字就不会......
  • 机器学习笔记:序列到序列学习[详细解释]
    介绍本节我们使用两个循环神经网络的编码器和解码器,并将其应用于序列到序列(sequencetosequence,seq2seq)类的学习任务。遵循编码器-解码器架构的设计原则,循环神经网络编码器使用长度可变的序列作为输入,将其转换为固定形状的隐状态。换言之,输入序列的信息被编码到循环神经网......
  • 多元时间序列分析统计学基础:基本概念、VMA、VAR和VARMA
    多元时间序列是一个在大学课程中经常未被提及的话题。但是现实世界的数据通常具有多个维度,所以需要多元时间序列分析技术。在这文章我们将通过可视化和Python实现来学习多元时间序列概念。这里假设读者已经了解单变量时间序列分析。1、什么是多元时间序列?顾名思义,多元时间序列是......
  • 超简单适合练手的双指针题:判断子序列
    给定字符串 s 和 t ,判断 s 是否为 t 的子序列。字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。示例1:输入:s="abc",t="ahbgdc"输出:true示例2:输入:s="axc......
  • 金蝶云星空锁库时同时锁定序列号
    金蝶云星空锁库时同时锁定序列号##业务背景公司业务要求,如果检查发现序列号有问题,先锁库不允许出库。##系统现状即时库存锁库,锁定的是数量,库存-锁库数=可用数,当可用量小于等于0就不可以再出库了。如果想要控制锁到序列号,系统就不支持了。##方案设计锁库时同时锁定库存和根......
  • Leetcode热题100-128.最长连续序列
    Leetcode热题100-128.最长连续序列1.题目描述2.解题思路3.代码实现1.题目描述128.最长连续序列2.解题思路使用哈希集合的思想:初始化一个unordered_set并将nums中所有元素放入集合中;遍历数组,依次判断当前元素是否为连续序列的开始,若是则求出当前连续序列......
  • HuggingFace:使用 Transformer 对 DNA 序列进行高效大规模嵌入提取
    我有一个非常大的数据框(60+百万行),我想使用转换器模型来获取这些行(DNA序列)的嵌入。基本上,这首先涉及标记化,然后我可以获得嵌入。由于RAM限制,我发现标记化然后将所有内容嵌入到一个py文件中是行不通的。这是我发现的解决方法,适用于大约3000万行的数据帧(但不适用于较大的d......