[CSP-S2019 江西] 多叉堆
[CSP-S2019 江西] 多叉堆
题意:
给定 \(n\) 个节点,初始独立,有 \(q\) 次操作:
1,x,y
选择 \(x\) , \(y\),将以 \(x\) 为根的子树合并到 \(y\)2 x
选择节点 \(x\) ,求节点 \(x\) 所在的树成 小根堆 的概率
分析:
我看题解大部分都是用组合数意义做的,我这个是用概率方面的知识写的。
小根堆:
什么情况下一棵树会形成小根堆?当然是每个点作为根节点时,其儿子节点的值大于这个点。
假如说这棵树只有一层,树的大小为 \(x\) ,每个节点赋值 \([1,x]\) ,形成小根堆的概率就是 \(\frac{1}{size[x]}\)
所以,我们可以拓展到更加复杂的树:
设一棵树为 \(E\) ,将这棵树每个节点赋值 \([1,size[E]]\) ,其形成小根堆概率为:
\[P(E)=\prod_{x \in E}\frac{1}{size[x]}
\]
这些数一共有 \(size[E]!\) 次填法,因此产生的小根堆次数为:
\[size[E]! \times P(E)
\]
因此,我们记录一下每个节点对应的根对应的形成小根堆的概率 \(P\),以及这个树的大小 \(size\)。
询问时,利用 并查集 搜索这个节点对应的根,运用公式输出即可。
但是,我并没有讲怎么合并两个树,接下来就是合并的部分:
合并两棵树:
先用一张图:
此时,\(x\) 要合并到 \(y\) 上,利用并查集合并。
子树大小好弄,也就是
size[y]+=size[x];
但是怎么合并概率呢?
看树的大小:此时,只有以 \(y\) 为根对应的子树大小改变了,而其他都没有改变。
因此,我们要去除 \(\frac{1}{size[y]}\) 的贡献,增加 \(\frac{1}{size[x]+size[y]}\) 的贡献。
同时,还要增加树 \(x\) 的贡献.
因为不能用除法,因此需要用到 逆元,根据 求逆元 的相关知识,去除一个数在逆元中的贡献,等于乘这个数的倒数
合并可以写成以下式子:
P[y]=P[y]*P[x]*size[y]*inv[sizes[x]+sizes[y]];
其中,\(inv\) 为逆元。
这样,我们把子树 \(x\) 父亲设成 \(y\) ,合并 概率 \(P\) 和 大小 \(size\) , 查询时利用公式输出即可。
代码:
// P5689 [CSP-S2019 江西] 多叉堆
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=6e5+5,mod=1e9+7;
int n,q;
int fa[N],sizes[N],ans;
int inv[N],mul[N],fac[N],now;
int P[N];
void init(int n){
inv[1]=mul[1]=1;
for(int i=2;i<=n;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;
for(int i=2;i<=n;i++) mul[i]=(mul[i-1]*i)%mod;
}
int get(int x){
if(x==fa[x]) return x;
else return fa[x]=get(fa[x]);
}
signed main(){
cin>>n>>q;
for(int i=0;i<n;i++) fa[i]=i,sizes[i]=1,P[i]=1;
init(300000);
while(q--){
int op,x,y; scanf("%lld",&op);
if(op==1){
scanf("%lld%lld",&x,&y); x=(x+ans)%n,y=(y+ans)%n;
int fx=get(x),fy=get(y);
P[fy]=P[fy]*P[fx]%mod*sizes[fy]%mod*inv[sizes[fx]+sizes[fy]]%mod;
fa[fx]=fy;
sizes[fy]+=sizes[fx];
}
else{
scanf("%lld",&x); x=(x+ans)%n;
int now=get(x);
ans=P[now]*mul[sizes[now]]%mod;
printf("%lld\n",ans);
}
}
system("pause");
return 0;
}
不关注的有难了😠😠😠https://b23.tv/hoXKV9