并查集(进阶)——C


前言

并查集(入门)学完后,对并查集有了基本的了解:

  • 并查集可以判断一幅无向图中有几个连通分量
  • 并查集的find、join函数都是必不可少的
  • 路径压缩算法对于并查集的优化也很关键

有了这些知识,我成功AC了hdu1232畅通工程,总觉得并查集不应该这么简单(套模板,修改一点点就AC),后来,遇到了poj1182食物链这道题,发现只学习了上篇博客的知识是无法解决它的,是时候增加自己的知识量了。


一、进阶学什么?

  • 并查集的进阶主要内容是解决带权并查集的相关问题。

  • 在原有并查集的基础上,加入集合内部元素和其父节点之间的关系,这样的拓展,可以解决更多问题

  • 带权并查集和普通并查集最大的区别在于带权并查集合并的是可以推算关系的点的集合(可以通过集合中的一个已知值推算这个集合中其他元素的值)。而一般并查集合并的意图在于这个元素属于这个集合。带权并查集相当于把"属于" 的定义拓展了一下,拓展为"有关系"的集合。

二、引入问题

1.问题描述

来看这么一道题目:警察抓获N个罪犯,这些罪犯只可能属于两个黑帮团伙中的一个,现在给出M个条件(D a b表示a和b不在同一团伙),对于每一个询问(A a b)确定a,b是不是属于同一黑帮团伙或者不能确定。

2.分析

其实要回答的是:

1.第一层:关系能确定

  • 第二层:a,b是属于同一团伙。
  • 第二层:a,b不属于同一团伙

2.第一层:a,b关系不确定

代码描述:

if(a和b属于同一连通分支){
    if(。。。。){
        cout<<"a和b属于同一团伙"<<endl;
    }
    else{
        cout<<"a和b不属于同一团伙"<<endl;
    }
}
else{
    cout<<"还没有确定a和b的关系"<<endl;
}

根据题目条件:D a b 表示a和b不在同一团伙

  • 我们之前的解题观点:
    D a b表示a和b不在同一个团伙??我们平时碰到的不都是两人在同一个团伙,然后用unite函数将两人放入同一个连通分支内。所以这道题给我的第一印象是将两个团伙看作两个连通分支,然后将相应的罪犯加入到相应的连通分支中,最后要询问时,只要判断两个罪犯的祖先是否一致(如果一致,那么两人是同一团伙)。
    不可行的地方:条件并没有明确说明a和b属于哪个团伙,所有分不了连通,(设想一下,如果说明了,那岂不是so easy,就变成入门了,分成不同阵营),那么该怎么办呢?

  • 新思路:

    仔细想想,题目条件其实是给我们传递了一个关系,先不考虑这种关系是什么(尽管在这个题关系是“不属于同一团伙”),这就想到了之前所讲的,可连通=有关系,所以呢,我们不管怎样,给出条件D a b就是a b能够建立连通,最后进行询问A a b的时候,我们通过判断find(a)==find(b),就得知是不是在一个连通里,若不成立,说明毫不相干(不连通);若成立,就是在一个连通里,就说明有关系,就可以说明a b关系能确定(回答了第一层问题),
    到底是什么具体的关系呢(第二层问题)?能够找到答案,问题就解决了

建立了连通很简单:既然每次D a b都将a和b连起来,而且最后形成的那有且仅有一个的连通分支是由每次D a b的a和b结点组成,于是我们就可以确定祖先结点就是第一次D a b的a或b(具体是a还是b要看你的unite函数怎么写的了),后面我们才慢慢这个连通分支上再添加结点的。

祖先节点我们搞懂了,再来搞懂具体关系即可。

想要确定具体关系,就要先学习如何表示出具体关系 !

三、进阶:增加权来代表具体关系

思考:我们是否可以利用带权来存储每个节点与父节点的关系内容。

拿这道题来说,我们用一维数组r[]存储每个节点与其父节点的关系内容,即是否属于同一团伙(例如r[x]=0表示结点x与其父节点属于同一团伙,而r[x]=1则表示结点x与其父节点不属于同一团伙)。这样一来,两点之间的具体关系就可以通过权值比较来得出。

其实,经过入门我们已经发现,单纯比较两个顶点父节点(经过found和join),会很麻烦的,需要路径压缩,直接与其各自的祖先建立联系(直接访问)。而本题,不仅要寻找他们的祖先是否相同(是否连通),若相同,还要进行第二层比较具体关系。说白了这就是进阶:

  1. 建立直接访问祖先(连通)
    建立连通很简单:前面说过了。

  2. 建立直接访问祖先(连通)的“同时”(后面会解释),能够将他们与祖先的具体关系(权值)也建立起来。由于他们之间或者他们的祖先之间也可能会再次联合(连通),所以祖先会不断变化,这就使得他们与各自祖先之间的具体关系(权值)不断变化,这一过程就是权值更新,这也是解决问题的关键!

