当前位置: 首页 > news >正文

Prufer序列 学习笔记

文章目录

P r u f e r Prufer Prufer 序列

P r u f e r Prufer Prufer 序列可以将一棵 n n n 个点的有编号无根树 转化成一个长度为 n − 2 n - 2 n2,值域在 [ 1 , n ] [1, n] [1,n] 的序列。也可以将 任意一个 长度为 n − 2 n - 2 n2,值域在 [ 1 , n ] [1, n] [1,n] 的序列转化成一棵 有编号无根树。可以理解成有标号完全图的生成树与数列之间的双射,常用于对树计数的问题。

对树建立 P r u f e r Prufer Prufer 序列

建立过程:
每次找到编号最小的叶子,往序列末尾加入它所连接的节点编号,然后删掉这个叶子。重复 n − 2 n - 2 n2 次后只剩下两个节点结束。

实现方式:
考虑维护指针 p p p 指向当前最小的叶子编号,每次删掉 p p p 后判断 p p p 相连的点是否变成了叶子并且编号小于 p p p,如果满足这两个条件那么接着删掉这个点。重复这个过程直到当前叶子相连的点不满足上述条件,然后令 p p p 不断自增直到找到下一个叶子。

正确性说明:

  • 每次删掉一个叶子后最多只会增加一个叶子,并且如果增加了叶子那么一定是当前叶子所连的点。
  • 设当前叶子为 p p p,删掉 p p p 后新增叶子 q q q
  • q < p q < p q<p,那么 q q q 一定比其它叶子更小,删 q q q 是正确的。
  • q > p q > p q>p,那么后面 p p p 自增时一定能枚举到,因此不用管。

这样指针只会移动 O ( n ) O(n) O(n) 次,每个点只会被删除一次,复杂度 O ( n ) O(n) O(n)

代码:

inline void TP() { // 树 -> prufer序列for(int i = 1; i < n; i ++ ) deg[fa[i]] ++;int p = 1;for(int i = 1; i <= n - 2; ) {int j = i;while(deg[p]) p ++; pf[j ++] = fa[p]; deg[fa[p]] --;int tp = p;while(j <= n - 2 && !deg[fa[p]] && fa[p] < tp) p = fa[p], pf[j ++] = fa[p], deg[fa[p]] --;i = j; p = ++ tp;}
}

P r u f e r Prufer Prufer 序列的性质:

  • 构造完 P r u f e r Prufer Prufer 序列后原树会剩下两个节点,其中一定有一个为 n n n
  • 原树中的每个点一定在 P r u f e r Prufer Prufer 序列中出现 d e g − 1 deg - 1 deg1 次,没有出现过的就是叶子节点。

从上述建立过程可以看出来:
任意一棵 n n n 个点有编号无根树都可以建立出唯一的 P r u f e r Prufer Prufer 序列,并且本质不同的树对应的 P r u f e r Prufer Prufer 序列一定不同
这就完成了 从树到 P r u f e r Prufer Prufer 序列 的单射。

P r u f e r Prufer Prufer 序列重建树

建立过程:
对于给定的长为 n − 2 n - 2 n2,值域在 [ 1 , n ] [1, n] [1,n] p r u f e r prufer prufer 序列,我们可以统计每个点的出现次数来求出每个点的度数。考虑每次找到编号最小的叶子来确定与它相连的点,显然这个相连点应该是当前 P r u f e r Prufer Prufer 序列的第一个数。然后把这个叶子删掉,令相连点的度数减一,然后把 P r u f e r Prufer Prufer 序列的第一个数删掉,重复这个过程 n − 2 n - 2 n2 次。剩下两个叶子,把它们之间连一条边即可。

实现方式:
注意到最后剩下的两个叶子也一定有一个是 n n n,我们将 P r u f e r Prufer Prufer 序列的末尾补上一个 n n n,然后重复上述过程 n − 1 n - 1 n1 次就可以连出 n − 1 n - 1 n1 条边。

与建立 P r u f e r Prufer Prufer 序列的过程类似,考虑维护指针 p p p 表示当前编号最小的叶子,然后每次删掉叶子后判断新增叶子 q q q 是否比 p p p 小,如果是的话就接着去连 q q q 的出边,否则就不断自增 p p p 直到找到下一个叶子。可以看作对于当前叶子 p p p P r u f e r Prufer Prufer 序列的开头就是以 n n n 为根下 p p p 的父亲节点

也不难看出 → \to 序列序列 → \to 的过程是互逆的。一个 P r u f e r Prufer Prufer 序列重建的树对应的 P r u f e r Prufer Prufer 序列也一定是这个 P r u f e r Prufer Prufer 序列。

