并查集算法

本文深入探讨并查集的数据结构及其操作,包括合并、判断集合归属和路径压缩,并通过实例解析并查集在实际问题解决中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

并查集是一种树型的数据结构,用于处理一些不相交集合的合并问题。

并查集的主要操作有

1-合并两个不相交集合

2-判断两个元素是否属于同一个集合

3-路径压缩

 

并查集的精髓

1Make_Set(x) 把每一个元素初始化为一个集合

初始化后每一个元素的父亲节点是它本身,每一个元素的祖先节点也是它本身(也可以根据情况而变)。

2、Find_Set(x) 查找一个元素所在的集合

查找一个元素所在的集合,其精髓是找到这个元素所在集合的祖先!这个才是并查集判断和合并的最终依据。
判断两个元素是否属于同一集合,只要看他们所在集合的祖先是否相同即可。
合并两个集合,也是使一个集合的祖先成为另一个集合的祖先。

3Union(x,y) 合并x,y所在的两个集合

合并两个不相交集合操作很简单:
利用Find_Set找到其中两个集合的祖先,将一个集合的祖先指向另一个集合的祖先。


并查集的优化:

1Find_Set(x)时 路径压缩
寻找祖先时我们一般采用递归查找,但是当元素很多亦或是整棵树变为一条链时,每次Find_Set(x)都是O(n)的复杂度,有没有办法减小这个复杂度呢?
答案是肯定的,这就是路径压缩,即当我们经过"递推"找到祖先节点后,"回溯"的时候顺便将它的子孙节点都直接指向祖先,这样以后再次Find_Set(x)时复杂度就变成O(1)了。

可见,路径压缩方便了以后的查找。

2Union(x,y)时 按秩合并
即合并的时候将元素少的集合合并到元素多的集合中,这样合并之后树的高度会相对较小。

主要代码实现

int father[MAX];   /* father[x]表示x的父节点*/
int rank[MAX];     /* rank[x]表示x的秩*/
 
/* 初始化集合*/
void Make_Set(int x)
{
    father[x] = x; //根据实际情况指定的父节点可变化
    rank[x] = 0;   //根据实际情况初始化秩也有所变化
}
 
/* 查找x元素所在的集合,回溯时压缩路径*/
int Find_Set(int x)
{
    if (x != father[x])
    {
        father[x] = Find_Set(father[x]); //这个回溯时的压缩路径是精华
    }
    return father[x];
}
/* 
   按秩合并x,y所在的集合(即按照树形的高度,将高度低的合并到高的里面去)
   下面的那个if else结构不是绝对的,具体根据情况变化
   但是,宗旨是不变的即:按秩合并,实时更新秩。
*/
void Union(int x, int y)
{
    x = Find_Set(x);
    y = Find_Set(y);
    if (x == y) return;
    if (rank[x] > rank[y]) 
    {
        father[y] = x;
    }
    else
    {
        if (rank[x] == rank[y])
        {
            rank[y]++;
        }
        father[x] = y;
    }
}

使用并查集查找时,如果查找次数很多,那么使用朴素版的查找方式肯定要超时。比如,有一百万个元素,每次都从第一百万个开始找,这样一次运算就是10^6,如果程序要求查找个一千万次,这样下来就是10^13,肯定要出问题的。

  这是朴素查找的代码,适合数据量不大的情况:

int findx(int x)
{
    int r=x;
   while(parent[r] !=r)
        r=parent[r];
   return r;
}

  

    下面是采用路径压缩的方法查找元素:

int find(int x)       //查找x元素所在的集合,回溯时压缩路径
{
    if (x != parent[x])
    {
        parent[x] = find(parent[x]);     //回溯时的压缩路径
    }         //从x结点搜索到祖先结点所经过的结点都指向该祖先结点
    return parent[x];
}


    上面是一采用递归的方式压缩路径, 但是,递归压缩路径可能会造成溢出栈,下面我们说一下非递归方式进行的路径压缩:

int find(int x)
{
    int k, j, r;
    r = x;
    while(r != parent[r])     //查找跟节点
        r = parent[r];      //找到跟节点,用r记录下
    k = x;        
    while(k != r)             //非递归路径压缩操作
    {
        j = parent[k];         //用j暂存parent[k]的父节点
        parent[k] = r;        //parent[x]指向跟节点
        k = j;                    //k移到父节点
    }
    return r;         //返回根节点的值            
}

例题一:

题目来源:http://acm.hdu.edu.cn/showproblem.php?pid=1232

题目描述

Problem Description

某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府畅通工程的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路? 

 

Input

测试输入包含若干测试用例。每个测试用例的第1行给出两个正整数,分别是城镇数目N ( < 1000 )和道路数目M;随后的M行对应M条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。为简单起见,城镇从1N编号。 
注意:两个城市之间可以有多条道路相通,也就是说
3 3
1 2
1 2
2 1
这种输入也是合法的
N0时,输入结束,该用例不被处理。 

 

