数据结构 之 并查集(Disjoint Set)

一、并查集的概念:

首先,为了引出并查集,先介绍几个概念:

1、等价关系(Equivalent Relation)

自反性、对称性、传递性。

如果a和b存在等价关系,记为a~b。

2、等价类:

一个元素a(a属于S)的等价类是S的一个子集,它包含所有与a有关系的元素。注意,等价类形成对S的一个划分:S的每一个成员恰好互斥地出现在一个等价类中。为了确定是否a~b,我们仅需验证a和b是否属于同一个等价类即可。

3、并查集:

即为等价类,同一等价类(并查集)中元素两两存在等价关系,不同并查集元素之间没有等价关系。

4、相关性质:Find/Union

输入数据最初是N个集合的类(Collection),每个集合含有一个元素。初始的描述是所有的关系均为false(自反的除外)。每个集合都有一个不同的元素,从而S(i)&&S(j) = NULL,这使得这些集合是不相交的(Disjoint)。

此时有两种运算可以进行,即找出给定元素所属的集合和合并两个集合。

一种运算是Find,它返回包含给定元素的集合(即等价类)的名字。

玲一种运算是添加关系。如果我们希望添加关系a~b,那么我们首先需要知道a和b是否已经存在关系。这可以通过对a和b执行Find检验它们是否属于同一个等价类来完成。如果它们不在同一类中,那么我们使用求并运算(Union),这种运算把含有a和b的两个等价类合并成一个新的等价类。

二、并查集的基本数据结构:

我们直接引出表示并查集的最佳数据结构,至于为什么不选择诸如链表等其他数据结构,可以参考《算法导论》和《数据结构和算法分析》。

在这里,我们使用树来表示每一个集合,因为树上的每一个元素都有相同的根。这样,我们就可以用根来命名所在的集合。

那么对于每一个元素,我们应该记录其所在的集合,这里,我们可以假设树被非显式地存储在一个数组中:数组的每个成员p[i]表示元素i的父亲。如果i是根,那么p[i]=0(也可以设置为p[i]=i)。

对于这样一个集合,当我们执行两个集合的Union运算,我们是一个节点的根指针指向另一棵树的根节点。

Union(5,6):

Union(7,8):

Union(5,7):

经过这样的过程后,上面的树的非显式表示(第一行为节点i的父节点p[i],第二行为节点i)即为:


0

0

0

0

0
5
5

7

1

2

3

4

5

6

7

8

或者(根节点p[i]=i)

1
2

3

4

5
5
5

7

1

2

3

4

5

6

7

8

三、并查集操作实现:

现在,我们考虑上面所提及的Find-Union操作的具体实现。

并查集操作:

makeSet(int x[]):建立一个新的集合,其唯一成员就是x。

unionSet(int x, int y):将包含x和y的动态集合(比如S(x)和S(y))合并为一个新的集合(即这两个集合的并集)。

int findSet(int x):返回x所在的集合编号。


    声明:

p[]数组——p[i]的值即为节点i的父节点

NumSets——const常量,表示初始集合的个数,亦即初始元素的个数


    makeSet(int x[])函数:

void makeSet(int x[])

{

int i;

for(i=0;i<NumSets;i++)

{

p[i] = 0;//Or p[i] = i

}

}

函数解析:

这个初始化过程,即是将数组中每个元素进行父节点赋值,这样即可确定元素的所属集合。由于初始化时,每个元素各自为单独的集合,因此,我们设置每一个元素的父节点为0(或节点自己)。


    unionSet(int x, int y)函数:

void unionSet(int x, int y)

{

int a = findSet(x);

int b = findSet(y);

p[b] = a;

}

函数解析:

unionSet函数实现的是将两个元素对应的集合合并起来。在这里,我们实现的是基础方法,其优化方法在后面的优化部分会有讲解。

这里的基础方法,实现的思想为:将两个集合合并的方法,即是将一个集合的根节点的父节点设置为另一个集合的根节点。

根据上面的思想,这里的步骤就显而易见了:

首先通过findSet函数得到两个元素对应的集合编号,然后将其中一个集合的根节点父节点设置为另一集合的根节点,即完成了所有的合并操作。


    findSet(int x)函数:

int findSet(int x)