复杂度 O ( n ) O(n) O(n)

代码:

inline void PT() { // prufer序列 -> 树pf[n - 1] = n;for(int i = 1; i <= n - 2; i ++ ) deg[pf[i]] ++;int p = 1;for(int i = 1; i <= n - 1;) {int j = i;while(deg[p]) p ++; fa[p] = pf[j ++], deg[fa[p]] --;int tp = p;while(j <= n - 1 && !deg[fa[p]] && fa[p] < tp) p = fa[p], fa[p] = pf[j ++], deg[fa[p]] --;i = j; p = ++ tp;}
}

由上述重构过程可以看出:
任意一个长度为 n − 2 n - 2 n2,值域在 [ 1 , n ] [1, n] [1,n] 的序列,都能作为一个 P r u f e r Prufer Prufer 序列唯一的构造出一棵树。并且任意两个不同 P r u f e r Prufer Prufer 序列构造的树都是不同的

这样就完成了从 P r u f e r Prufer Prufer 序列到树 的单射。结合上边,就得到了:

n n n 个点编号为 1 ∼ n 1 \sim n 1n 的无根树与长度为 n − 2 n - 2 n2,值域为 [ 1 , n ] [1, n] [1,n] 的序列双射

应用

Cayley 公式

n n n 个点有编号的完全图生成树个数为 n n − 2 n^{n - 2} nn2

证明:双射即可。

[HNOI2004] 树的计数

题意:
给定 n n n 个点的度数 d 1 , … d n d_1,\dots d_n d1,dn,任意两个点之间可以连边,问有多少个满足度数数组的生成树。

1 ≤ n ≤ 150 1 \leq n \leq 150 1n150

分析:
特判 n = 1 n = 1 n=1 的边界情况。那么任意一个点的度数不能为 0 0 0,并且所有点的度数和应为 2 n − 2 2n - 2 2n2
一个点只要在 P r u f e r Prufer Prufer 序列中出现 d i − 1 d_i - 1 di1 即满足构造出来的树中度数为 d i d_i di。这就是多重集的全排列。

CODE:

#include<bits/stdc++.h>
using namespace std;
const int N = 155;
typedef long long LL;
int n, deg[N], all;
LL res = 1, C[N][N];
int main() {scanf("%d", &n); int all = n - 2;for(int i = 0; i <= n; i ++ ) for(int j = 0; j <= i; j ++ ) if(!j) C[i][j] = 1;else C[i][j] = C[i - 1][j - 1] + C[i - 1][j];for(int i = 1; i <= n; i ++ ) {scanf("%d", &deg[i]);if(deg[i] == 0) {if(n == 1) {puts("1"); return 0;}else {puts("0"); return 0;}}res = res * C[all][deg[i] - 1];all -= deg[i] - 1;}if(all != 0) puts("0");else cout << res << endl;return 0;
}

「雅礼集训 2017 Day8」共

题意:
给定 n , k n, k n,k。 你需要求出有多少个编号为 1 ∼ n 1 \sim n 1n,以 1 1 1 为根的无向树,满足深度为奇数的点恰好有 k k k 个。 1 1 1 的深度认为是 1 1 1

1 ≤ k < n ≤ 5 × 10 5 1 \leq k < n \leq 5 \times 10^5 1k<n5×105

分析:
显然可以将点按照深度的奇偶性分成两类,这将树变成了一张二分图。
1 1 1 划分到左部点中,那么需要从剩下的 n − 1 n - 1 n1 个编号里选出 k − 1 k - 1 k1 个分到左部点。答案就是 ( n − 1 k − 1 ) × S ( k , n − k ) \binom{n - 1}{k - 1} \times S(k, n - k) (k1n1)×S(k,nk)
其中 S ( a , b ) S(a, b) S(a,b) 表示一张左部点有 a a a 个,右部点有 b b b 个的有标号完全二分图的生成树个数。
答案是 a b − 1 b a − 1 a^{b-1}b^{a - 1} ab1ba1
证明:
对于这样二分图的生成树, P r u f e r Prufer Prufer 序列构造过程的末尾一定剩下一个左部点和一个右部点。因此它的 P r u f e r Prufer Prufer 序列一定有 b − 1 b - 1 b1 个左部点和 a − 1 a - 1 a1 个右部点。
对于这两部分的构成的子序列内部,如果确定了编号和顺序。那么两部分之间在 P r u f e r Prufer Prufer 序列上的顺序就定下来了。因此总方案数就是 a b − 1 b a − 1 a^{b - 1}b^{a - 1} ab1ba1

