并查集学习笔记

并查集

基础并查集

算法学习

问题模型: 用于实现集合合并的数据结构。例如:朋友的朋友是朋友,判断 u , v u,v u,v 二人是否是朋友。

实质: 并查集实际上是一个树形结构,但是它没有连向子节点的边,只有连向父节点的边。 f a [ x ] fa[x] fa[x] 记录 x x x 的父亲。我们认为:属于同一祖先的两个点属于同一个集合。

算法实现

初始化: 定义 f a [ i ] = i fa[i]=i fa[i]=i 表示第 i i i 个人初始自己就是自己的父亲。即:各自属于一个集合。

寻找祖先: 不停的通过 f a [ x ] fa[x] fa[x] 找父亲,直到找到 f a [ x ] = x fa[x]=x fa[x]=x 的点即可。

合并操作: 对于朋友 x , y x,y x,y ,令 p 1 , p 2 p1,p2 p1,p2 分别为他们的祖先。令 f a [ p 1 ] = p 2 fa[p1]=p2 fa[p1]=p2 即可关联他们。

路径压缩: 对于特殊数据,数据可能会退化成一条链。此时每次找祖先的代价(不停的 f a [ x ] fa[x] fa[x] 找父亲)会退化为 O ( n ) O(n) O(n) 。可以使用路径压缩的方式:对于点 x x x ,在其找到祖先的过程中,会经过若干个点,最后找到祖先 p p p 。让经过的所有点的 f a [ ] = p fa[]=p fa[]=p 。将这条找祖先的链,之间变成一个高度为 2 2 2 的树。下次找的时候,就可以 O ( 1 ) O(1) O(1) 找到。

模板

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int fa[N];
int find(int x){
    if(x==fa[x])return x;
    else return fa[x]=find(fa[x]);
}
void solve(){
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++)fa[i]=i;
    while(m--){
        int x,y,op;
        cin>>x>>y>>op;
        int p1=find(x),p2=find(y);
        if(op==1){//判断关系
            if(p1==p2)cout<<"属于同一个集合\n";
            else cout<<"不属于同一个集合\n";
        }else{//合并
            if(p1!=p2){
                fa[p1]=p2;
            }
        }
    }
}

启发式并查集

算法学习

分析:

  • 使用路径压缩虽然可以优化时间复杂度,但是却丢失了 x x x 到其祖先这条路径的所有信息,只保留了最后的结果。

  • 使用朴素的并查集虽然可以保存路径的信息,但是却会被卡成 O ( n ) O(n) O(n) 的暴力复杂度。

  • 是否可以拥有一个二者兼得的并查集???

实质:

  • 并查集,实质上就是一个树。
  • 没有路径压缩的并查集是一棵高度无法保证的树,有路径压缩的并查集是一棵高度为 2 2 2 的树。

优化:

  • 尝试对朴素的并查集进行优化。
  • 合并的过程,原来是两个树合并在一起
  • 现在并查集的祖先节点有个高度,合并的时候令高度高的并查集,作为根。
  • 分析高度变化:
    • 两个高度一样均为 h h h 的并查集合并,得到的并查集高度为 h + 1 h+1 h+1
    • 两个高度不一样的并查集合并,得到的并查集高度为 m a x ( h 1 , h 2 ) max(h_1,h_2) max(h1,h2)
  • 与普通并查集想比,只需要多维护一个高度信息即可。

模板

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int fa[N],h[N];
int find(int x){
    if(x==fa[x])return x;
    else return find(fa[x]);
}
void solve(){
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++)fa[i]=i;
    while(m--){
        int x,y,op;
        cin>>x>>y>>op;
        int p1=find(x),p2=find(y);
        if(op==1){//判断关系
            if(p1==p2)cout<<"属于同一个集合\n";
            else cout<<"不属于同一个集合\n";
        }else{//合并
            if(p1!=p2){
                if(h[p1]>h[p2])fa[p2]=p1;
                else if(h[p1]<h[p2])fa[p1]=p2;
                else{
                    fa[p1]=p2;
                    h[p2]++;
                } 
            }
        }
    }
}

带权并查集

算法学习

前言: 之前的并查集只有父亲这一个信息,如果想要有更多信息,例如:边权。该如何去拓展它?

