并查集
一般对于连通性问题,并查集是非常好用的
不仅仅可以维护出是否连通,还可以顺便维护出当前连通块的一些信息,比如直径
有时候并查集的祖先节点不做区分,即合并是认定父子关系是随意的,但是是遇到构建 \(kruscal\) 重构树等情形就需要做出区分,甚至在并查集树上做一些事情,所以一般情况下不要乱写
一般情况下路径压缩就可以满足复杂度需求,然而路径压缩不具有可撤销性或造成树形结构紊乱,所以遇到线段树分治等情形必须使用按秩合并
经典例题
这是一个经典套路:时光倒流
对于拆分连通块的题,往往可以通过时间倒流的方式转化为连通块的合并
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=4e5+5;
int n,fa[maxn],tot,x,y,m,k,a[maxn],ans[maxn],cnt;
bool vis[maxn];
vector<int>edge[maxn];
int read(){
int x=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-')f=-1;
ch=getchar();
}
while(isdigit(ch)){
x=x*10+ch-48;
ch=getchar();
}
return x*f;
}
int find(int x){
return fa[x]==x?x:fa[x]=find(fa[x]);
}
void unionn(int x,int y){
x=find(x);
y=find(y);
if(x!=y)fa[y]=x,tot--;
//,cout<<x<<" "<<y<<endl;
return ;
}
int main(){
n=read();
m=read();
for(int i=1;i<=m;i++){
x=read()+1;
y=read()+1;
edge[x].push_back(y);
edge[y].push_back(x);
}
k=read();
for(int i=1;i<=k;i++){
a[i]=read()+1;
vis[a[i]]=true;
}
tot=n-k;
// cout<<tot<<" ";
for(int i=1;i<=n;i++){
fa[i]=i;
}
for(int i=1;i<=n;i++){
// fa[i]=i;
// tot++;
if(!vis[i]){
for(int j=0;j<edge[i].size();j++){
if(!vis[edge[i][j]]){
unionn(i,edge[i][j]);
}
}
}
}
ans[++cnt]=tot;
// cout<<tot<<endl;
for(int i=k;i>=1;i--){
tot++;
for(int j=0;j<edge[a[i]].size();j++){
if(!vis[edge[a[i]][j]]){
unionn(a[i],edge[a[i]][j]);
}
}
vis[a[i]]=false;
ans[++cnt]=tot;
// printf("%d\n",tot);
}
for(int i=k+1;i>=1;i--)printf("%d\n",ans[i]);
return 0;
}
扩展域并查集的经典套路,即给每个节点开一个虚拟节点,那么两个人有冲突,相当于这个人和另一个人的补集是朋友
这回可真的是涨知识了,居然可以用二进制拆分的思想维护并查集
因为题目要求做的操作是区间对应连边,但是一个一个连边显然不现实,那么可以用类似于倍增的思想,每一个并查集维护一个 \(2\) 的幂次的区间,那么每次操作只需要拆分成 \(log\) 个区间的连边即可
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
int cnt,ans,fa[maxn][25],n,m,l,r,ll,rr;
const int mod=1e9+7;
int read(){
int x=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-')f=-1;
ch=getchar();
}
while(isdigit(ch)){
x=x*10+ch-48;
ch=getchar();
}
return x*f;
}
int find(int x,int p){
return fa[x][p]==x?x:fa[x][p]=find(fa[x][p],p);
}
void merge(int x,int y,int p){
int xx=find(x,p);
int yy=find(y,p);
if(xx!=yy){
fa[xx][p]=fa[yy][p];
}
return ;
}
int po(int a,int b){
int ans=1;
while(b){
if(b&1)ans=1ll*ans*a%mod;
a=1ll*a*a%mod;
b>>=1;
}
return ans;
}
int main(){
n=read();
m=read();
for(int i=1;i<=n;i++){
for(int j=0;j<=20;j++){
fa[i][j]=i;
}
}
for(int i=1;i<=m;i++){
l=read();
r=read();
ll=read();
rr=read();
for(int j=20;j>=0;j--){
if(l+(1<<j)-1<=r){
merge(l,ll,j);
l+=(1<<j);
ll+=(1<<j);
}
}
}
for(int j=20;j>=1;j--){
for(int i=1;i<=n;i++){
if(i+(1<<j)-1>n)break;
merge(i,find(i,j),j-1);
merge(i+(1<<(j-1)),find(i,j)+(1<<(j-1)),j-1);
}
}
for(int i=1;i<=n;i++){
if(fa[i][0]==i)cnt++;
}
ans=9ll*po(10,cnt-1)%mod;
cout<<ans;
return 0;
}
这似乎是一个很常用的科技?
可以倒序枚举,这样每个点只需要被最后一次操作更新即可,也就是说每个点至多只会被更新一次
那么用并查集维护当前点的右边第一个没被更新的点是哪个,枚举时只需要 \(i=find(i+1)\) 即可,修改完当前点后 \(fa[i]=i+1\)
注意这种方式的常数还是非常大的,在使用之前思考能否用打标记的方法代替
比如改成每次覆盖一个子树,那么显然用这种方式改成 \(dfs\) 序上的事情并不划算,这是手动去掉了原问题自带的良好性质
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
int n,m,p,q,l,r,fa[maxn],ans[maxn];
int find(int x){return fa[x]==x?x:fa[x]=find(fa[x]);}
int main(){
cin>>n>>m>>p>>q;
for(int i=1;i<=n+1;i++)fa[i]=i;
for(int i=m;i>=1;i--){
l=(i*p+q)%n+1,r=(i*q+p)%n+1;if(l>r)l^=r^=l^=r;
l=find(l);while(l<=r)ans[l]=i,l=find(fa[l]=l+1);
}
for(int i=1;i<=n;i++)printf("%d\n",ans[i]);
return 0;
}
边带权并查集
边带权并查集的每个父子关系都是有边权的,那么这样就可以方便地查出儿子到祖先的距离
但是注意路径压缩的时候因为父子关系变了,那么相应的边权也要发生改变,代码这样实现:
if(fa[x]!=x){
int pre=fa[x];
fa[x]=find(fa[x]);
dis[x]+=dis[pre];
}
return fa[x];
这道题算是模板了,直接用并查集模拟题意即可
代码
#include<bits/stdc++.h>
using namespace std;
int n,father[30005],size[30005],dis[30005],x,y;
char c;
int find(int x){
if(father[x]==x)return x;
int pre=father[x],ans=find(father[x]);
dis[x]+=dis[pre];
father[x]=ans;
return father[x];
}
int main(){
cin>>n;
for(int i=1;i<=30000;i++){
father[i]=i;
size[i]=1;
}
for(int i=1;i<=n;i++){
scanf("\n");
c=getchar();
cin>>x>>y;
if(c=='M'){
int r1=find(x);
int r2=find(y);
father[r2]=r1;
dis[r2]+=size[r1];
size[r1]+=size[r2];
}
else{
int r1=find(x);
int r2=find(y);
if(r1!=r2){
//cout<<r1<<" "<<r2<<endl;
cout<<-1<<endl;
}
else{
cout<<abs(dis[x]-dis[y])-1<<endl;
}
}
}
return 0;
}
因为相同关系为长度为 \(3\) 的链,那么在并查集上将所有距离都模 \(3\) 即可
对于一个区间 \([l,r]\),利用前缀和的思想,从 \(r\) 向 \(l-1\) 连边权为 \(w\) 的边,每次并查集判断合法
当然也可以用差分约束来做
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=105;
int fa[maxn],dis[maxn],t,n,m,x,y,w,xx,yy;
int read(){
int x=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-')f=-1;
ch=getchar();
}
while(isdigit(ch)){
x=x*10+ch-48;
ch=getchar();
}
return x*f;
}
int find(int x){
if(fa[x]==x)return x;
int p=find(fa[x]);
dis[x]+=dis[fa[x]];
fa[x]=p;
return p;
}
int main(){
t=read();
while(t--){
n=read();
m=read();
for(int i=0;i<=n;i++){
fa[i]=i;
dis[i]=0;
}
bool flag=false;
for(int i=1;i<=m;i++){
x=read();
y=read();
w=read();
xx=find(x-1);
yy=find(y);
if(xx==yy){
if(w!=dis[x-1]-dis[y])flag=true;//,cout<<i<<endl;
}
else{
fa[xx]=yy;
dis[xx]=dis[y]+w-dis[x-1];
}
}
if(flag)puts("false");
else puts("true");
}
return 0;
}
可持久化并查集
其实就是用主席树暴力模拟所有并查集的操作了,不能路径压缩,维护出按秩合并用到的 \(fa\) 和 \(dep\) 然所有操作多带一个 \(log\) 暴力模拟就行了
(当年的主席树就是丑啊)
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=(2e5+5)*20;
int n,m,fa[maxn],lson[maxn],rson[maxn],dep[maxn],tot,root[maxn],op,a,b;
int read(){
int x=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-')f=-1;
ch=getchar();
}
while(isdigit(ch)){
x=x*10+ch-48;
ch=getchar();
}
return x*f;
}
void build(int &p,int l,int r){
p=++tot;
if(l==r){
fa[p]=l;
return ;
}
int mid=l+r>>1;
build(lson[p],l,mid);
build(rson[p],mid+1,r);
return ;
}
void change(int &p,int l,int r,int x,int k){
fa[++tot]=fa[p];
lson[tot]=lson[p];
rson[tot]=rson[p];
dep[tot]=dep[p];
p=tot;
if(l==r){
fa[p]=k;
return ;
}
int mid=l+r>>1;
if(x<=mid)change(lson[p],l,mid,x,k);
else change(rson[p],mid+1,r,x,k);
return ;
}
void add(int p,int l,int r,int x,int val){
if(l==r){
dep[p]+=val;
return ;
}
int mid=l+r>>1;
if(x<=mid)add(lson[p],l,mid,x,val);
else add(rson[p],mid+1,r,x,val);
return ;
}
int ask(int p,int l,int r,int x){
if(l==r)return p;
int mid=l+r>>1;
if(x<=mid)return ask(lson[p],l,mid,x);
else return ask(rson[p],mid+1,r,x);
}
int find(int pos,int x){
int p=ask(root[pos],1,n,x);
if(fa[p]==x)return p;
return find(pos,fa[p]);
}
void merge(int pos,int x,int y){
int xx=find(pos,x),yy=find(pos,y);
if(xx==yy)return ;
if(dep[xx]>dep[yy])swap(xx,yy);
change(root[pos],1,n,fa[xx],fa[yy]);
add(root[pos],1,n,yy,1);
return ;
}
int main(){
n=read();
m=read();
build(root[0],1,n);
for(int i=1;i<=m;i++){
op=read();
switch(op){
case 1:
a=read();
b=read();
root[i]=root[i-1];
merge(i,a,b);
break;
case 2:
a=read();
root[i]=root[a];
break;
case 3:
a=read();
b=read();
root[i]=root[i-1];
int x=find(i,a);
int y=find(i,b);
if(x==y)puts("1");
else puts("0");
}
}
return 0;
}