这样以来,一维权值数组r[]存储就是每个节点与其祖先的关系内容,即是否属于同一团伙(例如r[x]=0表示结点x与其祖先属于同一团伙,而r[x]=1则表示结点x与其祖先不属于同一团伙)。

到这里,我们已经掌握了进阶的思想了
将思想用代码描述:

if(find(a) == find(b)){		//a和b属于同一连通分支
    if(a和祖先的关系==b和祖先的关系){			
        cout<<"a和b属于同一团伙"<<endl;
    }
    else{
        cout<<"a和b不属于同一团伙"<<endl;
    }
}
else{
    cout<<"还没有确定a和b的关系"<<endl;
}

本题思路也就有了:

  1. 我们第一步要做的是将a和b归于同一个祖先之下(即连接a和b所在的连通分支,使a
    b连通),伴随这一操作的还有更新r[a]、r[b](怎么更新下面细说)。
  2. 在find(a)==find(b)成立的情况下,我们就可以通过判断r[a]==r[b]是否成立来确定他们的具体关系(成立就说明他们属于同一团伙,就可以输出"Inthe same gang.",不成立就说明他们不属于同一个团伙,就可以输出"In different gangs.")。
  3. 我们应该要知道上面那个式子r[a]==r[b]等价于r[a]==r[b]==0或r[a]==r[b]==1,即暗含着他们与他们的祖先在同一个团伙或不在同一个团伙,两种情况都说明a和b属于同一团伙。
if(find(a) == find(b)){
                    if(r[a] == r[b]){
                        cout<<"In the same gang.\n";
                    }
                    else{
                        cout<<"In different gangs.\n";
                    }
                }
                else{
                    cout<<"Not sure yet.\n";
                }

四、权值(具体关系)的更新

终于到了放大招的时刻了

前面说了,在建立连通(包括查找函数和联合函数)的同时,能够将他们与祖先的具体关系(权值)也建立起来(权值更新)。

那么如何实现呢?

再啰嗦一遍:我们前面说过数组r[x]表示节点x与根节点的关系,我们知道初始的时候,每个点都是一个连通分支(都有pre[i]=i,r[i]=0),而我们在构建一个由多个节点组成的连通分支时,我们新加入的节点的祖先在此刻发生变化,那么他们的r[]也要变化。

当然,这是每次D a b出现后,将a和b所在的连通分支连起来时对r[]的更新,也就是在unite函数内的更新。

此外,在find函数寻找根结点的时候也要不断更新r[](为啥啊为啥啊),因为unite函数内的r[]更新是在联合两棵树的时候进行的更新两棵树的根的关系,而其中相关子结点却未曾更新

两棵树的根的关系进行更新:

即unite函数内的r[]更新:

定义:fx 为 x的根节点, fy 为 y 的根节点,联合时,使得 pre[fx] = fy (即fy也变为x和fx的祖先)
同时也要更新 fx 与 fy 的关系(此时fy是fx的祖先)

void unite(int x, int y) 
{ 
    int fx = find(x); //x所在集合的根节点 
    int fy = find(y); 
    pre[fx] = fy; //合并 
    r[fx] = (r[x]+1+r[y])%2; //下面讲解证明,先弄清楚如何进行的更新
    						//fx与x关系 + x与y的关系 + y与fy的关系 = fx与fy的关系 
}

相关子节点进行更新:

我们知道:find函数其实是一个递归函数。

在find函数内,若我们如此调用find(a),那么find函数除了返回a的祖先,还会在这过程中确定r[a]的值(即a与祖先结点的关系)。所以说,是在寻找祖先的同时,一步步更新权值。

int find(int x)             //找根节点 
{ 
    if(x == pre[x]) return x; 
    int t = pre[x];             //记录父亲节点 方便下面更新r[] 
    pre[x] = find(pre[x]); 
    r[x] = (r[x]+r[t])%2;   //关系推导公式下面证明,先弄清楚如何进行的更新 
    						//根据子节点与父亲节点的关系和父节点与爷爷节点的关系,推导子节点与爷爷节点的关系
    return pre[x];          //容易忘记 
}

所以要在find函数内进行更新(这样才能判断r[a]==r[b]是否成立)。下面,我们先解释find函数内r[]是怎么更新的,再解释unite函数内r[]是怎么更新的。

五、公式证明

find():r[x] = (r[x]+r[t])%2;

我们先解释:根据子节点a与父亲节点b的关系r1和父节点b与爷爷节点c的关系r2推导子节点a与爷爷节点c的关系r3。一直递归到子节点a与祖先节点n的关系rn