Output

对每个测试用例,在1行里输出最少还需要建设的道路数目。 

 

Sample Input

4 2

1 3

4 3

3 3

1 2

1 3

2 3

5 2

1 2

3 5

999 0

0

 

Sample Output

1

0

2

998

 

题目大意

根据某省城镇交通状况以及现有城镇道路统计表(表中列出了每条道路直接连通的城镇)问,使得全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。

解题思路

这是一道最赤果果的并查集问题。首先通过城镇的个数来确认至少需要多少条道路,然后将已有的道路输入,判断是否已有重复道路(如果没有重复,就将道路总数减一),最终得到最少还需要建设多少条道路。

代码如下:

#include <stdio.h>
#include <string.h>
int per[1010];
int n,m;
 
void prime()//初始化 
{
    int i;
    for(i=1;i<=n;i++)
        per[i]=i;
}
 
int find(int x)//找根节点 
{
    int r=x,i,j;
    while(r!=per[r])
        r=per[r];
    return r;
}
 
bool join(int x,int y)//判断是否是一个集合 
{
    int fx=find(x);
    int fy=find(y);
    if(fx==fy)
        return false;
    else
        per[fy]=fx;
        return true;
}
 
int main()
{
    int i,j,x,y;
    int sum;
    while(scanf("%d",&n),n)
    {
        sum=n-1;//道路的总数目 
        scanf("%d",&m);
        prime();
        for(i=0;i<m;i++)
        {
            scanf("%d%d",&x,&y);
            if(join(x,y))
                sum--;//减去已有道路数目 
        }
        printf("%d\n",sum);
    }
    return 0;
}

 

例题

题目来源:http://acm.hdu.edu.cn/showproblem.php?pid=1272

题目描述

 %�� style="color:rgb(124,169,237)">Problem Description

上次Gardon的迷宫城堡小希玩了很久(见Problem B),现在她也想设计一个迷宫让Gardon来走。但是她设计迷宫的思路不一样,首先她认为所有的通道都应该是双向连通的,就是说如果有一个通道连通了房间AB,那么既可以通过它从房间A走到房间B,也可以通过它从房间B走到房间A,为了提高难度,小希希望任意两个房间有且仅有一条路径可以相通(除非走了回头路)。小希现在把她的设计图给你,让你帮忙判断她的设计图是否符合她的设计思路。比如下面的例子,前两个是符合条件的,但是最后一个却有两种方法从5到达8。 

 

Input

输入包含多组数据,每组数据是一个以0 0结尾的整数对列表,表示了一条通道连接的两个房间的编号。房间的编号至少为1,且不超过100000。每两组数据之间有一个空行。 
整个文件以两个-1结尾。

 

Output

对于输入的每一组数据,输出仅包括一行。如果该迷宫符合小希的思路,那么输出"Yes",否则输出"No"

 

Sample Input

6 8  5 3  5 2  6 4

5 6  0 0

 

8 1  7 3  6 2  8 9  7 5

7 4  7 8  7 6  0 0

 

3 8  6 8  6 4

5 3  5 6  5 2  0 0

 

-1 -1

 

Sample Output

Yes

Yes

No

 

题目大意

判断设计图是否符合任意两个房间有且仅有一条路径可以相通(除非走了回头路)。

解题思路

首先将道路依次输入,然后判断是否有重复道路,如果有,那怕是只有一条重复道路,即为不符合要求。

代码如下:

#include <stdio.h>
#include <string.h>
#define N 100010
int per[N];
int vis[N];//做标记 
bool flag;
 
void prime()//初始化 
{
    for(int i=1;i<N;i++)
        per[i]=i;
    memset(vis,0,sizeof(vis));
}
 
int find(int x)
{
    int r=x;
    while(r!=per[r])
        r=per[r];
    return r;
}
 
bool join(int x,int y)
{
    int fx=find(x);
    int fy=find(y);
    if(fx==fy)
        flag=false;//表示已成环 
    else
        per[fy]=fx;
}
 
int main()
{
    int a,b;
    while(~scanf("%d%d",&a,&b))
    {
        flag=true;
        if(a==-1&&b==-1)
            break;
        if(a==0&&b==0)//特殊数据 
        {
            puts("Yes");
            continue;
        }
        prime();
        vis[a]=vis[b]=1;
        join(a,b);
        while(scanf("%d%d",&a,&b),a||b)
        {
            vis[a]=vis[b]=1;
            join(a,b);
        }
        int num=0;
        for(int i=1;i<N;i++)
        {
            if(vis[i]&&i==per[i])//被标记的根节点是自己 
                num++;
            if(num>1)
            {
                flag=false;
                break;
            }
        }
        if(flag)
            puts("Yes");
        else
            puts("No");
    }
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值