模型: 一维数轴上有 n n n 个点,初始不知道他们的相对距离。 q q q 次操作,每次操作得到信息 ( x , y , d ) (x,y,d) (x,y,d) ,表示从 x x x y y y 的左边 d d d 距离。对于第 i i i 条消息,请问是否会与前面得到的信息冲突,若一致则输出 y e s yes yes ,若不一致则忽略该信息。

分析:

  • f a [ x ] fa[x] fa[x] x x x 的父亲, d i s [ x ] dis[x] dis[x] 表示 f a [ x ] fa[x] fa[x] x x x 左边的距离。
  • 对于信息 ( x , y , d ) (x,y,d) (x,y,d)
    • x , y x,y x,y 属于同一个集合,显然其距离已经被确定。距离为: d i s [ y ] − d i s [ x ] dis[y]-dis[x] dis[y]dis[x]
    • x , y x,y x,y 不属于同一个集合,合并他们即可,但是合并的时候,要同时更新 d i s dis dis 数组(在路径压缩和合并的时候,均要更新)。
      • 路径压缩时: d i s [ x ] dis[x] dis[x] 表示 x x x 到此时的父亲 f a [ x ] fa[x] fa[x] 的距离,路径压缩完 d i s [ f a [ x ] ] dis[fa[x]] dis[fa[x]] 表示父亲到其父亲的距离。由于压缩后, x x x 的父亲会变为父亲的父亲,则: d i s [ x ] = d i s [ x ] + d i s [ f a [ x ] ] dis[x]=dis[x]+dis[fa[x]] dis[x]=dis[x]+dis[fa[x]] 。( x x x 到父亲的距离加上父亲到父亲的父亲的距离)。
      • 合并时: f a [ p 2 ] = p 1 fa[p2]=p1 fa[p2]=p1 ,由于 d = d= d= x x x y y y 左边的距离, d i s [ x ] dis[x] dis[x] 表示 p 1 p1 p1 x x x 左边的距离, d i s [ y ] dis[y] dis[y] 表示 p 2 p2 p2 y y y 左边的距离。则有: d i s [ p 2 ] − d i s [ x ] + d i s [ y ] = d dis[p2]-dis[x]+dis[y]=d dis[p2]dis[x]+dis[y]=d 得到: d i s [ p 2 ] = d i s [ y ] − d i s [ x ] + d dis[p2]=dis[y]-dis[x]+d dis[p2]=dis[y]dis[x]+d
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=2e5+10;
int fa[N];
LL dis[N];
int find(int x){
    if(x==fa[x])return x;
    else {
        int t=fa[x];
		fa[x]=find(fa[x]);
		dis[x]+=dis[t];
		return fa[x];
    }
}
void solve() {
	int n,q;
	cin>>n>>q;
	for(int i=1;i<=n;i++){
		fa[i]=i;
		dis[i]=0;
	}
	while(q--){
        int x,y;
        LL d;
		cin>>x>>y>>d;
		int p1=find(x),p2=find(y);
		if(p1==p2){
			if(d==dis[y]-dis[x])cout<<"与前面信息一致\n";
			else cout<<"与前面信息不一致\n";
		}else{
			fa[p2]=p1;
			dis[p2]=dis[y]-dis[x]+d;
		}
	}
}

例题练习

例题1:P8779

题目描述: n n n 个数,不知道数值。 m m m 个已知信息,每个信息给出 l i , r i , v i l_i,r_i,v_i li,ri,vi 表示 ∑ i = l r a i = v \sum_{i=l}^r a_i=v i=lrai=v q q q 个询问,每个询问给出 l , r l,r l,r ,询问 ∑ i = l r \sum_{i=l}^r i=lr ,若无法确定,输出 NOKNOWN n , m , q < = 1 e 5 , v i ∈ [ − 1 e 12 , 1 e 12 ] n,m,q<=1e5,v_i\in[-1e12,1e12] n,m,q<=1e5,vi[1e12,1e12]

问题分析: 转化为前缀和之后,就是带权并查集模板题(注意变成了 n + 1 n+1 n+1 个点)。

int fa[N];
LL dis[N];
int find(int x){
    if(x==fa[x])return x;
    else{
        int t=fa[x];
        fa[x]=find(fa[x]);
        dis[x]+=dis[t];
        return fa[x];
    }
}
void solve() {
    int n,m,q;
    cin>>n>>m>>q;
    for(int i=0;i<=n;i++){
        fa[i]=i;
        dis[i]=0;
    }
    for(int i=0;i<m;i++){
        int l,r;
        LL v;
        cin>>l>>r>>v;
        int p1=find(l-1),p2=find(r);
        if(p1!=p2){
            fa[p2]=p1;
            dis[p2]=dis[l-1]-dis[r]+v;
        }
    }
    while(q--){
        int l,r;
        cin>>l>>r;
        int p1=find(l-1),p2=find(r);
        if(p1!=p2)cout<<"UNKNOWN\n";
        else cout<<dis[r]-dis[l-1]<<'\n';
    }
}
例题2:P1196