CODE:

// 这种双射还真是秒啊
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5 + 10;
typedef long long LL;
int n, k, p;
inline LL Pow(LL x, LL y) {LL res = 1, k = x;while(y) {if(y & 1) res = res * k % p;y >>= 1;k = k * k % p;}return res;
}
LL fac[N], inv[N];
int main() {cin >> n >> k >> p;fac[0] = 1; for(int i = 1; i < N; i ++ ) fac[i] = fac[i - 1] * i % p;inv[N - 1] = Pow(fac[N - 1], p - 2); for(int i = N - 2; i >= 0; i -- ) inv[i] = inv[i + 1] * (i + 1) % p;int L = k, R = n - k; LL res = fac[n - 1] * inv[k - 1] % p * inv[n - k] % p * Pow(L, R - 1) % p * Pow(R, L - 1) % p;cout << res << endl;return 0;
}

[THUPC 2018] 城市地铁规划

题意:

给定一个 n , k , m o d n, k, mod n,k,mod,然后给你一个 k k k 次多项式 f ( x ) f(x) f(x) 的每一项系数 a 0 , … , a k a_0,\dots,a_k a0,,ak。对于一个度数为 d d d 的点,它的贡献为 f ( d ) ( m o d m o d ) f(d) \pmod{mod} f(d)(modmod)。你可以给编号为 1 ∼ n 1 \sim n 1n 的点之间任意连边,求所有生成树中所有点贡献和最大的方案,输出最大贡献和以及一组构造。

1 ≤ n ≤ 3000 , 0 ≤ k ≤ 10 , m o d = 59393 1 \leq n \leq 3000, 0 \leq k \leq 10, mod = 59393 1n3000,0k10,mod=59393

分析:
首先预处理出 i ∈ [ 1 , n ] i\in [1, n] i[1,n] 的所有 w i = f ( i ) w_i = f(i) wi=f(i)
注意到任意一个 P r u f e r Prufer Prufer 序列都能构造出一棵树,并且在这棵树上一个点的度数为它在序列中的出现次数加一。因此有一个暴力的 d p dp dp
f i , j f_{i, j} fi,j 表示考虑了编号为 1 ∼ i 1 \sim i 1i 的点, 这些点在 P r u f e r Prufer Prufer 序列的出现总次数为 j j j 的最大代价。转移枚举 i + 1 i + 1 i+1 的出现次数 k k k 即可。最后的答案就是 f n , n − 2 f_{n, n - 2} fn,n2
但是这样 d p dp dp 的复杂度为 O ( n 3 ) O(n^3) O(n3),考虑优化:
注意到我们不关心每个点的出现次数,只关心 每种出现次数有几个点。 所以可以以出现次数为阶段 d p dp dp
f i , j f_{i, j} fi,j 表示考虑了 1 ∼ i 1 \sim i 1i 这些出现次数,当前总出现次数为 j j j 的最大代价。
那么转移是: f i , j = max ⁡ k = 0 ⌊ j i ⌋ ( f i − 1 , j − i × k + k × w i + 1 ) \Large{f_{i, j} = \max\limits_{k = 0}^{\left \lfloor \frac{j}{i} \right \rfloor}}(f_{i - 1, j - i \times k}+k \times w_{i + 1}) fi,j=k=0maxij(fi1,ji×k+k×wi+1)

现在转移复杂度就变成 O ( n ln ⁡ n ) O(n\ln n) O(nlnn),总复杂度 O ( n 2 ln ⁡ n ) O(n^2\ln n) O(n2lnn)
但是好像有个问题,我们没有计算度数为 1 1 1 的点的贡献,并且记录的出现次数也没办法体现有多少个点的度数不是 1 1 1
肯定不能多加一维来记录。我们考虑初始令 f 0 , 0 = n × w 1 f_{0, 0} = n \times w_1 f0,0=n×w1,然后每次转移除了 + k × w i +k\times w_{i} +k×wi 还要 − k × w 1 -k\times w_1 k×w1 表示把这 k k k 个点的代价换掉。
这样就对了。构造只需要对每个状态记录前驱,然后任意拿一些编号构造 p r u f e r prufer prufer 序列即可。

复杂度 O ( n 2 ln ⁡ n ) O(n^2 \ln n) O(n2lnn)
CODE:

