题目
思路来源
https://www.cnblogs.com/mountaink/p/10322513.html 按秩合并
https://www.cnblogs.com/dilthey/p/10433419.html 路径压缩
题解
首先,考虑答案怎么算,设被询问的人最终出的是石头
则他作为a次主场赢的人,出的是石头或者剪刀;他作为b次,客场赢的人, 出的是剪刀,
答案是,其中被询问的人放石头,其余按主、客、空位计算
三种情况对称,乘3即可,答案是,
当然也可以直接理解化简式的含义,随便取*用概率限制主场*用概率限制客场
形如1类型的操作y->x,根据这个关系建边,最后是一棵有根树,
由于撤去的座位一定是之前有的座位,故建的新边,一定是上一时刻的树根a和树根b之间的建边,
这样在任意时刻,都是若干棵有根树的森林,
考虑把一棵树视为一个并查集,并查集内维护的是,能坐在树根这个座位的点的集合
那么对于合并y-->x来说,就是可能坐下y的一个人向可能坐在x的一个人发起了挑战,
此时,x所在并查集的主场数均+1,y所在的并查集的客场数均+1,
实际操作时,把这个add标记打到根上,
合并的时候,x挂到了y上,此时多加了y的标记量,
x作为根时的标记应不变,故减去多余量,
以下参考两种写法,
①按秩合并,是直接的做法,
用logn控制深度,每次查询的时候暴力上跳统计祖先该下放的标记总和
②路径压缩,是奇技淫巧,考虑一种情况,
x是根,且x是y的父亲,y是z的父亲,对x加a1,查询z时路径压缩,再对x加a2,再查询z,
此时z的值为a1+a1+a2,但实际应为a1+a2,是x的下标重复下放导致的,
其本质是根无法区分下放的标记和没下放的标记,
那我们就专把根留出来用于攒标记不下放,
其余的点由于下放后不会后续再产生新的没下放的标记,可以下放并路径压缩,这样树深也不过为2,
查询时,如果是根,就只考虑标记量;否则考虑根的标记量+当前节点的权值
代码1(按秩合并)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+10,mod=998244353;
typedef pair<int,int> P;
#define fi first
#define se second
#define pb push_back
int n,m,op,x,y,par[N],rk[N],a[N],b[N];//a:主场 b:客场
ll i13,i23;
ll modpow(ll x,ll n,ll mod){
ll res=1;
for(;n;n/=2,x=x*x%mod){
if(n&1)res=res*x%mod;
}
return res;
}
int find(int x){
for(;x!=par[x];x=par[x]);
return x;
}
//y向x发起挑战 y客场 x主场
void unite(int x,int y){
x=find(x),y=find(y);
a[x]++;b[y]++;
if(rk[x]>rk[y])swap(x,y);
if(rk[x]==rk[y])rk[y]++;
//x向y上挂 并减去多余的向下传的标记
par[x]=y;
a[x]-=a[y];
b[x]-=b[y];
}
//3^n * (2/3)^a *(1/3)^b
ll cal(ll x){
ll na=a[x],nb=b[x];
for(;x!=par[x];x=par[x],na+=a[x],nb+=b[x]);
//printf("na:%lld nb:%lld\n",na,nb);
ll res=modpow(3,n,mod);
res=res*modpow(i23,na,mod)%mod;
res=res*modpow(i13,nb,mod)%mod;
return res;
}
int main(){
i13=modpow(3,mod-2,mod);//1/3
i23=2ll*i13%mod;//2/3
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i){
rk[i]=1;
par[i]=i;
}
for(int i=1;i<=m;++i){
scanf("%d%d",&op,&x);
if(op==1){
scanf("%d",&y);
unite(x,y);
}
else{
printf("%lld\n",cal(x));
}
}
return 0;
}
代码2(路径压缩)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+10,mod=998244353;
typedef pair<int,int> P;
#define fi first
#define se second
#define pb push_back
int n,m,op,x,y,par[N],a[N],b[N];//a:主场 b:客场
ll i13,i23;
ll modpow(ll x,ll n,ll mod){
ll res=1;
for(;n;n/=2,x=x*x%mod){
if(n&1)res=res*x%mod;
}
return res;
}
//路径压缩 但此时不加上根的贡献
int find(int x){
if(x==par[x])return x;
int anc=find(par[x]);
if(par[x]!=anc){
a[x]+=a[par[x]];
b[x]+=b[par[x]];
}
return par[x]=anc;
}
//y向x发起挑战 y客场 x主场
void unite(int x,int y){
x=find(x),y=find(y);
a[x]++;b[y]++;
par[x]=y;
a[x]-=a[y];
b[x]-=b[y];
}
//3^n * (2/3)^a *(1/3)^b
ll cal(ll a,ll b){
ll res=modpow(3,n,mod);
res=res*modpow(i23,a,mod)%mod;
res=res*modpow(i13,b,mod)%mod;
return res;
}
int main(){
i13=modpow(3,mod-2,mod);//1/3
i23=2ll*i13%mod;//2/3
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i){
par[i]=i;
}
for(int i=1;i<=m;++i){
scanf("%d%d",&op,&x);
if(op==1){
scanf("%d",&y);
unite(x,y);
}
else{
y=find(x);
if(x==y)printf("%lld\n",cal(a[x],b[x]));
else printf("%lld\n",cal(a[x]+a[y],b[x]+b[y]));
}
}
return 0;
}