题目描述: n n n 个战舰,编号为 [ 1 , n ] [1,n] [1,n] ,初始排成一排(一排 n n n 列)。 m m m 次操作,操作 1 1 1 ( M , u , v ) (M,u,v) (M,u,v) 表示编号为 u u u 的战舰所在列整体排到编号为 v v v 的战舰列整体的后面;操作 2 2 2 ( C , u , v ) (C,u,v) (C,u,v) 表示如果 u , v u,v u,v 不属于同一列,则输出 − 1 -1 1 ,否则输出两个战舰之间的战舰数量。 n = 30000 , m < = 5 e 5 n=30000,m<=5e5 n=30000,m<=5e5

问题分析:

  • 按每列为一个集合,维护带权并查集,初始每一列独立。

  • 对于操作1:

    • u u u 的根接到 v v v 的根上,并维护 d i s , s i z dis,siz dis,siz 。其中 d i s [ i ] dis[i] dis[i] 表示编号为 i i i 的点与其所属列的第一个点的位置差值。

    • 具体的:设 u u u 的根为 p 1 p1 p1 v v v 的根为 p 2 p2 p2 。则这条信息可以转化为: p 1 p1 p1 v v v 所属列的第一个点的位置差值为 s i z [ p 2 ] siz[p2] siz[p2]

  • 对于操作 2 2 2

    • 若属于同一个集合,则直接做差即可得到他们中间的战舰个数。
int find(int x){
	if(x==fa[x])return fa[x];
	else {
		int t=fa[x];
		fa[x]=find(fa[x]);
		dis[x]+=dis[t];
		return fa[x];
	}
}
int main() {
	int n=30000,m;
	for(int i=1;i<=n;i++)fa[i]=i,siz[i]=1;
	cin>>m;
	while(T--){
        int u,v;
        char op;
		cin>>op>>u>>v;
		if(op=='M'){
			int p1=find(u),p2=find(v);
			fa[p1]=p2;
			dis[p1]=siz[p2]; 
			siz[p2]+=siz[p1];
		}else{
			int p1=find(u),p2=find(v);
			if(p1!=p2)cout<<-1<<endl;
			else cout<<abs(dis[u]-dis[v])-1<<endl;
		}
	}
}
例题3:TOJ1003

题目描述: 给定 n n n 表示有一个 n n n 个元素的 01 01 01 序列,初始不知道每个元素的值。 m m m 条消息,每条消息给出 ( l , r , o p ) (l,r,op) (l,r,op) ,表示区间 [ l , r ] [l,r] [l,r] 内有奇数/偶数个 1 1 1 。判断从哪句话开始,一定无法满足限制。 n < = 1 e 9 , m < = 5000 n<=1e9,m<=5000 n<=1e9,m<=5000

问题分析: 区间 01 01 01 个数就是区间异或和,转化为前缀异或和之后,就变成 pre[l-1]^pre[r]=op 。直接用带权并查集维护异或的关系即可。由于点数量过多,因此需要采取动态开点。

int fa[N],cnt;
map<int,int>id,dis;
void newnode(int x){
	if(id.find(x)==id.end()){
		id[x]=++cnt;
		fa[cnt]=cnt;
		dis[cnt]=0;
	}
}
int find(int x){
	if(x==fa[x])return fa[x];
	else{
		int t=fa[x];
		fa[x]=find(fa[x]);
		dis[x]=dis[x]^dis[t];
		return fa[x];
	}
}
void solve(){
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        int l,r,op;
        cin>>l>>r>>op;
        newnode(l-1),newnode(r);
        int x=id[l-1],y=id[r];
        int p1=find(x),p2=find(y);
        if(p1==p2){
            if(dis[x]^dis[y]==op)continue;
            else{
                cout<<i<<'\n';
                return;
            }
        }else{
            fa[p1]=p2;
           	dis[p1]=dis[x]^dis[y]^op;
        }
    }
    cout<<-1<<'\n';
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值