【C语言】并查集配例题讲解,通俗易懂

只是一个鶸用来记录学习内容的文章罢了,如果能多少帮到你那真是太好了,发现错误欢迎指正。

并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中。其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。

以上是并查集的简单介绍,摘自百度百科。

先看一到简单例题帮助理解
在这里插入图片描述
在这里插入图片描述
题目要求能将若干个无关联的数据建立起连系,并能在短时间内完成查找,是一道典型的并查集题目。

并查集的“并”就是将有关联的元素并入同一棵树中,同一棵树即根节点为同一节点的树,后面统称为这个结点的根节点。两节点的根节点相同则两节点在同一集合中,若两节点根节点不相同则他们并不在同一集合中,即在不在同一棵树中。

那么如何知道两节点的根节点是不是相同呢(两节点所在的树的根节点是不是相同,即在不在同一集合),我们可以用数组来记录每个节点的根节点。

查找根节点(向上探测)

定义parent[]数组,数组大小和数据规模相当,我们计划让数组下标对应的值为数组下标的父节点,比如2的父节点是3我们就记录parent[2] = 3。当一个节点没有父节点的时候就记录它的父节点为-1(当然也可以是其他无影响的数)比如1没有父节点,则有parent[1] = -1。

当需要合并两元素的时候只需要向上探测,查看这两个节点的根节点rootA和rootB(parent[rootA]为-1 且 parent[rootB]为-1)是否相等,如果不相等的话就将parent[rootA] = rootB或者parent[rootB] = rootA,意思就是将一棵树的根节点作为子节点连接到另一棵树的根节点上,具体是rootA作为子树连到rootB上还是反过来,取决于两棵树的高度,具体后文会解释,合适的操作可以降低树的深度,从而减少搜索时间。

在这里插入图片描述

举个例子:上图中
parent[0]==2;
parent[2] == parent[3] == 1;
parent[5] == parent[6] == parent[7]==4;
parent[1] == parent[4] == -1;

比方说查找0和5在不在同一集合中,只需要循环向上索引0的父节点和5的父节点直到找到他们各自的根节点即可。0的父节点parent[0]是2,2还有父节点,继续向上探索2的父节点parent[2]是1,parent[1]为-1,说明1为0的根节点。探索5的父节点,parent[5]是4,parent[4]是-1,4为5的根节点,0和5的根节点不同,不在同一集合。

对于查找一个元素x的根节点的代码实现如下

int find(int x,int parent[])
{
    int root = x;
    while(parent[root]!=-1)
    {
        root = parent[root];
    }
    return root;
}

还有更快的方法,一边查找一边压缩路径

int find(int n,int parent[])
{
	if(parent[n] == -1) return n;
	else return parent[n] = find(parent[n],parent);//压缩路径 
}

合并

还是上图的例子,我查找到了0和5不在同一集合中,我现在想在0和5之间建立联系,该怎么做呢。其实,表面上是0和5建立联系,实际上0能联系到0所在的树内的所有元素,5也能连系到5所在的树内的所有元素,即连接到0相当于连接到0,1,2,3,连接到5相当于连接到4,5,6,7。所以,在0和5之间建立连系其实就相当于将0所在的树合并到5所在的树,让他们处在同一集合中。

画在图上就变成了这样
在这里插入图片描述
将5的根节点4作为子节点并在0的根节点1下方,这样0所在的集合和5所在的集合内的所有元素就在同一棵树当中了。

但是为什么是4并在1下方而不是1并在4下方呢,如果是1并在4下方的话就成了这样。
在这里插入图片描述

仔细看并完之后的两棵树,4并在1下的树深度为3,1并在4下的树深度为4,对于同样的数据个数,我们应该尽量压低树的深度,从而提高查找效率,深度为3和深度为4差别看起来还不是很大,来看一个比较极端的例子。

在这里插入图片描述
同样是7个节点,如果树长成这个样子的话查找元素6的根节点需要递归5次,效率很低。

