你真的了解并查集?

并查集

从本篇文章你可以收获什么

本篇文章会说明并查集是什么,其构造是什么,对应的核心算法,以及优化算法——状态压缩。还有场景的一些使用场景。会有代码,也会有图示进行说明。

并查集能够解决的问题

并查集是一个很小巧实用的算法,通常用于解决 连通性(关联性)集合 的相关问题。
类似于以下的问题

  1. 一个节点能否连通另一个节点。判断两个节点的连通性。
  2. 一个节点是否属于某一个集合

并查集是一棵树

很多地方就将并查集划分到算法中。虽然数据结构和算法不分家,但是如果从数据结构的角度去分析并查集,那么对其的理解会更加深刻。也会应用的更加自如。

先来分析这些问题,的特性是什么。连通性,通常是指一个节点与另一个节点是否连通。集合问题一般是指一个节点是否属于这个集合。
用数据结构的角度去分析,一个节点与其他节点是否连通的问题,其实就是一个节点与哪些节点保持连接,就表示这个节点与这些节点是连通的。
假如一开始的节点是这样的,每一个数据都是独立的。
你真的了解并查集?_第1张图片
那么是否可以根据节点跟节点之间的关系,将其进行链接构造成一棵树,因为树就是一对多的关系,因此是可以用树的结构来存储和表示这些点的关系的。如下图
你真的了解并查集?_第2张图片
如果将各个节点构造成这样的一棵树
如今要判断两个节点之间是否是同一个集合。对于上面这棵树来说就是判断两个节点的是否是属于同一棵树,也就是判断两个节点的根节点是否是相同的。
因此就将原本判断两个节点是否是同一个集合的问题,转化成了是否属于同一棵树的问题。

核心算法

这里对于树的操作有两个

  • 寻找指定节点的根节点
  • 将一棵树与另一树进行连接
    • 在连通性中就是连通两个节点
    • 在集合中就是合并集合

在下列代码中。是使用数组去构造树形结构。而非使用对象的方式,形象的构造。其构造方式是
p[] 表示存储节点的父节点
size[] 表示存储节点所在集合的节点个数
d[] 表示存储节点到根节点的距离
其中这些数组的下标表示的是一个节点。例如一个节点为5
那么
p[5] 表示的是5这个节点的父节点。
size[5] 表示的是5这个节点所在集合的节点个数
d[5] 表示的是5这个节点到达根节点的距离。

获取指定节点的根节点

// 这个是普通的递归搜索
find(int x)
{
	if(p[x] != x) return find(p[x]);
	else return x;
}

// 这个是进行了路径压缩
find(int x)
{
	if(p[x] != x) p[x] = find(p[x]);
	else return p[x];
}

合并两棵树

Unino(int a, int b)
{
	// 让a的父指针指向b
	p[a] = b;
}

使用

if(find(a) == find(b)){
	// a 和 b两个节点属于同一个集合
}

if(find(a) != find(b)){
	// a 和 b 两个节点不属于同一个集合
}

Unino(a,b) 将a,b两个集合合并

优化——状态压缩

你真的了解并查集?_第3张图片
在上图中,每一个箭头表示的都是节点跟节点之间的联系。
find方法寻找一个节点的根节点,就需要根据一个箭头一个箭头的向上寻找。并且每一次都需要这样寻找。
那是否可以第一次寻找完之后,就做一个缓存,或者就在每一个节点上缓存一份根节点的地址,那么下次再次find的时候,就可以根据地址直接获取到根节点,而不需要根据箭头一个个向上寻找了。
你真的了解并查集?_第4张图片
如图Find(A)寻找节点A的根节点时,会经过中间两个节点。在第一次寻找之后,就可以将节点A和中间两个节点直接指向根节点。如果下次再寻找这三个节点的根节点的话就可以直接获取到根节点的内容,而不需要再一个个向上去寻找。

记录到达根节点长度

对于一些特殊的情况,需要记录子节点到达根节点的长度。如果使用上面的状态压缩进行优化的话,那么这个长度也需要跟着维护。
假设使用数组 d[] 来存储每一个节点到达根节点的距离,用 size[] 存储每一个集合的大小。
这个距离的维护需要两步,因为集合的合并具有两种情况:

  1. 单个元素加入集合。两个集合进行合并的时候,其中一个集合中只有一个元素。
    你真的了解并查集?_第5张图片
    对于这种情况,合并操作只会影响到一个单位的距离信息,那么在合并 unino 的时候就可以直接维护了。
Unino(int a,int b)
{
	int pa = find(a);
    int pb = find(b);
	if(pa != pb){
		d[pa] = Size[pb];
		Size[pb] += Size[pa];
		p[pa] = pb;
	}
}
  1. 一个集合加入另一个集合
    你真的了解并查集?_第6张图片
    这种情况下,合并操作会影响到合并结合中的所有元素,这个过程类似于 差分 数组,集合根节点元素增加距离,子节点也需要增加,因此每一个节点到根节点的距离计算方式是一个类似于 前缀和 的操作。
    find 方法执行的时候会遍历一个节点到根节点的所有节点。并且将其指向根节点。在这个操作的过程中,就可以进行前缀和的操作,从而维护这些节点到达根节点的距离。
int find(int x){
    if(x != p[x]){
        int root = find(p[x]);
        d[x] += d[p[x]];
        p[x] = root;
    }
    return p[x];
}

在回溯的时候,相当于是从根节点向子节点反向遍历的过程,对这个过程上的节点进行前缀和操作,即可维护每一个节点到达根节点的距离。
同样的这种情况也需要上面第一种情况中对 Unino 的维护。

总结

本篇文章说明了并查集能够解决哪类问题。这些问题都是比较典型的。并且从数据结构的角度去剖析了并查集是一棵树。并且给出了并查集中的核心算法,以及对应的实现代码。能够知道并查集是如何去解决这类连通性,所属集合的相关问题。还分享了如何对find 方法使用状态压缩进行优化,以及如何扩展维护节点到达根节点的距离。
本篇文章会说从数据结构的角度去分析并查集是因为,如果不清楚并查集的本质是一个棵树的话,就很难理解findunino 这两个核心算法具体在做的事情。
但是如果知道了并查集是一棵树的话,那么就非常清晰的知道这两个核心算法就是寻找节点的根节点,和合并两棵树。当知道其核心目的之后,不管之后遇到什么变种的情况也能够灵活实现对应的方法。

你可能感兴趣的:(数据结构,算法,数据结构)