【数学】prufer 序列
题目描述
请实现 Prüfer 序列和无根树的相互转化。
为方便你实现代码,尽管是无根树,我们在读入时仍将 \(n\) 设为其根。
对于一棵无根树,设 \(f_{1\dots n-1}\) 为其父亲序列(\(f_i\) 表示 \(i\) 在 \(n\) 为根时的父亲),设 \(p_{1 \dots n-2}\) 为其 Prüfer 序列。
另外,对于一个长度为 \(m\) 的序列 \(a_{1 \dots m}\),我们设其权值为 \(\operatorname{xor}_{i = 1}^m i \times a_i\)。
\(1 \leq n \leq 5 \times 10^6\) 。
算法描述
prufer 序列,用于将一个 \(n\) 个点的有标号无根树映射到一个长为 \(n - 2\) 的序列上。下面讲解实现过程。
无根树转化为 prufer 序列
考虑每次在叶节点当中选择编号最小的那一个,删除它和它连的边,在 prufer 序列中加入它指向的那个点,直到只有两个点为止。
我们发现,对于任意一棵无根树,一定可以转化为一个序列(树总有叶节点),而且过程当中每一步都是唯一的,所以树可以映射到序列上。
这样做用堆维护是 \(\Theta(n \log n)\) 的。
事实上可以用指针简单的代替这一过程,只需要每次右移,如果不是叶节点就不管,如果是就删掉,观察删掉过后有没有可能产生新的叶节点,并且如果产生了,这个点编号是否小于当前的指针,如果是的话不移动指针,继续删这个点就好了。
这样就解决了 “编号最小” 的问题,由于每个点只会被删一次,所以是 \(\Theta(n)\) 的。
inline void Tree_to_prufer()
{
for(int i = 1;i <= n - 1;i++) deg[fa[i]]++,deg[i]++;
int cnt = 0;
for(int tp = 1;tp <= n;tp++)
{
if(deg[tp] > 1) continue;
int now = tp;
do{
p[++cnt] = fa[now];
deg[fa[now]]--; deg[now]--;
if(deg[fa[now]] == 1) now = fa[now];
else now = n + 2;
}while(now < tp);
}
}
prufer 序列转化为无根树
我们研究上面的过程,第一步一定是选了 prufer 序列的第一个点和最小的叶节点连边。
prufer 序列一个显然的性质就是无根树上 \(x\) 的度数等于它在 prufer 序列中出现的次数 \(+1\) 。
所以我们可以通过序列得到点的度数,就可以知道第一步时,谁是最小的叶节点,这个点也是唯一的。
所以我们直接维护,连一条 \(p_1 \to leaf\) 的边,然后将两个度数都减 \(1\),类似上面的过程,每次找到编号最小的叶节点即可,优化方法和上面一样。
考虑结束以后,我们才连了 \(n - 2\) 条边,但是总共有 \(n - 1\) 条边。
我们发现 “编号最小的叶子” 这个事情怎么样也轮不到 \(n\) 。所以剩下的一定是 \(n\) 和另外一个点,连起来即可,也是 \(\Theta(n)\) 的。
inline void Prufer_to_tree()
{
for(int i = 1;i <= n - 2;i++) deg[p[i]]++;
for(int i = 1;i <= n;i++) deg[i]++;
for(int i = 1,tp = 1;i <= n - 2;tp++)
{
if(deg[tp] > 1) continue;
int now = tp;
do{
fa[now] = p[i];
deg[now]--; deg[p[i]]--;
if(deg[p[i]] == 1) now = p[i];
else now = n + 2;
i++;
}while(now < tp && i <= n - 2);
}
for(int i = 1;i < n;i++) if(deg[i] > 0) {fa[i] = n; break;}
}
Code
#include<bits/stdc++.h>
using namespace std;
const int N = 5e6 + 5;
int deg[N],fa[N],p[N],n;
inline void Tree_to_prufer()
{
for(int i = 1;i <= n - 1;i++) deg[fa[i]]++,deg[i]++;
int cnt = 0;
for(int tp = 1;tp <= n;tp++)
{
if(deg[tp] > 1) continue;
int now = tp;
do{
p[++cnt] = fa[now];
deg[fa[now]]--; deg[now]--;
if(deg[fa[now]] == 1) now = fa[now];
else now = n + 2;
}while(now < tp);
}
}
inline void Prufer_to_tree()
{
for(int i = 1;i <= n - 2;i++) deg[p[i]]++;
for(int i = 1;i <= n;i++) deg[i]++;
for(int i = 1,tp = 1;i <= n - 2;tp++)
{
if(deg[tp] > 1) continue;
int now = tp;
do{
fa[now] = p[i];
deg[now]--; deg[p[i]]--;
if(deg[p[i]] == 1) now = p[i];
else now = n + 2;
i++;
}while(now < tp && i <= n - 2);
}
for(int i = 1;i < n;i++) if(deg[i] > 0) {fa[i] = n; break;}
}
int main()
{
int op;
scanf("%d%d",&n,&op);
if(op == 1)
{
for(int i = 1;i <= n - 1;i++) scanf("%d",&fa[i]);
Tree_to_prufer();
long long ans = 0;
for(int i = 1;i <= n - 2;i++) ans ^= 1ll * i * p[i];
printf("%lld",ans);
}
else
{
for(int i = 1;i <= n - 2;i++) scanf("%d",&p[i]);
Prufer_to_tree();
long long ans = 0;
for(int i = 1;i <= n - 1;i++) ans ^= 1ll * i * fa[i];
printf("%lld",ans);
}
return 0;
}
prufer 序列的用法
著名的 Cayley 公式:
\(n\) 个点的有标号无根树的数量是 \(n^{n - 2}\)。
考虑 prufer 序列的值域是 \([1,n]\) ,而且手模可以发现,任意一个长为 \(n - 2\) 的,值域 \([1,n]\) 的数列都可以转化成一个无根树,而且可以用上述方式映射回来,所以这是一个一一对应关系,prufer 序列的数量就是无根树的数量。
相关题目:
P6086 【模板】Prufer 序列 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
P4430 小猴打架 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
上面两道是板题,下面用另一道板题作为例题:
Clues - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
给定一个 \(n\) 个点 \(m\) 条边的带标号无向图,它有 \(k\) 个连通块,求添加 \(k-1\) 条边使得整个图连通的方案数,答案对 \(p\) 取模。
\(1 \leq n \leq 10^5,0 \leq m \leq 10^5,1 \leq p \leq 10^9\)。
设 \(a_i\) 是第 \(i\) 个连通块的大小,考虑枚举每一棵树 \(T\) ,答案就是:
将按照边计数转化为按照点计数:
容易发现,一棵树的 prufer 序列每一项的 \(a\) 值相乘的结果是:
所以答案就是:
所以我们要求所有的 \(k\) 项 \(prufer\) 序列的 \(a\) 值乘积的和,假设 \(f_i\) 为前 \(i\) 项所有序列的和,不难发现最后一项填什么都可以,所以:
所以最终答案就是:
直接跑连通块 \(\Theta(n)\) 计算即可。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int n,m,MOD;
struct Edge{
int v,next;
}e[N * 2];
int vis[N],head[N],tot = 0,a[N],cnt = 0;
typedef long long ll;
ll f[N];
inline void add(int x,int y)
{
++tot;
e[tot].v = y;
e[tot].next = head[x];
head[x] = tot;
}
inline void dfs(int x,int num)
{
++a[num];
vis[x] = 1;
for(int i = head[x];i;i = e[i].next)
{
int to = e[i].v;
if(vis[to]) continue;
dfs(to,num);
}
}
inline ll ksm(ll base,int pts)
{
ll ret = 1;
for(;pts > 0;pts >>= 1,base = base * base % MOD)
if(pts & 1)
ret = ret * base % MOD;
return ret;
}
int main()
{
cin>>n>>m>>MOD;
for(int i = 1,x,y;i <= m;i++)
{
cin>>x>>y;
add(x,y);
add(y,x);
}
if(MOD == 1) {puts("0"); return 0;}
memset(vis,0,sizeof(vis));
for(int i = 1;i <= n;i++) if(!vis[i]) {++cnt; dfs(i,cnt);}
ll prod = 1;
for(int i = 1;i <= cnt;i++) prod = prod * a[i] % MOD;
if(cnt == 1) {puts("1"); return 0;}
cout<<prod * ksm(n,cnt - 2) % MOD;
return 0;
}