算法:并查集

并查集是一种用途广泛的数据结构,能够快速地处理集合的合并和查询问题,并且实现起来非常方便,在很多场合中都有着非常巧妙的应用,。
本文首先介绍并查集的定义、原理及具体实现,然后以其在最小生成树算法中的一个经典应用为例讲解其具体使用方法。

一 并查集原理及实现

并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。

并查集在使用中通常以森林来表示,每个集合组织为一棵树,并且以树根节点为代表元素。实际中以一个数组father[x]即可实现,表示节点x的父亲节点。另外用一个变量n表示节点的个数。但为了提升性能,通常还用一个数组rnk[x]表示节点x的子树的深度,具体后面解释。

const int mx = 100000 + 1; //最大节点个数
int n;			//节点个数
int father[mx]; //节点的父亲
int rnk[mx];	//节点的子树深度

并查集通常支持三种操作:初始化、查找、合并。

1. 初始化

初始化把每个点所在集合初始化为其自身,即n个节点就有n个集合。遍历一次n个节点,把每个的父亲初始为自己,子树的深度初始化为0。

void makeSet() //初始化
{
	for (int i = 0; i < n; i++) father[i] = i, rnk[i] = 0;
}

2. 查找

查找元素所在的集合,即根节点。因为一个集合只用根节点作为代表元,查找的时候只要沿着father数组一直往上走直到根节点为止,而根节点的父亲为其本身,于是代码为:

int findSetOriginal(int x) //非路径压缩查找
{
	if (x != father[x]) x = father[x];
	return father[x];
}

但实际中会做一个称为路径压缩的优化。因为从该节点x到根节点可能会有很长的一条路径,查找的时间复杂度极端情况下为O(n)。我们可以在查找的过程中,把每个节点的父亲都指向跟节点,于是查找完成之后原本长度为n的一条路径变成了n条长度为1的路径,这些节点的查找时间复杂相应变成了O(1)。路径压缩的实现如下所示:

int findSet(int x) //递归路径压缩查找
{
	if (x != father[x]) father[x] = findSet(father[x]);
	return father[x];
}

但实际中递归算法可能会造成栈溢出的问题,以下为相应的非递归算法。主要思想是先找到该集合的根节点,然后把路径上的节点的父亲都改为根节点。

int findSetNonRecursive(int x) //非递归路径压缩查找
{
	int root = x;		//根节点
	while (root != father[root])
		root = father[root];
	int tem = x;
	while (tem != root)  //路径压缩
	{
		int temFather = father[tem];//暂存父亲节点
		father[tem] = root;			//更新父亲为根
		tem = temFather;			//移动到父亲节点
	}
	return root; //返回根节点
}

3. 合并

将两个元素所在的集合合并为一个集合。合并的时候先使用2中的查找函数找到两个集合的根节点。如果根节点相同,说明属于同一个集合,则不需要合并。如果不同,只需把一个根节点的父亲指向另一个根节点即可。

实际中使用了一个称为按秩合并的优化,因为直接合并可能产生一棵深度很深的树,这不利于后续的查找。前面的rnk[x]数组表示节点x的秩,即该节点子树的深度。合并时我们总是把秩小的节点的父亲指向秩大的节点之上,这样可以尽量较少新生成的树的深度。当两个节点的秩相同时,新生成树的根节点的秩需要加1,因为子树的深度增加了1,否则子树深度没有变化,秩也不需要改变。

int unionSet(int x, int y) //合并
{
	x = findSet(x), y = findSet(y);
	if (x == y) return 0;	//属于同一个集合,不需合并
	if (rnk[x] > rnk[y]) father[y] = x;
	else father[x] = y, rnk[y] += rnk[x] == rnk[y];
	return 1;				//属于不同集合,且已合并成功
}

并查集(Union-find Sets)是一种非常精巧而实用的数据结构,它主要用于处理一些不相交集合的合并问题。一些常见的用途有求连通子图、求最小生成树的 Kruskal 算法和求最近公共祖先(Least Common Ancestors, LCA)等。

使用并查集时,首先会存在一组不相交的动态集合 S={S1,S2,?,Sk},一般都会使用一个整数表示集合中的一个元素。

每个集合可能包含一个或多个元素,并选出集合中的某个元素作为代表。每个集合中具体包含了哪些元素是不关心的,具体选择哪个元素作为代表一般也是不关心的。我们关心的是,对于给定的元素,可以很快的找到这个元素所在的集合(的代表),以及合并两个元素所在的集合,而且这些操作的时间复杂度都是常数级的。

并查集的基本操作有三个:

  1. makeSet(s):建立一个新的并查集,其中包含 s 个单元素集合。
  2. unionSet(x, y):把元素 x 和元素 y 所在的集合合并,要求 x 和 y 所在的集合不相交,如果相交则不合并。
  3. find(x):找到元素 x 所在的集合的代表,该操作也可以用于判断两个元素是否位于同一个集合,只要将它们各自的代表比较一下就可以了。

并查集的实现原理也比较简单,就是使用树来表示集合,树的每个节点就表示集合中的一个元素,树根对应的元素就是该集合的代表,如图 1 所示。