很容易通过穷举发现其关系式:a 和 b 的关系为 r1, b 和 c 的关系为r2,则 a 和 c 的关系r3为: r3 = ( r1 + r2) % 2; //(PS:因为只用两种情况所以对 2 取模)
在这里插入图片描述

unite():r[fx] = (r[x]+1+r[y])%2;

证明:fx 与 x 的关系是 r[x], x 与 y 的关系是 1 (因为联合函数是接受输入“D”时才调用的,即不同类),y与 fy 关系是 r[y],模 2 是因为只有两种关系,所以由上面的一点所推出的定理可以证明 fx 与 fy 的关系是: (r[x]+r[y]+1)%2;

完结撒花


全部代码

#include<cstdio>
#include<iostream>
using namespace std;
const int maxn = 100000+10;

int pre[maxn];         //存父亲节点
int r[maxn];         //存与根节点的关系,0 代表同类, 1代表不同类
int T,n,m;

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

int find(int x)
{
    if(x == pre[x])    return x;
    int t = pre[x];
    pre[x] = find(pre[x]);
    r[x] = (r[x]+r[t])%2;
    return pre[x];
}

void unite(int x,int y)
{
    int fx = find(x);
    int fy = find(y);
    pre[fx] = fy;
    r[fx] = (r[x]+1+r[y])%2;
}

int main()
{
    scanf("%d",&T);
    while(T--){
        scanf("%d%d",&n,&m);
        init();
        int a,b;
        char ch;
        while(m--){
            getchar();
            scanf("%c%d%d",&ch,&a,&b);
            if(ch == 'D'){
                unite(a,b);
            }
            else{
                if(find(a) == find(b)){
                    if(r[a] == r[b]){
                        cout<<"In the same gang.\n";
                    }
                    else{
                        cout<<"In different gangs.\n";
                    }
                }
                else{
                    cout<<"Not sure yet.\n";
                }
            }
        }
    }
    return 0;
    
}

poj1703

相关例题

有了并查集进阶的思想,相关问题迎刃而解。

题意:给定n只虫子。规定不同性别的可以在一起,相同性别的不能在一起。给m对虫子,教授判断每对都为异性, 问是否有错误判断,即存在x, y为同性

因为刚刚做完了上道题,这道题一看到,上面那道题开始就给出了顶点之间关系,而这道题没有给出。

你可能会疑惑刚开始的一对虫子性别关系怎么判断?

我们可以转变以下思维,反证法,假设前面的教授判断都是正确的, 连通(联系)就建立起来了,若后面存在与前面判断矛盾的数据,那么教授判断有误;

每得到一对虫子,我们就判断它们是否属于同一连通分支,如果是,那就可以这俩虫子有关系,我们再判断它们各自与根结点的性别是否相同,如果两个性别都与根结点的性别相同,说明这一对虫子的性别相同,答案就出来了。

我们可以用rank[x]记录x与其父亲节点的关系, rank[x]=0表同性, rank[x]=1表异性;
代码几乎一样

#include <iostream>
#include <stdio.h>
#define MAXN 2010
using namespace std;

int rank[MAXN], pre[MAXN]; //***rank[x]存储x与其父亲节点的关系

int find(int x){  //***递归压缩路径
    if(x!=pre[x]){
        int px=find(pre[x]);
        rank[x]=(rank[x]+rank[pre[x]])%2;  //***跟新rank[x]
        pre[x]=px;
    }
    return pre[x];
}

int jion(int x, int y){
    int px=find(x);
    int py=find(y);
    if(px==py){
        if((rank[x]+rank[y])%2==0){ //**rank得出x, y的关系为同性,又由题意得出输入的x, y是异性, 矛盾
            return 1;
        }else{
            return 0;
        }
    }else{
        pre[py]=px; //**合并
        rank[py]=(rank[x]+rank[y]+1)%2; //**更新rank[py]
    }
    return 0;
}

int main(void){
    int t, m, n;
    scanf("%d", &t);
    for(int k=1; k<=t; k++){
        scanf("%d%d", &n, &m);
        for(int i=0; i<=n; i++){
            pre[i]=i;
            rank[i]=0;
        }
        int flag=0;
        while(m--){
            int x, y;
            scanf("%d%d", &x, &y);
            if(jion(x, y)){
                flag=1;
            }
        }
        if(flag){
            printf("Scenario #%d:\nSuspicious bugs found!\n\n", k);
        }else{
            printf("Scenario #%d:\nNo suspicious bugs found!\n\n", k);
        }
    }
    return 0;
}

总结

估计你的脑子里会形成这一个想法:建立一个连通(将顶点扯上关系),将需要确定关系的顶点放入连通里,就能得到结果。

后面呢,咱们都直接进入poj1182食物链吧。

这只是我参考别的博主的帖子,加上自己学习过程中的理解,如有误请指正。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Strive_LiJiaLe

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值