要想查找效率高,树的深度更低,就要尽量让数据分散在根节点的两端,而不是挤在一起。树的深度取决于最长分支的长度。所以,当我们需要将两棵树合并为一棵树的时候,要将深度小的树合并在深度大的树下。假设树A的深度为5,树B的深度为3,将A并在B下得到的新树深度则为6。若将B并在A下则新树的深度仍为5。深度小的树并在深度大的树下得到的新树的最深分支是属于原来深度大的树的,所以新树不会长高。每次合并前比较两树的深度,将矮的树并在高的树下可以降低树随数据输入而长高的速度。

那么怎么比较两树的深度呢,如果在每次比较前都先计算树的深度也太浪费时间了,违背我们的本愿。方法是再维护一个rank[]数组,用于记录每一个根节点的深度,比如rank[root] == 4表示root作为根节点时它的子树深度为4。

先初始化rank[]内的所有数据为1,单个节点的深度记为1。当每次合并时,若深度小的合并在深度大的树下则深度大的树的根节点为新树根节点且该根节点深度不变。若两深度相等的树合并则无所谓谁合并在谁的子树下,只是需要注意新树节点的深度需要加一。

在这里插入图片描述
还是这个例子。

右边树的创建过程中,最开始5并在4下,两者深度都为1(初始化为1),所以rank[4]变为2。6并在4下,rank[6] == 1且rank[4] == 2;所以6并在4下rank[4]不变,同理7的深度为1,4的深度为2,7并在4下且4的深度不变。
左边树创建过程中0并在2下,3并在1下,当需要将2和1并起来的时候rank[2]和rank[1]都为2,那么1做新树节点还是2做新树节点都无所谓了,我画的是1做新树节点,随后让rank[1]++;

代码实现如下

int merge(int x,int y,int parent[],int rank[])
{
    int x_root = find(x,parent);
    int y_root = find(y,parent);
    if(x_root==y_root)
    {
        return 0;//在同一集合
    }
    else
    {
    	if(rank[x_root] < rank[y_root])//深度小的合并到深度大的 
    	{
        	parent[x_root] = y_root;
        }
        else if(rank[x_root] > rank[y_root])
        {
        	parent[y_root] = x_root;
    	}
        else 
       	{
			parent[x_root] = y_root;
			rank[y_root]++;
		}
        return 1;
    }
}

最后附上这道例题的完整AC代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int find(int x,int parent[])
{
    int root = x;
    while(parent[root]!=-1)
    {
        root = parent[root];
    }
    return root;
}

int merge(int x,int y,int parent[],int rank[])
{
    int x_root = find(x,parent);
    int y_root = find(y,parent);
    if(x_root==y_root)
    {
        return 0;//在同一集合
    }
    else
    {
    	if(rank[x_root] < rank[y_root])//深度小的合并到深度大的 
    	{
        	parent[x_root] = y_root;
        }
        else if(rank[x_root] > rank[y_root])
        {
        	parent[y_root] = x_root;
    	}
        else 
       	{
			parent[x_root] = y_root;
			rank[y_root]++;
		}
        return 1;
    }
}

int main()
{
    int n,x,y;
    scanf("%d",&n);
    int parent[n],rank[n];//rank记录树深度 
    for(int i = 0;i < n;i++)
    {
    	rank[i] = 1;
    	parent[i] = -1;
	}
    char c;
    while(1)
    {
        scanf("%c",&c);
        if(c=='S')
        {
            break;
        }
        scanf("%d",&x);
        scanf("%d",&y);
        if(c=='C')
        {
            if(find(x,parent)==find(y,parent))
            {
               printf("yes\n");
            }
            else
            {
                printf("no\n");
            }
        }
        else if(c=='I')
        {
            merge(x,y,parent,rank);
        }
    }
    int count = 0;
    for(int i = 0;i < n;i++)
    {
        if(parent[i]==-1)
            count++;
    }
    if(count==1)
        printf("The network is connected.");
    else
        printf("There are %d components.",count); 
    return 0;
}

  • 8
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值