{

if( p[x] <= 0)  // Or if( p[x] == x )

return x;

else

return findSet(p[x]);

}

函数解析:

根据查找步骤,如果节点的父节点为0(或节点自己),那么就返回节点自己,因为该节点即为该树的根节点;如果节点的父节点不为0(或者不是节点自己),那么就说明该节点存在父节点,那么就返回该父节点所在的集合编号,此集合编号亦为该节点的集合编号。

根据上面的代码,我们发现其是由递归的算法思想来实现的,递归终止条件为查找到根节点处,递归模式的理论基础为节点所属集合与节点的父节点所属集合是同一集合。


四、并查集操作的优化:

首先,我们先看这样一个例子:

初始时,我们有5个元素,各自形成5个独立的集合:

   现在,我们按照前面的unionSet函数进行这样一些集合合并操作:

unionSet(2,1):

unionSet(3,1):

unionSet(4,1):

现在,我们考查这个过程,当我们执行完三次unionSet操作后,这个集合便成为了一个链表形式。这样导致的问题就是查找效率会很低。

为什么链表的整体性能较差呢?

对于并查集,我们经常使用的是Find-Union操作。如果使用链表数据结构来存储并查集,我们常常指定链表的第一个元素作为它所在集合的代表。那么对于查找Find操作,对于尾端的元素,要查找至链表头节点,需要遍历整个链表,其时间复杂度为O(n)。而对于合并union操作,将两个元素所在的集合合并起来,即将两个链表合并起来,一般可以使用两种方法,一种是头头相连,一种是头尾相连。但是,无论是哪种合并方法,都需要经历查找过程,找到链表的头节点,然后再进行连接操作。综合看来,其查找和合并操作所需的时间复杂度高于根树的实现方法。

在这里,我们所采用的优化方式是尽量避免合并之后树的深度过分增加的启发式策略。

第一种启发式是按秩合并(Union by Rank)。其思想是使包含较少节点的树的根指向包含较多节点的树的根。对每个节点,用秩表示节点高度的一个上界。在按秩合并中,具有较小秩的根在Union操作中要指向具有较大秩的根。

第二种启发式即路径压缩(Path Compression)。路径压缩在一次findSet2操作期间执行而与用来执行unionSet的方法无关。设操作为findSet2(x),此时路径压缩的效果是,从x到根的路径上的每个节点都使它的父节点变为根。

现在,一一讲解这两种方法:

1、按秩合并(Union by Rank):

这里,我们需要增加字段rank。在初始化过程中,需要对每个节点(集合)进行rank字段的初始化。

(1)、makeSet(int x[])


void makeSet(int x[])

{

int i;

for(i=0;i<NumSets;i++)

{

p[x[i]] = 0;//Or p[i] = i

rank[i] = 0;

}

}



    (2)、按秩合并过程:具有较小秩的根在Union操作中要指向具有较大秩的根。



    void unionByRank(int x, int y)

{

int fx = findSet(x);

int fy = findSet(y);

if( rank[fx] > rank[fy])

p[fy] = fx;

else

{

p[fx] = fy;

if(rank[fx] == rank[fy])

rank[fy]++;

}

}

函数解析:

这里主要集中这样几个问题:

a、为什么要找到根节点?

b、哪些情况下需要更新秩?

c、对于不同的秩的大小比较情况应采取何种不同的措施?

解答:

a、我们合并两个元素,即是合并两个元素分别对应的集合,也就是两棵不同的树。为了使合并后的树的深度的增加尽可能小,我们应该考虑的是树的秩,即根节点的秩,而不是儿子节点的秩。

b、更新秩的原因在于合并后的集合的秩发生了变化。而发生变化的情况只有一种:那就是合并前的两个集合的秩相等,这样必然会有其中一个集合的树的根指向另外一个集合的树的根节点,这样新的集合的秩必然增加1。

c、我们在合并时采用秩小的树合并到秩大的树,比较之后,通过改变p[i]即可实现合并过程。

举例说明这样合并的好处:

现有操作union(5,3):

对于传统的Union操作,即函数unionSet(5,3):

首先,找到5,3对应的集合,即findSet(5)=4, findSet(3)=1;

然后,p[1] = 4;

于是得到如下集合1:

对于按秩合并,即函数unionByRank(5,3):

首先,找到5,3对应的集合,即findSet(5)=4, findSet(3)=1;

然后,由于4的rank小于1的rank,从而有p[4] = 1;

于是得到如下集合2:

通过对比集合1和集合2,显然集合2的构造更为均匀,会给查找操作带来很大的便利。


2、路径压缩(Path
Compression):

对于路径压缩,其关键过程在于一定要使树的结构偏向于直连根节点的节点数增加,从而减少Find操作。

对于整个路径压缩过程,其核心就在于每次findSet操作过后均需将遍历到的节点动态直连到根节点上。也就是说,每次findSet时都会对整个集合的树结构进行调整。



    void findSetWithPathCompression(int x)

{

if(p[x] == 0) //Or if(p[x] == x)

return x;

else

return p[x] = findSetWithPathCompression(p[x]);

}

函数解析:

仔细观察findSetWithPathCompression()函数与findSet()函数的区别,其区别就在于 p[x]
= findSetWithPathCompression(p[x])。这个区别所产生的影响在什么地方呢?

举个例子:

对于下图所示的集合(根树),我们希望找到6所对应的集合编号:

显然,对于findSet(6),其需要遍历节点6,2,1,4,经过四个步骤才能找到根节点,得到集合编号4。

而对于findSetWithPathCompression(6),其同样会遍历节点6,2,1,4,但是,由于p[x] = findSetWithPathCompression(p[x])的作用,p[6] = p[2] = p[1] =4。

从而,整个集合的根树就发生了结构性调整:

树的结构发生的变化带来的是后面查找操作的简化,同时没有改变整个集合的内容(我们不关注集合是怎样实现和存放的,我们关注的是集合中到底有哪些关系和元素)。


五、并查集应用实例:

描述

若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易。现给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。 规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚。

Input

第 一行:三个整数n、m、p (n< =5000,m< =5000, p< =5000),分别表示有n个人,m个亲戚关系,询问p对亲戚关系。

接下来的m行:每行两个数Mi、Mj (1< =Mi、Mj< =N),表示Mi和Mj具有亲戚关系。

接下来的p行:每行两个数Pi、Pj,询问Pi和Pj是否具有亲戚关系。

Output

p行,每行一个’Yes’或’No’。表示第i个询问的答案为“具有”或“不具有”亲戚关系。

问题分析

对于该问题,初步的感觉是利用图的连通性来实现,但是,由于数据量的巨大,要使用图来实现这样一个过程是不现实的。

注意,题目中的规定实质上指定了x与y的等价关系。因此,综合此题,我们可以使用并查集的方式来实现题目求解。这样,题意就转换为:

n个元素,通过m个等价关系构建集合,判断p对元素是否在同一集合中。

代码解答


    #include <stdio.h>

#define MAX 5000

int p[MAX];

void makeSet(int x);

void unionSet(int x, int y);

void findSetWithPathCompression(int x);

int main(void)

{

int n,m,p;

int a,b;

int x,y;

scanf("%d %d %d", &n, &m, &p);

makeSet(n);

while(m--)

{

scanf("%d %d", &a, &b);

unionSet(a,b);

}

while(p--)

{

scanf("%d %d",&x,&y);

if( findSetWithPathCompression(x) == findSetWithPathCompression(y) )

printf("Yes\n");

else

printf("No\n");

}

}

void makeSet(int x)

{

int i;

for(i = 0; i < x; i++ )

p[i] = 0;

}

Other Functions is Writen in above Content.


时间: 2024-08-18 00:46:43

数据结构 之 并查集(Disjoint Set)的相关文章

并查集(Disjoint Set)

在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中.这一类问题其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受:即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在规定的运行时间(1-3秒)内计算出试题需要的结果,只能用并查集来描述. 本文地址:http://www.cnblogs.com/archimedes/p/disj

编程算法 - 并查集(disjoint set) 代码(C)

并查集(disjoint set) 代码(C) 本文地址: http://blog.csdn.net/caroline_wendy 并查集(disjoint set)是一种常用的数据结构.树形结构, 包含查询(find)和合并(unite)操作. 时间复杂度O(a(n)), 比O(logn)要快. 代码: class DisjoinSet { static const int MAX_N = 10000; int par[MAX_N]; int rank[MAX_N]; public: void

