[数据结构] 并查集入门

一、问题引入
原题: HDU1232畅通工程
            首先在地图上给你若干个城镇,这些城镇都可以看作点,然后告诉你哪些对城镇之间是有道路直接相连的。最后要解决的是整幅图的连通性问题。比如随意给你两个点,让你判断它们是否连通,或者问你整幅图一共有几个连通分支,也就是被分成了几个互相独立的块。像畅通工程这题,问还需要修几条路,实质就是求有几个连通分支。如果是1个连通分支,说明整幅图上的点都连起来了,不用再修路了;如果是2个连通分支,则只要再修1条路,从两个分支中各选一个点,把它们连起来,那么所有的点都是连起来的了;如果是3个连通分支,则只要再修两条路……

二、并查集原理
            并查集就是由一个整形数组和2个函数构成,一个函数是find():查找一个点的标识,一个union():将2个点进行合并成为一个连通分支。

首先,我们先来看一副图:

在这里插入图片描述            上面表示了3个连通分支。假设不同连通的人就敌人,现在我们需要判断蜘蛛侠和蚁人是不是敌人,那我们该如何判断?我们可以首先找到蜘蛛侠所在分支的标识(钢铁侠),然后再找到蚁人所在分支的标识(美国队长),然后我们判断一下2个人是否是同一个人,很明显不是,所以蜘蛛侠和蚁人就是敌人,现在我们来看一下鹰眼和蚁人是不是敌人,他们的标识都是美国队长,所以他们不是敌人。

现在我们想让美国队长这个分支与钢铁侠这个分支成为一个分支(合并),应该如何做呢?
            我们可以直接让标识:美国队长指向钢铁侠,也就是钢铁侠成为新分支的标识。
在这里插入图片描述下面我们来看如何通过代码进行实现:

	int[] parent; 

parent[]这个数组,记录了每个人的上级是谁。如过这个人的上级是自己,那么他就是标识。也可以一个人就是一个集合,那么他的标识当然是自己了。

int find(int x)                        //查找x的这个分支的标识
{                         
    while(patent[x] != x)                 
        x = patent[x] ;                   
    return  x ;                        
}

find()方法查找x所在分支的标识,patent[x] != x 代表他的上级不是自己,那么他也不是标识,所以继续往上查找,x=patent[x];当x == patent[x] 的时候,代表x就是这个连通分支的标识了,这个时候就返回.

void union(int x,int y)                   
{
    int fx=find(x);
    int fy=find(y);             
    if(fx != fy)                              
        parent[fx]=fy;                       
}

比如我们想让上面的蚁人(x)和蜘蛛侠(y)成为一个阵营的,首先我们先找到x这个分支的表示fx(美国队长)和y这个分支的标识fy(钢铁侠),让fx(美国队长)的上级变成钢铁侠(parent[fx] = fy). 这个时候蚁人和蜘蛛侠就变成一个连通分支里面的点了。

上面就介绍了并查集的基本原理,但是还存在一些问题,接下来我们来看看如何解决这些问题,优化并查集。

三、并查集优化
            我们将两个点进行union的时候完全是随机的,谁当谁的上级都是随机的,这样的话我们的分支结构可能会变成这样:
在这里插入图片描述
变成这样的话,当我们要查找最底层元素所在分支的标识的话会消耗一下时间。所以我们想让我们的分支结构变成所有下级都指向一个同一个上级,这样树的高度只有2,每个人直接与自己的上级就行相连,查找就会节约很多时间。如下图:
在这里插入图片描述
现在我们就来学学如何转换为这种结构也就是路径压缩算法:
1.非递归:

public static int find2(int x) {
    int p = x;
    //寻找这个集合的标识
    while (p != parent[p]) {
        p = parent[p];
    }
    while (x != parent[x]) {
        int tem = parent[x];
        //将每一个点都直接指向标识
        parent[x] = p;
        x = tem;
    }
    //最后返回查找到的标识
    return p;
}

2.递归形式:

int find(int x) {
	if (x == p[x]) return x;
	p[x] = find(p[x]);
	return p[x];
}
public static int find(int x) {
     return x == parent[x] ? x : (parent[x] = find(parent[x]));
 }

下面再来另外一个问题:

在这里插入图片描述如果我们按照上面的union()方法进行合并的话,就会得到图右边的这种情况,这样树形结构又会使查询的速度变慢。所以我们要优化一下union()方法。下图是优化后的结果:
在这里插入图片描述可以看到我们经过优化后深度明显变小了,这样会使查询速度提高很多。
这样的优化算法称为 加权union(),下面看代码:

int rank[N];    //树的高度 
public static void union(int x, int y) {
        int fx = find(x);
        int fy = find(y);
        if (fx == fy) return;
        if (rank[fx] > rank[fy]) {
            p[fy] = fx;
        } else {
            if (rank[fx] == rank[fy]) rank[fy]++;
            p[fx] = fy;
        }
}

首先我们将每个点的高度都初始化为0。然后再进行合并的时候,我们先比较2个标识的高度。rank[fx] > rank[fy]:表示fx这棵树高度大于fy这颗树,这个时候我们可以直接将fy指向fx即可。若进入else:表名fx < 或者 = fy的高度,
所以进行判断一下,如果fx = fy,2棵树的高度相同,我们直接把fy的高度+1,然后让fx指向fy即可,若fx != fy,直接fx指向fy即可。

在这里插入图片描述

三、例题代码

#include <cstdio>
#include <cstring>
using namespace std;
const int N = 1005;
int p[N], rank[N];

int find(int x) {
	return x == p[x] ? x : (p[x] = find(p[x]));
}
void merge(int x, int y) {
	int fx = find(x);
	int fy = find(y);
	if (fx == fy)  return;
	if (rank[fx] > rank[fy]) {
		p[fy] = fx;
	} else {
		if (rank[fx] == rank[fy]) rank[fy]++;
		p[fx] = fy;
	}
}
int n, m, x, y;
void init() {
	memset(rank, 0, sizeof(rank));
	for (int i = 1; i <= n; i++) {
		p[i] = i;
	}
} 
int main() {
	//将p全部置为1 
	while (scanf("%d", &n), n) {
		scanf("%d", &m);
		init();		
		for (int i = 0; i < m; i++) {
			scanf("%d%d", &x, &y);
			merge(x, y);	
		}		
		int ans = 0;
		//统计一下有多少个连通分支
		for (int i = 1; i <= n; i++) {
			if (p[i] == i) ans++;
		} 
		printf("%d\n", ans - 1);
	}	
	return 0;
} 

Note :文章虽然到这里就结束了,但是并查集并不只是这些,这些只是基础入门的东西,若有兴趣可以自己去探索更高深的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值