并查集

一 并查集介绍

并查集是一种维护集合的数据结构,并查集名字“并” “查” “集”分别取自Union  Find  Set这3个单词。并查集支持下面2个操作

(1)合并:合并2个集合。

(2)查找:判断2个元素是否在一个集合中。

并查集是用数组实现的。

int father[i];

father[i]表示元素i的父亲节点,而父亲节点本身也是这个集合中的元素。father[1]=2表示元素1的父亲节点是元素2,以这种父亲关系来表示元素所属的集合。如果father[i]=i则表示元素i是这个集合的根节点。

对同一个集合来说只存在一个根节点,且将其作为所属集合的标识。

如图所示,1 2 3 4同属于一个集合,根节点是1。5 6同属于一个集合,根节点是5。这是2个不同的集合。

二 并查集的基本操作

1 初始化

一开始,每个元素都是一个独立的集合

for(int i=1;i<=N;i++)
{
    father[i]=i;
}

2 查找

由于集合中只存在一个根节点,因此查找操作就是对给定的节点寻求根节点的过程。实现的方式可以是递归和循环。直到找到father[i]=i的点。

递推/循环

int findfather(int x)
{
    while(x!=father[x])
    {
        x=father[x];
    }
    return x;
}

递归

int findfather(int x)
{
    if(x==father[x])
        return x;
    else
        return findfather(father[x]);
}

3 合并

合并指把2个集合合并为1个集合。一般给出2个元素,要求把这2个元素所在的集合合并。

一般,我们需要先判断这2个元素是否属于2个集合,如果不是,就不用合并。

如果不在同一个集合,可以把其中一个集合的根节点的父亲指向另一个集合的根节点。

 

void Union(int a,int b)
{
    int faA=findfather(a);
    int faB=findfather(b);
    if(faA!=faB)
    {
        father[faA]=faB;
    }
}

在合并的过程中,只对2个不同的集合进行合并,如果2个元素在相同的集合中,就不会进行操作。保证了在集合中一定不会出现环,即并查集产生的每一个集合都是树。

三 路径压缩

上述并查集查找函数没有经过优化,因为在极端情况下效率极低。如果有100000个数字连接成一条链,那么查询就需要100000次。

优化:

因为查找都是查找根节点,那么可以直接令father[i]=根节点即可。即把当前查询节点的路径上的所有节点的父亲都指向根节点。

如何实现:

之前的查找函数,是从给定节点不断获得其父亲节点而最终到达根节点。

转换过程可以分为:

(1)按照查找根节点的写法先获取x的根节点r

(2)重新从x走一遍寻找根节点的路程,把路径上经过的所有节点的父亲全改为根节点r。

循环写法

int findfather(int x)
{
    int a=x;   //保存初始节点
    while(x!=father[x])
    {
        x=father[x];
    }
    //现在x是根节点,现在把路径上所有节点的父亲节点都改为根节点
    
    while(a!=father[a])
    {
        int z=a;   //保存当前节点
        a=father[a];
        father[z]=x;
    }
    
    return x;    //返回根节点
}

递归写法

int findfather(int x)
{
    if(x==father[x])
        return x;
    else
    {
        int f=findfather(father[x]);
        father[x]=f;
        return f;
    }
}

四 题目练习

(一) 好朋友

题目描述:

有一个叫做数码宝贝的奇异空间,其中有很多数码宝贝,他们之间可以是好朋友。

A与B是好朋友意味着B与A也是好朋友。

A与C是好朋友,B与C是好朋友,则A与B也是好朋友。

现在给出这些宝贝中所有好朋友的信息。问:这些宝贝可以分为多少组(每组内任意2个宝贝都是好朋友),且任意2组之间的宝贝都不是好朋友。

输入格式

第一行有2个整数n和m(都小于等于100),分别表示数码宝贝的个数和好朋友的组数。接下来有m行,每行2个整数表示一组朋友关系。

输出格式

输出一个整数,表示这些宝贝可以被分为的组数。

样例输入

4 2

1 4

2 3

样例输出

2

思路

每个好朋友组可以看作一个集合,朋友关系可以看作2个节点的边。在输入这些好朋友关系时就可以同时进行并查集的合并操作,在处理完毕后得到一些集合,集合的个数就是组数。

对于集合的个数,由于每个集合都有唯一的一个根节点作为所属集合的标识。因此可以开一个bool数组记录每个节点是否作为集合的根节点。

#include <iostream>
#include <cstdio>
using namespace std;

const int maxn=110;
int father[maxn];
bool isRoot[maxn];

int findfather(int x)
{
    while(x!=father[x])
    {
        x=father[x];
    }
    return x;    //返回根节点
}

void Union(int x,int y)
{
    int fax=findfather(x);
    int fay=findfather(y);
    if(fax!=fay)
    {
        father[fax]=fay;
    }
}

int main()
{
    int n,m,a,b;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        father[i]=i;
        isRoot[i]=false;
    }

    for(int i=0;i<m;i++)
    {
        scanf("%d%d",&a,&b);
        Union(a,b);
    }

    for(int i=1;i<=n;i++)
    {
        isRoot[findfather(i)]=true;
    }

    int ans=0;
    for(int i=1;i<=n;i++)
    {
        ans+=isRoot[i];
    }
    printf("%d\n",ans);
    return 0;
}


(二)几个岛

岛的个数等于原来岛的个数-新建立的岛与原来的岛建立的桥数(用并查集算出来的桥)+1

#include <iostream>
using namespace std;

int X[4]={0,0,-1,1};
int Y[4]={1,-1,0,0};   //2个数组模拟四个方位

const int maxn=100;

int a[maxn][maxn]={0};   //地图初始化为水
int flags[10000]={0};        //flags[i]表示元素i的父亲节点
int b[10000]={0};            //存储最终岛屿数目

int m,n,k,sum;

int find_father(int x)
{
    while(x!=flags[x])
    {
        x=flags[x];
    }
    return x;
}

void check(int x,int y)
{
    for(int i=0;i<4;i++)
    {
        int newx=x+X[i];
        int newy=y+Y[i];
        if(newx<0||newx>=m||newy<0||newy>=n||a[newx][newy]==0)
        {
            continue;
        }
        int f_1=find_father(a[x][y]);
        int f_2=find_father(a[newx][newy]);
        if(f_1!=f_2)
        {
            sum--;
            flags[f_1]=flags[f_2];
        }
    }
}

int main()
{
    while(cin>>m>>n>>k)
    {
        int x,y;
        sum=0;
        for(int i=1;i<=k;i++)
        {
            cin>>x>>y;
            if(x<0||x>=m||y<0||y>=n||a[x][y]!=0)   //若输入的坐标超出了边界/重复,岛屿个数没有变化,但仍需要输出数值
            {
                b[i]=sum;
                continue;
            }
            flags[i]=i;
            a[x][y]=i;   //极其重要
            check(x,y);
            b[i]=++sum;
        }

        for(int i=1;i<=k;i++)
        {
            if(i!=k)
                cout<<b[i]<<" ";
            else
                cout<<b[i]<<endl;
        }
    }
    return 0;
}

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值