注意:寻找祖先时我们一般采用递归查找,但是当元素很多亦或是整棵树变为一条链时,每次Find_Set(x)都是O(n)的复杂度,有没有办法减小这个复杂度呢?
答案是肯定的,这就是路径压缩,即当我们经过"递推"找到祖先节点后,"回溯"的时候顺便将它的子孙节点都直接指向祖先,这样以后再次Find_Set(x)时复杂度就变成O(1)了,如下图所示;可见,路径压缩方便了以后的查找。

参考文献:

http://noalgo.info/454.html

http://www.cnblogs.com/cyjb/p/UnionFindSets.html

http://www.ahathinking.com/archives/10.html

时间: 2024-07-29 02:13:55

算法:并查集的相关文章

模板——最小生成树kruskal算法+并查集数据结构

并查集:找祖先并更新,注意路径压缩,不然会时间复杂度巨大导致出错/超时 合并:(我的祖先是的你的祖先的父亲) 找父亲:(初始化祖先是自己的,自己就是祖先) 查询:(我们是不是同一祖先) 路径压缩:(每个点只保存祖先,不保存父亲) 最小生成树kruskal:贪心算法+并查集数据结构,根据边的多少决定时间复杂度,适合于稀疏图 核心思想贪心,找到最小权值的边,判断此边连接的两个顶点是否已连接,若没连接则连接,总权值+=此边权值,已连接就舍弃继续向下寻找: 并查集数据结构程序: #include<ios

克鲁斯卡尔算法+并查集

算法要点:Kruskal算法的最难点在于怎样判断加入边(x,y)后是否形成了环. 问题可化为:判断边(x,y)的两个顶点x,y在图(实际是森林)mst中最否已经连通.如果已经连通,加入边将形成环:否则,不形成环. 在kruskal算法中,要用到并查集的合并和查找 并查集: 1 int getfa(int k) //找到最祖先 2 { 3 if(fa[k]==k) return k; 4 fa[k]=getfa(fa[k]); 5 return fa[k]; 6 } 7 8 void merge(

[经典算法]并查集

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

最小生成树Kruskal算法+并查集实现

今天刚掌握Kruskal算法,写下随笔. 对于稀疏图来说,用Kruskal写最小生成树效率更好,加上并查集,可对其进行优化. Kruskal算法的步骤: 1.对所有边进行从小到大的排序. 2.每次选一条边(最小的边),如果如果形成环,就不加入(u,v)中,否则加入.那么加入的(u,v)一定是最佳的. 并查集: 我们可以把每个连通分量看成一个集合,该集合包含了连通分量的所有点.而具体的连通方式无关紧要,好比集合中的元素没有先后顺序之分,只有"属于"与"不属于"的区别.

算法 - 并查集

并查集 - 知乎专栏 POJ 1611(并查集+知识) POJ 2524(并查集) 这个时候我们就希望重新设计一种数据结构,能够高效的处理这三种操作,分别是 MAKE-SET(x),创建一个只有元素x的集合,且x不应出现在其他的集合中 UNION(x, y),将元素x所在集合Sx和元素y所在的集合Sy合并,这里我们假定Sx不等于Sy FIND-SET(x),查找元素x所在集合的代表 并查集的具体实现. 对于有根树或者说森林的表示,可以用一个数组parent来实现. parent[i]记录元素i的

HDU 2586 How far away? Tarjan算法 并查集 LCA

Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 23506    Accepted Submission(s): 9329 Problem Description There are n houses in the village and some bidirectional roads connecting them. Every da

[SCOI2010][BZOJ1854] 游戏|二分图匹配|匈牙利算法|并查集

1854: [Scoi2010]游戏 Time Limit: 5 Sec  Memory Limit: 162 MBSubmit: 3018  Solved: 1099[Submit][Status][Discuss] Description lxhgww最近迷上了一款游戏,在游戏里,他拥有很多的装备,每种装备都有2个属性,这些属性的值用[1,10000]之间的数表示.当他使用某种装备时,他只能使用该装备的某一个属性.并且每种装备最多只能使用一次. 游戏进行到最后,lxhgww遇到了终极boss

编程算法 - 并查集(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

Prim算法(并查集)

普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树.意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点(英语:Vertex (graph theory)),且其所有边的权值之和亦为最小 基本思想:假设G=(V,E)是连通的,TE是G上最小生成树中边的集合.算法从U={u0}(u0∈V).TE={}开始.重复执行下列操作: 在所有u∈U,v∈V-U的边(u,v)∈E中找一条权值最小的边(u0,v0)并入集合TE中,同时v0并入U,直到V=U为止. 图例

*并查集的题*

POJ 1182 食物链http://acm.pku.edu.cn/JudgeOnline/problem?id=1182题目告诉有3种动物,互相吃与被吃,现在告诉你m句话,其中有真有假,叫你判断假的个数(如果前面没有与当前话冲突的,即认为其为真话)这题有几种做法,我以前的做法是每个集合(或者称为子树,说集合的编号相当于子树的根结点,一个概念)中的元素都各自分为A, B, C三类,在合并时更改根结点的种类,其他点相应更改偏移量.但这种方法公式很难推,特别是偏移量很容易计算错误.下面来介绍一种通用