在计算机科学中,并查集是一种树型的数据结构,用于处理一些不交集(Disjoint Sets)的合并及查询问题。有一个**联合-查找算法(union-find algorithm)**定义了两个用于此数据结构的操作:
- Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
- Union:将两个子集合并成同一个集合。
由于支持这两种操作,一个不相交集也常被称为联合-查找数据结构(union-find data structure)或合并-查找集合(merge-find set)。其他的重要方法,MakeSet,用于创建单元素集合。有了这些方法,许多经典的划分问题可以被解决。[1]
并查集
并查集是一种树形结构,又叫“不相交集合”,保持了一组不相交的动态集合,每个集合通过一个代表来实现,代表即是集合中的某个成员,通常选择根节点作为此代表。
主要操作
Make_Set(x )
建立一个新的集合,其唯一成员就是x,因此这个集合的代表也是x,并查集要求各集合是不相交的,因此要求x没有在其他集合中出现过。Find_Set(x)
返回能代表x所在集合的节点,通常返回x所在集合的根节点。有递归和非递归两种方法.Union_Set(x,y)
将包含x,y的动态集合合并为一个新的集合。合并两个集合的关键是找到两个集合的根节点,如果两个根节点相同则不用合并;如果不同,则需要合并。
通过步骤分析,易知:- 当只有一个元素的时候,元素即是集合。
- 每次在连接之后,都会产生一个代表。
- 两个集合连接过程中,小的集合中将代表节点指向大的集合代表节点上。
易知确定两个元素是否属于同一子集,是根据他们是否有同一个代表来判断的.
并查集的优化
[路径压缩]
通过上面分析,容易知道在判断两个节点是否是属于同一子集的时候,如下图b,判断b
,g
是否属于同一子集,b
每次都需要经过b -> h ->c ->f
,g
每次都要经过g - >d -> f
,在判断出两个节点的代表节点为同一个,判断出属于同一个节点。
优化过程是:每次经过了上述过程之后,都将沿途中的几点直接指向代表节点。
如上操作之后,集合中元素变成如下:
查找路径上的每个节点都直接指向根节点,这样下次再找根节点的时间复杂度会变成o(1)
.
【按秩合并】
合并时,如果两个集合的秩相同,任选一个根做为父节点,并增加其秩。
秩不同时,让较小秩的集合指向较大秩的集合,这时秩的大小不变。
秩和集合的数目是不一样的,秩表示节点高度的一个商界;集合的数目表示集合中节点的总数。
代码实现
public class UnionFindSet {
public static class Node{
// whatever
}
public HashMap<Node,Node > fatherMap ; // key: child , value:father
public HashMap<Node,Integer> sizeMap; // 秩的大小, node 代表节点
public UnionFindSet() {
fatherMap = new HashMap<Node, Node>();
sizeMap = new HashMap<Node, Integer>();
}
public void makeSets(List<Node> nodes) {
fatherMap.clear();
sizeMap.clear();
for (Node node : nodes) {
fatherMap.put(node, node);
sizeMap.put(node, 1);
}
}
private Node findHead(Node node) {
Node father = fatherMap.get(node);
if (father != node) {
father = findHead(father);
}
fatherMap.put(node, father);
return father;
}
public boolean isSameSet(Node a, Node b) {
return findHead(a) == findHead(b);
}
public void union(Node a, Node b) {
if (a == null || b == null) {
return;
}
Node aHead = findHead(a);
Node bHead = findHead(b);
if (aHead != bHead) {
int aSetSize= sizeMap.get(aHead);
int bSetSize = sizeMap.get(bHead);
// 按照大小合并
if (aSetSize <= bSetSize) {
fatherMap.put(aHead, bHead);
sizeMap.put(bHead, aSetSize + bSetSize);
} else {
fatherMap.put(bHead, aHead);
sizeMap.put(aHead, aSetSize + bSetSize);
}
}
}
}
应用场景
假设有一比较常见的问题:[3]
在一个地图中,找出一共有多少个岛屿。
我们用一个二维数组表示这个地图,地图中的 1 表示陆地,0 表示水域。一个岛屿是指由上下左右相连的陆地,并且被水域包围的区域。
你可以假设地图的四周都是水域。
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 1, 0, 0, 0, 0, 0 },
{ 0, 1, 1, 1, 0, 0, 0, 0 },
{ 0, 0, 1, 0, 0, 1, 1, 0 },
{ 0, 0, 0, 0, 0, 1, 1, 0 },
{ 0, 0, 1, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 1 },
比如这种情况,期望输出结果为4。
【常规思路】
public class Test {
static int daoyu[][] =
{
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 1, 0, 0, 0, 0, 0 },
{ 0, 1, 1, 1, 0, 0, 0, 0 },
{ 0, 0, 1, 0, 0, 1, 1, 0 },
{ 0, 0, 0, 0, 0, 1, 1, 0 },
{ 0, 0, 1, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 1 },
};
public static void main(String[] args) {
int count = 0;
for (int i = 0; i < daoyu.length; i++)
for (int j = 0; j < daoyu[i].length; j++)
if (daoyu[i][j] == 1) {
count++;
lj(i, j);
}
System.out.println("岛屿数量为:" + count);
}
//递归修改附近数值
static void lj(int i, int j) {
daoyu[i][j] = 2;
// 上边
if (i - 1 >= 0 && daoyu[i - 1][j] == 1)
lj(i - 1, j);
// 下边
if (i + 1 < daoyu.length && daoyu[i + 1][j] == 1)
lj(i + 1, j);
// 左边
if (j - 1 >= 0 && daoyu[i][j - 1] == 1)
lj(i, j - 1);
// 右边
if (j + 1 < daoyu[i].length && daoyu[i][j + 1] == 1)
lj(i - 1, j + 1);
}
}
【并行系统解决方案】
这是比较常见的解决方案,但是,假设我地图非常大,大到单核cpu无法承载运算,此时就需要搭建并行系统。
可以把整个地图划分成比较小的部分,如图3所示,有八个岛屿。
使用并行系统的计算方式,可以将岛屿分成四个部分(当然,还可以更小),如图3绿线分割。每个部分分别计算出各个岛屿的数量,左上为3个,右上为2个,左下为3个,右下为4个。此时需要解决的问题就是各个岛屿的合并问题。易知合并岛屿只需要处理边界问题即可。先考虑左上和右上合并的情况。(其他合并类似,是一个递归过程)。易知c和左上没有边界之间的交叉,判断下一个a,b。
通过并查集判断是否属于同一个子集来判断是否已经在上次判断为属于同一个子集。如果没有,合并。对合并的操作,对两个岛屿的数量相加,在减去合并岛屿的数量。如上图3,左上和右上合并之后,上面的岛屿变为4个。按照此类过程,一直到合并结束。
如果,不用并查集,方法将会变得比较困难。
代码不进行书写 。自行code.
参考:
https://zh.wikipedia.org/wiki/%E5%B9%B6%E6%9F%A5%E9%9B%86 [1]
https://blog.csdn.net/Yu1441/article/details/78895168 [3]