文章目录
- P r u f e r Prufer Prufer 序列
- 应用
- Cayley 公式
- [HNOI2004] 树的计数
- 「雅礼集训 2017 Day8」共
- [THUPC 2018] 城市地铁规划
- CF156D Clues
- [ARC106F] Figures
P r u f e r Prufer Prufer 序列
P r u f e r Prufer Prufer 序列可以将一棵 n n n 个点的有编号无根树 转化成一个长度为 n − 2 n - 2 n−2,值域在 [ 1 , n ] [1, n] [1,n] 的序列。也可以将 任意一个 长度为 n − 2 n - 2 n−2,值域在 [ 1 , n ] [1, n] [1,n] 的序列转化成一棵 有编号无根树。可以理解成有标号完全图的生成树与数列之间的双射,常用于对树计数的问题。
对树建立 P r u f e r Prufer Prufer 序列
建立过程:
每次找到编号最小的叶子,往序列末尾加入它所连接的节点编号,然后删掉这个叶子。重复 n − 2 n - 2 n−2 次后只剩下两个节点结束。
实现方式:
考虑维护指针 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 deg−1 次,没有出现过的就是叶子节点。
从上述建立过程可以看出来:
任意一棵 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 n−2,值域在 [ 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 n−2 次。剩下两个叶子,把它们之间连一条边即可。
实现方式:
注意到最后剩下的两个叶子也一定有一个是 n n n,我们将 P r u f e r Prufer Prufer 序列的末尾补上一个 n n n,然后重复上述过程 n − 1 n - 1 n−1 次就可以连出 n − 1 n - 1 n−1 条边。
与建立 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 n−2,值域在 [ 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 1∼n 的无根树与长度为 n − 2 n - 2 n−2,值域为 [ 1 , n ] [1, n] [1,n] 的序列双射。
应用
Cayley 公式
n n n 个点有编号的完全图生成树个数为 n n − 2 n^{n - 2} nn−2。
证明:双射即可。
[HNOI2004] 树的计数
题意:
给定 n n n 个点的度数 d 1 , … d n d_1,\dots d_n d1,…dn,任意两个点之间可以连边,问有多少个满足度数数组的生成树。
1 ≤ n ≤ 150 1 \leq n \leq 150 1≤n≤150。
分析:
特判 n = 1 n = 1 n=1 的边界情况。那么任意一个点的度数不能为 0 0 0,并且所有点的度数和应为 2 n − 2 2n - 2 2n−2。
一个点只要在 P r u f e r Prufer Prufer 序列中出现 d i − 1 d_i - 1 di−1 即满足构造出来的树中度数为 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", °[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 1∼n,以 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 1≤k<n≤5×105。
分析:
显然可以将点按照深度的奇偶性分成两类,这将树变成了一张二分图。
将 1 1 1 划分到左部点中,那么需要从剩下的 n − 1 n - 1 n−1 个编号里选出 k − 1 k - 1 k−1 个分到左部点。答案就是 ( n − 1 k − 1 ) × S ( k , n − k ) \binom{n - 1}{k - 1} \times S(k, n - k) (k−1n−1)×S(k,n−k)。
其中 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} ab−1ba−1。
证明:
对于这样二分图的生成树, P r u f e r Prufer Prufer 序列构造过程的末尾一定剩下一个左部点和一个右部点。因此它的 P r u f e r Prufer Prufer 序列一定有 b − 1 b - 1 b−1 个左部点和 a − 1 a - 1 a−1 个右部点。
对于这两部分的构成的子序列内部,如果确定了编号和顺序。那么两部分之间在 P r u f e r Prufer Prufer 序列上的顺序就定下来了。因此总方案数就是 a b − 1 b a − 1 a^{b - 1}b^{a - 1} ab−1ba−1。
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 1∼n 的点之间任意连边,求所有生成树中所有点贡献和最大的方案,输出最大贡献和以及一组构造。
1 ≤ n ≤ 3000 , 0 ≤ k ≤ 10 , m o d = 59393 1 \leq n \leq 3000, 0 \leq k \leq 10, mod = 59393 1≤n≤3000,0≤k≤10,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 1∼i 的点, 这些点在 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,n−2。
但是这样 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 1∼i 这些出现次数,当前总出现次数为 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=0max⌊ij⌋(fi−1,j−i×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 k−1 条边使得图联通的方案数。答案对 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 1≤n,m≤105,1≤p≤109。
分析:
将连通块缩成一个点,就转换成了类似完全图求生成树的问题。
如果用连通块的编号来构造 P r u f e r Prufer Prufer 序列,那么答案就是 k k − 2 k^{k - 2} kk−2。这显然不对,原因在于一个连通块编号实际上代表了 s i z e size size 个点。
如果对于每一种以连通块编号构造的 P r u f e r Prufer Prufer 序列,我们都把每个连通块的编号换成任意连通块内的点的编号。那么这实际上就等于将所有点的编号拿出来构造长为 k − 2 k - 2 k−2 的 P r u f e r Prufer Prufer 序列,方案数为 n k − 2 n^{k - 2} nk−2。
这对不对呢?很遗憾,它也是不对的。
我们来尝试理解这样得到的 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} nk−2 就行。
最后答案就是 n k − 2 ∏ s i z e i n^{k - 2}\prod size_i nk−2∏sizei。复杂度线性。
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 2≤N≤2×105,1≤di<998244353。
分析:
和上道题非常类似,只是一个联通块中的一个点只能被连接一次,上一道题是可以连接任意次。
一样的思考方式,如果我们把所有孔的编号拿出来,有 M = ∑ d i M = \sum d_i M=∑di 个孔,用 A M N − 2 A_{M}^{N - 2} AMN−2 来生成 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} AM−NN−2 来构建 P r u f e r Prufer Prufer 序列。这样就对了,答案就是 A M − N N − 2 ∏ d i A_{M - N}^{N - 2}\prod d_i AM−NN−2∏di。
复杂度线性。
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;
}