并查集及其扩展

文章首发于:My Blog 欢迎大佬们前来逛逛

基础并查集

并查集就是用来维护一些元素之间的关系的集合。

例如 A的亲戚是B ,则我们可以把 A与B放到同一个集合中,表示AB属于同一种关系(亲戚关系),并且标记A和B的关系数组都为B

B的亲戚是C,则我们可以把 B和C也放到同一个集合中,则BC也属于同一个集合,并且标记B和C的关系数组都为C

再往后我们需要知道A和C是否是 同一个关系(即是否具有传递性),我们可以通过一直查找A和C的关系数组直到没有关系为止,看看他们是否是相同的元素值,即可知道他们是否属于同一个关系

  1. A的关系数组的值是 B,而B的关系数组的值是C,所以A的关系为C
  2. C的关系数组的值就是C,所以A和C属于同一关系

  1. 并查集的初始化

每个元素的关系数组的关系只有其一个人:A默认为A,B默认为B

void init(){
	for (int i=1;i<n;i++){
		parent[i]=i;
	}
}

  1. 并查集的查找:(路径压缩)

每个元素在确定关系的时候都需要一直往上寻找,直到到达它的最顶级关系:A的关系不是B,而是往上B的关系,直到到达C,C的关系为其本身,这就到到达了A的最顶级关系,就是C

int find_set(){
	if (x!=parent[x]){
		parent[x]=find_set(parent[x]);
	}
	return parend[x];
}

  1. 并查集的合并

合并是并查集最重要的功能,就如同上述的A和B或者B和C的确认关系的时候,把他们合并到同一个关系中,A的关系是B,B的关系为其本身

void merge_set(int x,int y){
	x=find_set(x);
	y=find_set(y);
	if (x!=y){
		parent[x]=y;	//X合并到Y上
	}
}

基础并查集便可以解决一系列的存在关系的问题

1. P1551 亲戚

P1551 亲戚

这道题让我们判断两个人之间是否是亲戚,和我们上面的描述的基本是一致的,只需要判断他们两个是否属于同一个集合即可。

//TODO: Write code here
int n,m,p;
const int N=1e5+10;
int parent[N];
void init_set(){
    for (int i=1;i<=n;i++){
        parent[i]=i; 
    }
}
int find_set(int x){
    if (x!=parent[x]){
        parent[x]=find_set(parent[x]);
    }
    return parent[x];
}
void merge_set(int x,int y){
    x=find_set(x);
    y=find_set(y);
    if (x!=y){
        parent[y]=x;//y合并到x上
    }
}
signed main()
{
	cin>>n>>m>>p;
    init_set();
    for (int i=1;i<=m;i++){
        int a,b;
        cin>>a>>b;
        merge_set(a,b);
    }
    while (p--){
        int a,b;
        cin>>a>>b;
        if (find_set(a)==find_set(b)){
            cout<<"Yes\n";
        }else{
            cout<<"No\n";
        }
    }
#define one 1
	return 0;
}

2. P1536 村村通

P1536 村村通

这道题其实就是求有多少个集合的问题

如果一共有四个城市,有两条道路,分别为 1到3 和 3到4,则我们画图可以看出来 1 3 4 都属于同一个集合2城市单独属于一个集合,总共有两个集合,因此如果要将两个集合合并,则需要添加的道路数量就是总集合数-1

//TODO: Write code here
int n,m;
const int N=1e5+10;
int parent[N];
int find_set(int x){
    if (x!=parent[x]){
        parent[x]=find_set(parent[x]);
    }
    return parent[x];
}
void merge_set(int x,int y){
    x=find_set(x);
    y=find_set(y);
    if (x!=y){
        parent[y]=x;    //y合并到x
    }
}
void display(){
    for (int i=1;i<=n;i++){
        printf("i=%d,parent[%d]=%d\n",i,i,parent[i]);
    }
    cout<<endl;
}
signed main()
{
	while (cin>>n>>m){
        if (n==0){
            break;
        }
        for (int i=1;i<=n;i++){
            parent[i]=i;
        }
        while (m--){
            int a,b;
            cin>>a>>b;
            merge_set(a,b);
        }
        int ans=0;
        for (int i=1;i<=n;i++){
            //查找有几个不同的find_set
            if (find_set(i)==i) ans++;//统计总集合数
        }
        cout<<ans-1<<endl;
    }
#define one 1
	return 0;
}

种类并查集

种类并查集属于扩展域并查集一类。

不同的个体之间存在不同的关系,而每个相同个体之间都属于同一关系,不同的关系的个体的种类不同,因此就有了种类并查集。

1. P1892 [BOI2003]团伙