【经典数据结构】并查集

等价关系与等价类 若对于每一对元素(a,b),a,b∈S,a R b或者为true或者为false,则称在集合S上定义关系R.如果a R b为true,那么我们说a与b有关系. 等价关系(equivalence relation)是满足下列三个性质的关系R: (1) 自反性:对于所有a∈S,a R a (2) 对称性:若a R b当且仅当b R a (3) 传递性:若a R b且b R c 则a R c 关系“≤”不是等价关系.虽然它是自反的(即a≤a).可传递的(即由a≤b和b≤c得出a≤c)

数据结构之并查集Union-Find Sets

1.  概述 并查集(Disjoint set或者Union-find set)是一种树型的数据结构,常用于处理一些不相交集合(Disjoint Sets)的合并及查询问题. 2.  基本操作 并查集是一种非常简单的数据结构,它主要涉及两个基本操作,分别为: A. 合并两个不相交集合 B. 判断两个元素是否属于同一个集合 (1)       合并两个不相交集合(Union(x,y)) 合并操作很简单:先设置一个数组Father[x],表示x的"父亲"的编号.那么,合并两个不相交集合的方

数据结构之并查集

并查集(Union-find Sets)是一种非常精巧而实用的数据结构,它主要用于处理一些不相交集合的合并问题.一些常见的用途有求连通子图.求最小生成树的 Kruskal 算法和求最近公共祖先(Least Common Ancestors, LCA)等. 使用并查集时,首先会存在一组不相交的动态集合 S={S1,S2,?,Sk},一般都会使用一个整数表示集合中的一个元素. 每个集合可能包含一个或多个元素,并选出集合中的某个元素作为代表.每个集合中具体包含了哪些元素是不关心的,具体选择哪个元素作为

【数据结构】并查集

 [并查集] 为实现 在 不相交集合 上的操作 (1.合并两个集合  2.查询某个元素属于哪个集合)而定义的一种数据结构     其实现有两种方式:链表和有根树 [应用] 在图论中 一个联通分量的所有点 对应一个集合 对应的操作可以为 判断两个点是不是在同一个联通分量之中 添加一条边合并两个联通分量 [模板] 此处用树来实现 用数组储存 [优化] (1)路径压缩(优化查找操作) (2)通俗点说法就是要合并两个树,将树高度低的接到 高度高的树下, 使合并后的树的高度尽量小 (优化合并操作) [参考

数据结构(并查集||树链剖分):HEOI 2016 tree

[注意事项] 为了体现增强版,题目限制和数据范围有所增强: 时间限制:1.5s 内存限制:128MB 对于15% 的数据,1<=N,Q<=1000. 对于35% 的数据,1<=N,Q<=10000. 对于50% 的数据,1<=N,Q<=100000,且数据均为官方数据. 对于100% 的数据,1<=N,Q<=1000000. 请注意常数因子对于程序运行的影响. 并查集很简单,并查集就是倒序处理,表示删除一个点的标记,删除后不会再加回来,删完后,合并当前点与其

0050数据结构之并查集

-------------------------并查集------------------------- 并查集是一种特殊的树,由孩子指向父亲 用于解决连接问题和路径问题: 判断网络中节点的连接状态 将每一个元素,看做是一个节点,将a和b合并成一个集合的时候,只需要让a所在的根节点指向b所在的根节点即可,而查询两个元素是否在一个集合中,只需要找到各自的根节点,如果两个根节点是同一个根节点,则说明是在同一个集合中:这样查询较快,合并也较快. 并查集接口设计如下: package unionFin

并查集 (Union-Find Sets)及其应用

定义 并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题.常常在使用中以森林来表示. 集就是让每个元素构成一个单元素的集合,也就是按一定顺序将属于同一组的元素所在的集合合并. 主要操作 初始化 把每个点所在集合初始化为其自身. 通常来说,这个步骤在每次使用该数据结构时只需要执行一次,无论何种实现方式,时间复杂度均为O(N). 查找 查找元素所在的集合,即根节点. 合并 将两个元素所在的集合合并为一个集合. 通常来说,合并之前,应先判断两个元素是否属于