繁凡出品的全新系列:解题报告系列 —— 超高质量算法题单,配套我写的超高质量题解和代码,题目难度不一定按照题号排序,我会在每道题后面加上题目难度指数( 1 ∼ 5 1 \sim 5 1∼5),以模板题难度 1 1 1 为基准。
这样大家在学习算法的时候就可以执行这样的流程:
%
阅读我的【学习笔记】 / 【算法全家桶】学习算法 ⇒ \Rightarrow ⇒ 阅读我的相应算法的【解题报告】获得高质量题单 ⇒ \Rightarrow ⇒ 根据我的一句话题解的提示尝试自己解决问题 ⇒ \Rightarrow ⇒ 点开我的详细题解链接学习巩固(好耶)%
解题报告系列合集:【解题报告系列】超高质量题单 + 题解(ICPC / CCPC / NOIP / NOI / CF / AT / NC / P / BZOJ)
本题单前置知识:【学习笔记】树的计数,prufer(Prüfer)编码,Cayley公式及相应例题
1. prufer \text{prufer} prufer 序列
Prufer数列是无根树的一种数列。在组合数学中,Prufer数列由有一个对于顶点标过号的树转化来的数列,点数为n的树转化来的Prufer数列长度为n-2。
2. prufer \text{prufer} prufer 序列的性质
性质1: prufer \text{prufer} prufer 序列与无根树一一对应。
显然
性质2: 度数为 d i d_i di 的节点会在 prufer \text{prufer} prufer 序列中出现 d i − 1 d_i-1 di−1 次。
当某个节点度数为 1 1 1 时,会直接被删掉,否则每少掉一个相邻的节点,它就会在序列中出现 1 1 1 次。
因此共出现 d i − 1 d_i-1 di−1 次。
性质3: 一个 n 个节点的完全图的生成树个数为 n n − 2 n^{n-2} nn−2 (Cayley 公式)。
对于一个 n n n 个点的无根树,它的 prufer \text{prufer} prufer 序列长为 n − 2 n-2 n−2 ,而每个位置有 n n n 种可能性,因此可能的 prufer \text{prufer} prufer 序列有 n n − 2 n^{n-2} nn−2 种。
又由于 prufer \text{prufer} prufer 序列与无根树一一对应,因此生成树个数应与 prufer \text{prufer} prufer 序列种树相同,即 n n − 2 n^{n-2} nn−2 。
性质4: 对于给定度数为 d 1 ∼ n d_{1\sim n} d1∼n 的一棵无根树共有 ( n − 2 ) ! ∏ i = 1 n ( d i − 1 ) ! \cfrac{(n-2)!}{\prod_{i=1}^n(d_i-1)!} ∏i=1n(di−1)!(n−2)! 种情况。
由上面的性质可以知道,度数为 d i d_i di 的节点会在 prufer \text{prufer} prufer 序列中出现 d i − 1 d_i-1 di−1 次。
则就是要求出 d i − 1 d_i-1 di−1 个 i ( 1 ≤ i ≤ n ) i(1\le i\le n) i(1≤i≤n)的全排列个数。
而上面那个式子就是可重全排列公式。(即全排列个数除以重复元素内部的全排列个数)
3. Cayley \text{Cayley} Cayley 公式的推论
-
n n n 个无序点的有根树个数为 n n − 1 n^{n-1} nn−1
-
n n n 个无序点的无根树个数为 n n − 2 n^{n-2} nn−2
-
n n n 个有序点的有根树个数为 n n − 1 × ( n − 1 ) ! n^{n-1}\times (n-1)! nn−1×(n−1)!
-
n n n 个有序点的无根树个数为 n n − 2 × ( n − 1 ) ! n^{n-2}\times (n-1)! nn−2×(n−1)!
A、(P6086 【模板】)Prufer 序列
Weblink
https://www.luogu.com.cn/problem/P6086
Problem
请实现 Prufer 序列和无根树的相互转化。
为方便你实现代码,尽管是无根树,我们在读入时仍将 n 设为其根。
对于一棵无根树,设 f 1 … n − 1 f_{1\dots n-1} f1…n−1 为其父亲序列( f i f_i fi 表示 i 在 n 为根时的父亲),设 p 1 … n − 2 p_{1 \dots n-2} p1…n−2 为其 Prufer 序列。
另外,对于一个长度为 m 的序列 a 1 … m a_{1 \dots m} a1…m ,我们设其权值为 xor i = 1 m i × a i \operatorname{xor}_{i = 1}^m i \times a_i xori=1mi×ai 。
Solution
由无根树得到其 prufer 序列:
找到编号最小的度数为1的点
删除该节点并在序列中添加与该节点相连的节点的编号
重复1,2操作,直到整棵树只剩下两个节点
双指针 O ( n ) O(n) O(n) 实现: 指针指向编号最小的叶节点。每次删掉它之后,如果产生了新的叶节点且编号比指针指向的更小,则直接继续删掉,否则自增找到下一个编号最小的叶节点。
由 prufer 序列还原该无根树:
每次取出prufer序列中最前面的元素u
在点集中找到编号最小的没有在prufer序列中出现的元素v
给u,v连边然后分别删除
最后在点集中剩下两个节点,给它们连边
双指针 O ( n ) O(n) O(n) 实现: 指针指向编号最小的度数为 1 的节点。每次将它与当前枚举到的 Prufer 序列的点连接之后,如果产生了新的度数为 1 的节点且编号比指针指向的更小,则直接继续将它与下一个 Prufer 序列的点连接,否则自增找到下一个编号最小的度数为 1 的节点。
Code
const int N = 6e6 + 7, mod = 1e9 + 7;
int n, m, t;
int p[N], f[N], d[N];
ll ans = 0;
void TtoP()
{
for(int i = 1; i <= n - 1; ++ i) {
scanf("%d", &f[i]);
d[f[i]] ++ ;
}
for(int i = 1, j = 1; i <= n - 2; ++ i, ++ j) {
while(d[j]) ++ j;//从小到大找到第一个度数为 1 (d[j] == 0) 的结点
p[i] = f[j];//将父结点放入 prufer 序列里
while(i <= n - 2 && -- d[p[i]] == 0 && p[i] < j)
p[i + 1] = f[p[i]], ++ i;
}
ans = 1ll * p[1];
for(int i = 2; i <= n - 2; ++ i) {
ans ^= 1ll * i * p[i];
}
}
void PtoT()
{
for(int i = 1; i <= n - 2; ++ i) {
scanf("%d", &p[i]);
d[p[i]] ++ ;
}
p[n - 1] = n;
for(int i = 1, j = 1; i <= n - 1; ++ 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;
}
ans = 1ll * f[1];
for(int i = 2; i <= n - 1; ++ i)
ans ^= 1ll * i * f[i];
}
int main()
{
scanf("%d%d", &n, &m);
if(m == 1) TtoP();
else PtoT();
printf("%lld\n", ans);
return 0;
}
B、(P2290 [HNOI2004])树的计数
Weblink
https://www.luogu.com.cn/problem/P2290
Problem
一个有 n n n 个节点的树,设它的节点分别为 v 1 , v 2 , … , v n v_1,v_2,\ldots,v_n v1,v2,…,vn ,已知第 i i i 个节点 v i v_i vi 的度数为 d i d_i di ,问满足这样的条件的不同的树有多少棵。
Solution
由 性质4: 对于给定度数为 d 1 ∼ n d_{1\sim n} d1∼n 的一棵无根树共有 ( n − 2 ) ! ∏ i = 1 n ( d i − 1 ) ! \cfrac{(n-2)!}{\prod_{i=1}^n(d_i-1)!} ∏i=1n(di−1)!(n−2)! 种情况。我们可以直接用公式求解。但是这个公式可能会爆 long long ,不想写高精,所以考虑另一种思路。
我们发现题目其实可以转化成一共有 n − 2 n-2 n−2 个位置,要放 n n n 个数,其中数 i i i 要占 d [ i ] − 1 d[i]-1 d[i]−1 个位置。显然答案为: a n s = ∏ i = 1 n C d [ i ] − 1 s u m ans \ = \prod_{i=1}^n\ C_{d[i] - 1} ^ {sum} ans =∏i=1n Cd[i]−1sum ,其中 s u m sum sum 为剩余的位置数。
最后需要特判一下不合法的情况:
-
图不合法,即有点的度数为 0 0 0 ,说明不是连通图,也就不是树,输出 0 0 0。
-
prufer序列不合法,即 ∑ d i − 1 ! = n − 2 \sum d_i-1!=n-2 ∑di−1!=n−2
-
若 n = 1 n=1 n=1 ,当 d 1 = 0 d_1=0 d1=0 合法,输出
1
,否则不合法,输出0
。
Code
const int N = 507;
ll c[N][N];
int n, m;
ll d[N], sum;
void solve()
{
scanf("%d", &n);
if(n == 1) {
int d;
scanf("%lld", &d);
if(d == 0)
puts("1");
else puts("0");
return ;
}
bool flag = 0;
for(int i = 1; i <= n; ++ i) {
scanf("%lld", &d[i]);
if(d[i] == 0) flag = 1;
sum += d[i] - 1;
}
if(sum != n - 2 || flag) {
puts("0");
return ;
}
c[0][0] = c[1][0] = c[1][1] = 1;
for(int i = 2; i <= sum; ++ i) {
c[i][0] = 1;
for(int j = 1; j <= sum; ++ j) {
c[i][j] = c[i - 1][j - 1] + c[i - 1][j];
}
}
ll ans = 1;
for(int i = 1; i <= n; ++ i) {
ans *= c[sum][d[i] - 1];
sum -= d[i] - 1;
}
printf("%lld\n", ans);
return ;
}
int main()
{
solve();
}
C、(CF156D) Clues
Weblink
https://www.luogu.com.cn/problem/CF156D
Problem
给定一个 n n n 个点 m m m 条边的带标号无向图,它有 k k k 个连通块,求添加 k − 1 k-1 k−1 条边使得整个图连通的方案数,答案对 p p p 取模。
Solution
结论: a n s = n k − 2 × ∏ i = 1 k s i \displaystyle ans=n^{k-2}\times \prod_{i=1}^ks_i ans=nk−2×i=1∏ksi
其中 s i s_i si 为第 i i i 个连通块的点数。
我们直接用并查集计算一下连通块的点的数量即可。
Code
const int N = 1e5 + 7, mod = 1e9 + 7;
int n, m, p, k;
int fa[N];
int s[N];
int qpow(int a, int b)
{
int res = 1;
while(b) {
if(b & 1) res = 1ll * res * a % p;
a = 1ll * a * a % p;
b >>= 1;
}
return res;
}
int Find(int x)
{
if(fa[x] == x) return x;
return fa[x] = Find(fa[x]);
}
void Union(int x, int y)
{
int fx = Find(x), fy = Find(y);
fa[fx] = fy;
}
int main()
{
scanf("%d%d%d", &n, &m, &p);
if(p == 1) return puts("0"), 0;
for(int i = 1; i <= n; ++ i)
fa[i] = i;
for(int i = 1; i <= m; ++ i) {
int x, y;
scanf("%d%d", &x, &y);
Union(x, y);
}
for(int i = 1; i <= n; ++ i)
s[Find(i)] ++ ;
ll ans = 1;
for(int i = 1; i <= n; ++ i)
if(i == Find(i)) ++ k, ans = (ans * 1ll * s[i]) % p;
if(k == 1) return puts("1"), 0;
ans = (ans * 1ll * qpow(n, k - 2)) % p;
printf("%lld\n", ans);
return 0;
}
D、UVA10843 Anne’s game
Weblink
https://www.luogu.com.cn/problem/UVA10843
Problem
A n n e Anne Anne 喜欢玩一个游戏:
- 她在一张纸上画一个圆
- 然后再画一个圆,并用一条线将其与另一个圆连接起来
- 接着再画一个圆,并用一条线将其与前两个圆中的任意一个连接起来
- 重复上述操作,直至她画了 n n n 个圆,且每个圆都与先前绘制的任意一个圆连接,所有圆都不相交,且每一条线也不相交
- 最后,她在这些圆中随机填入 1 1 1 ~ n n n 这些数字(每个圆仅填入一个数字)
那么, A n n e Anne Anne 可以得到多少种不同的图?
两幅图不同的条件:其中一幅图有一条线连接了编号为 i i i 和编号为 j j j 的圆,而另一幅图没有。
1 < n ≤ 100 1<n\le100 1<n≤100
Solution
随机填入编号 —— 无序点, n n n 个无序点的无根树个数为 n n − 2 n^{n-2} nn−2。
直接输出即可。
Code
const int N = 1e5 + 7, p = 2000000011;
int n, m, k, kcase, t;
int qpow(int a, int b)
{
int res = 1;
while(b) {
if(b & 1) res = 1ll * res * a % p;
a = 1ll * a * a % p;
b >>= 1;
}
return res;
}
int main()
{
scanf("%d", &t);
while(t -- ) {
scanf("%d", &n);
if(n <= 2) {
printf("Case #%d: 1\n", ++ kcase);
continue;
}
printf("Case #%d: %d\n", ++ kcase, qpow(n, n - 2));
}
return 0;
}
E、(P4430) 小猴打架
Problem
一开始森林里面有N只互不相识的小猴子,它们经常打架,但打架的双方都必须不是好朋友。每次打完架后,打架的双方以及它们的好朋友就会互相认识,成为好朋友。经过N-1次打架之后,整个森林的小猴都会成为好朋友。 现在的问题是,总共有多少种不同的打架过程。 比如当N=3时,就有{1-2,1-3}{1-2,2-3}{1-3,1-2}{1-3,2-3}{2-3,1-2}{2-3,1-3}六种不同的打架过程。
Solution
其实就是有 n n n 个结点,经过 n − 1 n-1 n−1 次连边形成一棵树,问有多少种连边过程,不止是连边的方案数,最后生成的树的不同方案也算方案数。
首先显然 n n n 个无序点的无根树的方案数为 n n − 2 n^{n-2} nn−2,并且有 n − 1 n-1 n−1 次连边,也就是有 ( n − 1 ) ! (n-1)! (n−1)! 种连边方案,总方案数为 ( n − 1 ) ! × n n − 2 (n-1)!\times n^{n-2} (n−1)!×nn−2。
const int N = 1e5 + 7, mod = 9999991;
int n, m, ans;
int main()
{
ans = 1;
scanf("%d", &n);
for(int i = 1; i <= n - 2; ++ i) ans = 1ll * ans * n % mod;
for(int i = 1; i <= n - 1; ++ i) ans = 1ll * ans * i % mod;
printf("%d\n", ans);
return 0;
}
F、(BZOJ1005) 明明的烦恼
Problem
给出标号为 1 到 N 的点,以及某些点最终的度数,允许在任意两点间连线,可产生多少棵度数满足要求的树?
1 ≤ n ≤ 1000 1\le n\le 1000 1≤n≤1000
Solution
https://www.cnblogs.com/zhj5chengfeng/archive/2013/08/23/3278557.html