P1892 [BOI2003]团伙

定义:

  • 一个人的朋友的朋友是朋友
  • 一个人的敌人的敌人是朋友

(很挺符合常理)

因此我们便可以确定两个并查集的种类:朋友和敌人。

不妨设每个人都有一个 enemies数组,表示他的敌人,则它的 enemies数组存储的可能是它的另一个敌人的朋友,即它的两个敌人互为朋友。

我们便可以确定出这样的关系:

  • 如果A没有敌人:则A的敌人包括B和B的朋友们
  • 如果A有敌人,则A的敌人是B的朋友
  • 对于B同理
//a b是敌人
if (enemies[a]==0){
    //如果a没有敌人,则a的敌人就包括b和b的朋友们
    enemies[a]=find_set(b);
}
else{
    //如果a有敌人,则a的敌人们就是b的朋友
    merge_set(enemies[a],b);
}
if (enemies[b]==0){
    //如果b没有敌人,则b的敌人就包括a和a的朋友们
    enemies[b]=find_set(a);
}
else{
    //如果b有敌人,则b的敌人们就是a的朋友
    merge_set(enemies[b],a);
}

也可以使用这种并查集的做法

表达敌人与朋友两种关系,不妨将并查集的数组开大两倍,作为并查集的扩展域,于是可以表达朋友以及敌人两部分,朋友对应第一倍,敌人对应第二倍。于是我们可以得到如下合并公式

  • A和B是敌人的情况:
    • A的敌人:A+n 是B 的朋友
    • B的敌人: B+n是A 的朋友
  • A和B是朋友: A和B
if (ch=='E'){
    //ab是敌人 则a+n与b,b+n与a是朋友
    merge_set(a+n,b); 
    merge_set(b+n,a); 
}       
else if (ch=='F'){
    merge_set(a,b);
}

2. P1525 [NOIP2010 提高组] 关押罪犯

P1525 [NOIP2010 提高组] 关押罪犯

如题所示,我们需要把多个罪犯分成不同的种类,而每对罪犯之间又存在着不同的仇恨值

我们需要把这些罪犯分到两个种类区域中,使得合并后每两个罪犯之间的最大仇恨值为一个理论最小值

我们显然需要对每对罪犯仇恨值按照从大到小的顺序排序。

如果A和B 是敌人,则A和B要尽量分在两个不同的区域中,那么A不能和B在一起,要和谁在一起呢?

我们可以让A和(B的其他敌人)在一起,因为按照贪心的原理来看,如果A和B有仇,则A和B的仇人不一定有仇,相反B的仇人可能和A是朋友,因为他们都和B有仇,当然这句话是我的补充,题中并没有说他们是朋友,只是为了便于理解

因此寻找B仇人们,让他们和A是一伙的;同理寻找A的仇人们,让他们和B是一伙的。因为是按照仇恨值从大到小排序的,因此A和B的仇人们一伙,如果存在敌对关系的话,仇恨值一定是比B要小的(A和B仇恨值大,先判断,越往后肯定仇恨值越小);同理对于B和A的仇人们也是一样的。

一直把A和B的仇人们合并,B和A的仇人们合并。

在对A合并,寻找B的仇人的时候,直到B的仇人们的仇人的仇人.... 又回到了A,则A和B不可避免的又碰到了一起,即这个时候已经无法在分了,A和B不管找谁的仇人他们都是有仇的,则此时我们直接输出这个组合,此时这个组合关系的仇恨值一定是一个理论最小值,因为是按照从大到小排序的,既然不存在其他组合了,则这种一定是最小的。

//TODO: Write code here
int n,m;
const int N=2e5+10;
int parent[N],enemies[N];
struct Node
{
    int a,b,c;
}node[N];
bool comp(Node a,Node b){
    return a.c>b.c;
}   
int find_set(int x){
    if (x!=parent[x]){
        parent[x]=find_set(parent[x]);
    }
    return parent[x];
}
void merge_set(int x,int y){
    x=find_set(x);
    y=find_set(y);
    if (x!=y){
        parent[x]=y;
    }
}
bool check(int x,int y){
    x=find_set(x);
    y=find_set(y);
    if (x==y){
        return true;
    }
    return false;
}
void init(){
    for (int i=1;i<=n;i++){
        parent[i]=i;
    }
}
void display(){
    for (int i=1;i<=n;i++){
        printf("i=%d,parent[%d]=%d\n",i,i,parent[i]);
    }
    cout<<endl;
}
signed main()
{
	cin>>n>>m;
    init();
    for (int i=1;i<=m;i++){
        cin>>node[i].a>>node[i].b>>node[i].c;
    }
    sort(node+1,node+1+m,comp);
    for (int i=1;i<=m+1;i++){
        int a=node[i].a,b=node[i].b;
        if (check(a,b)){
            cout<<node[i].c<<endl;
            break;
        }
        if (enemies[a]==0){
            enemies[a]=b;
        }
        else{
            merge_set(enemies[a],b);
        }
        if (enemies[b]==0){
            enemies[b]=a;
        }
        else{
            merge_set(enemies[b],a);
        }
    }
#define one 1
	return 0;
}