// prufer 序列与有标号无根树双射的好处:任意 prufer 序列都能构造出一棵树。因此只需要考虑 prufer 序列最优即可
// 对每个点去 dp 它在 prufer 序列出现多少次,复杂度 n^3 优化不了。
// 但是我们只关心每种次数有多少个。 对每个次数 dp 复杂度就降到了 n^2 ln n
#include<bits/stdc++.h>
using namespace std;
const int N = 4010;
const int mod = 59393;
int n, k, a[N];
int val[N], f[N][N], pre[N][N]; // f[i][j] 表示考虑了前 1~i 这些次数, 当前用的总次数为 j 的最优值
int fa[N], prufer[N], tot, p, deg[N];
inline void get_prufer(int x, int y) {if(!x) return ;if(pre[x][y] < y) {for(int i = 1; i <= (y - pre[x][y]) / x; i ++ ) {for(int j = 1; j <= x; j ++ ) prufer[++ tot] = p;p ++;}}get_prufer(x - 1, pre[x][y]);
}
inline void PT() {prufer[n - 1] = n;for(int i = 1; i <= n - 1; i ++ ) deg[prufer[i]] ++;int p = 1;for(int i = 1; i <= n - 1;) {int j = i;while(deg[p]) p ++; fa[p] = prufer[j ++], deg[fa[p]] --;int tp = p;while(j <= n - 1 && !deg[fa[tp]] && fa[tp] < p) tp = fa[tp], fa[tp] = prufer[j ++], deg[fa[tp]] --;i = j; p ++;}
}
int main() {scanf("%d%d", &n, &k);for(int i = 0; i <= k; i ++ ) scanf("%d", &a[i]);for(int i = 0; i <= n; i ++ ) {int v = 1;for(int j = 0; j <= k; j ++ ) {val[i] = (val[i] + v * a[j] % mod) % mod;v = v * i % mod;}}if(n == 1) {printf("0 %d\n", val[0]); return 0;}memset(f, 0xcf, sizeof f); f[0][0] = n * val[1]; // 初始所有度数都是 1for(int i = 1; i <= n - 2; i ++ ) {for(int j = 0; j <= n - 2; j ++ ) {for(int k = 0; k * i <= j; k ++ ) {if(f[i - 1][j - k * i] + k * val[i + 1] - k * val[1] > f[i][j]) {f[i][j] = f[i - 1][j - k * i] + k * val[i + 1] - k * val[1];pre[i][j] = j - k * i;}}}}printf("%d %d\n", n - 1, f[n - 2][n - 2]);p = 1;get_prufer(n - 2, n - 2);PT();for(int i = 1; i < n; i ++ ) printf("%d %d\n", i, fa[i]);return 0;
}

CF156D Clues

题意:
给定一张 n n n 个点, m m m 条边的有标号无向图,它有 k k k 个连通块,求添加 k − 1 k - 1 k1 条边使得图联通的方案数。答案对 p p p 取模。

1 ≤ n , m ≤ 10 5 , 1 ≤ p ≤ 10 9 1 \leq n,m \leq 10^5, 1 \leq p \leq 10^9 1n,m105,1p109

分析:
将连通块缩成一个点,就转换成了类似完全图求生成树的问题。
如果用连通块的编号来构造 P r u f e r Prufer Prufer 序列,那么答案就是 k k − 2 k^{k - 2} kk2。这显然不对,原因在于一个连通块编号实际上代表了 s i z e size size 个点。
如果对于每一种以连通块编号构造的 P r u f e r Prufer Prufer 序列,我们都把每个连通块的编号换成任意连通块内的点的编号。那么这实际上就等于将所有点的编号拿出来构造长为 k − 2 k - 2 k2 P r u f e r Prufer Prufer 序列,方案数为 n k − 2 n^{k - 2} nk2
这对不对呢?很遗憾,它也是不对的。
我们来尝试理解这样得到的 P r u f e r Prufer Prufer 序列有什么实际含义:相当于每次取出度数为 0 0 0 的编号最小的连通块,然后把这个连通块和 P r u f e r Prufer Prufer 序列开头编号的连通块相连,并且根据 P r u f e r Prufer Prufer 序列我们知道连向的点的编号就是序列开头数字。
然后你就能发现问题所在了:我们不知道当前的 “叶子” 连出去的点编号是什么!!
因此开始的时候将答案乘上 ∏ s i z e i \prod size_i sizei 表示先对每个连通块钦定一个往外连出去的点的编号,然后再乘上 n k − 2 n^{k - 2} nk2 就行。

最后答案就是 n k − 2 ∏ s i z e i n^{k - 2}\prod size_i nk2sizei。复杂度线性。

CODE:

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
typedef long long LL;
int n, m, mod, sz[N], bin[N];
int Find(int x) {return x == bin[x] ? x : bin[x] = Find(bin[x]);}
inline void Merge(int u, int v) {int f1 = Find(u), f2 = Find(v);if(f1 != f2) sz[f2] += sz[f1], bin[f1] = f2;
}
inline LL Pow(LL x, LL y) {LL res = 1, k = x;while(y) {if(y & 1) res = res * k % mod;y >>= 1;k = k * k % mod;}return res;
}
int main() {ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);cin >> n >> m >> mod;for(int i = 1; i <= n; i ++ ) bin[i] = i, sz[i] = 1;for(int i = 1; i <= m; i ++ ) {int u, v; cin >> u >> v;Merge(u, v);}LL res = 1; int cnt = 0;for(int i = 1; i <= n; i ++ ) {if(Find(i) == i) res = res * sz[i] % mod, cnt ++;}if(cnt == 1) {cout << (1 % mod) << endl; return 0;}else {		res = res * Pow(n, cnt - 2) % mod;cout << res << endl;}return 0;
}

[ARC106F] Figures

题意:
N N N 个点,每个点有 d i d_i di互不相同、可被区分 的孔,每次可以选择两个不同点,连接两个未被连接过的孔,有多少种方案使得最后形成一棵树。合法方案中可以不把孔填满。答案对 998244353 998244353 998244353

2 ≤ N ≤ 2 × 10 5 , 1 ≤ d i < 998244353 2 \leq N \leq 2 \times 10^5, 1\leq d_i < 998244353 2N2×105,1di<998244353

分析:
和上道题非常类似,只是一个联通块中的一个点只能被连接一次,上一道题是可以连接任意次。
一样的思考方式,如果我们把所有孔的编号拿出来,有 M = ∑ d i M = \sum d_i M=di 个孔,用 A M N − 2 A_{M}^{N - 2} AMN2 来生成 P r u f e r Prufer Prufer 序列。
这样还是不正确的,错误原因仍然是每个点作为叶子连出去时没确定一个孔。
如果最后考虑这个孔是谁,我们还需要知道每个点有多少个孔在 P r u f e r Prufer Prufer 序列出现,这不太好。
还是最开始就考虑这个孔是谁,先乘上 ∏ d i \prod d_i di 的系数,然后这些孔就不能用了,因此我们用 A M − N N − 2 A_{M - N}^{N - 2} AMNN2 来构建 P r u f e r Prufer Prufer 序列。这样就对了,答案就是 A M − N N − 2 ∏ d i A_{M - N}^{N - 2}\prod d_i AMNN2di

复杂度线性。

CODE:

#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
typedef long long LL;
const LL mod = 998244353;
int n, d[N];
LL res = 1;
inline LL A(LL n, LL m) {if(n < m) return 0;LL res = 1;for(LL i = n; i > n - m; i -- ) res = res * (i % mod) % mod;return res;
}
int main() {scanf("%d", &n); LL s = 0;for(int i = 1; i <= n; i ++ ) {scanf("%d", &d[i]); s += d[i] - 1;res = res * d[i] % mod;}res = res * A(s, n - 2) % mod; cout << res << endl;return 0;
}

相关文章:

  • SNMPv3 的安全命名空间详解
  • 需要做一款小程序,用来发券,后端如何进行设计能够保证足够安全?
  • MySQL 多表查询、事务
  • 交叉熵损失函数的优势
  • Java常见异常详解及解决方案(九)
  • 【Git】关于项目开发分支的使用规范
  • 分库分表之优缺点分析
  • 结合 STM32CubeMX 使用 FreeRTOS 实时操作系统
  • XXE(XML外部实体注入)详解
  • 基于集体智能长尾识别的超声乳腺病变亚型分类|文献速递-深度学习医疗AI最新文献
  • [Jenkins在线安装]
  • 关于机器学习中迁移学习与深度学习的思考
  • CMake基础:常用内部变量和环境变量的引用
  • ntfs!CcGetDirtyPages函数分析之DirtyPages=0x1和TargetAttribute=0xe0的一个例子
  • 借助AI学习编程,走向架构师之路
  • AntV F2入门教程
  • OpenCV CUDA模块设备层---- 绝对值函数abs()
  • HarmonyOS 5 原子化服务卡片测试全攻略
  • 探究webView与html的通讯
  • LLaVA-Med常见问题解决方案
  • 网站设计排版怎么做/北京企业网络推广外包
  • 安阳网站优化/seo 优化一般包括哪些内容
  • 个人网站模板/汕头网站建设方案开发
  • phpweb手机网站程序/seo网站排名优化服务
  • 网站建站专业/合肥seo网站建设
  • 手机网站表单页面制作/企业网站推广策略