3. P2024 [NOI2001] 食物链

[P2024 NOI2001] 食物链 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

我们在这个题中能得到三种关系:

  1. 属于同一类
  2. A吃B 的关系
  3. A被B吃 的关系

那么我们就可以创建三个种类,即三个集合,来分别合并他们之间的关系。

如果他们两个是同类的关系:输入1 u v

  • u与v是同类(第一群系A)
  • u+n和v+n是同类(第二群系B)
  • u+n+n和v+n+n是同类(第三群系C)
merge_set(u,v)
merge_set(u+n,v+n)
merge_set(u+n+n,v+n+n)

如果它们两个是吃与被吃的关系: 输入 2 u v

由于他们三个群系属于一个环形生物链,并且同一群系中生物不能互吃

因此只能是 A群系吃B群系 , B群系吃C群系, C群系吃A群系

merge_set(u,v+n+n);   // A的u被C的v吃
merge_set(u+n,v);     // B的u被A的v吃
merge_set(u+n+n,v+n); // C的u被B的v吃

我们如何判断话的真假?

  • 如果输入2 u v,则uv一定不属于同一群系,因为uv存在u吃v的关系。

如果它是个假的

  1. 只需要判断uv属于同一群系,则False
  2. 或者判断 v吃u ,则不满足 u吃v,则False

则这两种情况如下:

分别表示 uv 是同一群系 或者 u被v吃

if (find_set(u)==find_set(v) || find_set(u)==find_set(v+n)) ans++;//假话++

  • 如果输入 1 uv,则uv属于同一群系。

如果它是个假的:

  1. 只需要判断 存在 u吃v,则False;
  2. 或者判断 存在 v吃u,则False

则这两种情况如下:

if (find_set(v+n)==find_set(u) || find_set(u)==find_set(v+n)) ans++//假话

完整代码如下:

//TODO: Write code here
int n,m;
const int N=1e5+10;
int parent[N*3];
void init(){
    for (int i=1;i<=3*n;i++){
        parent[i]=i;
    }
}
int find_set(int x){
    if (x!=parent[x]){
        parent[x]=find_set(parent[x]);
    }
    return parent[x];
}
void merge_set(int x,int y){
    x=find_set(x);
    y=find_set(y);
    if (x!=y){
        parent[x]=y;
    }
}
signed main()
{
    int ans=0;
	cin>>n>>m;
    init();
    for (int i=1;i<=m;i++){
        int opt,a,b;
        cin>>opt>>a>>b;
        if (a>n || b>n){
            ans++;
            continue;
        }
        if (opt==1){
            //判断是不是假话: a与b是同类
            //如果a与b 存在 a吃b 或者 b吃a 的关系,则a和b是同类是假话
            if (find_set(a)==find_set(b+n) ||  // a被b吃
                find_set(b)==find_set(a+n)){   // b被a吃
                    ans++;
                }
            else{
                //真话
                merge_set(a,b);
                merge_set(a+n,b+n);
                merge_set(a+2*n,b+2*n);
            }
        }
        else if (opt==2){
            //判断是不是假话: a吃b
            //如果a与b同类;或者b吃a 成立,则a吃b是假话
            if (find_set(a)==find_set(b) || 	// a与b是同类
                find_set(a)==find_set(b+n)){	// a被b吃
                    ans++;
                }
            else{
                //真话
                merge_set(a,b+n+n);   // A的a被C的b吃
                merge_set(a+n,b);     // B的a被A的b吃
                merge_set(a+n+n,b+n); // C的a被B的b吃
            }
        }
    }
    cout<<ans;
#define one 1
	return 0;
}

带权并查集

就是在维护集合关系的树中添加边权的并查集,这样做可以维护更多的信息。

不难发现所谓的并查集本质上也是一种树,由于路径压缩的存在,使得一个并查集树中,其被压缩过的子节点总是直接指向根节点,于是我们可以给根节点与子节点之间的边进行赋权。

这个权值可以表示该节点与根节点之间的距离

待更新。。。。

posted @ 2023-03-17 18:31  hugeYlh  阅读(35)  评论(0编辑  收藏  举报