您的位置:首页 > 理论基础 > 数据结构算法

数据结构与算法学习-并查集

2016-10-08 23:27 323 查看
并查集(Union-Find Set):

用于解决若干的不相交集合的如下几种操作的统称:

1.MAKE-SET(x):初始化操作,建立一个只包含元素x的集合。

2.UNION(x,y):合并操作,将包含x和y的集合合并为一个新的集合。

3.FIND-SET(x):查询操作,计算x所在的集合。

通常使用这种数据结构来表示集合,树中的每一个结点代表一个元素,每棵树代表一个集合。那么如何判断两棵树是不是同一个集合呢?

举个例子,比如包含点1,2,3,4,5,6的图有三棵树{1,2},{3},{4,5,6},每棵树的根结点就是代表这个集合的代表元,1!=3,3!=4,所以一共有3个集合。如果两棵树的根节点相同,则代表这两棵树是同一个集合。

以上只是并查集的基本操作,未进优化的并查集的查询操作的时间复杂度是O(n),这是因为如果对应到森林都是点连到一条链的一端,则就退化成了一个链表,有点类似于二叉平衡树的诞生原因。为了改善这种情况,一般有两种方法,一种是按秩合并优化,另外一种是路径压缩优化。

按秩合并优化:

秩的意思可以简单理解为一棵树的深度。按秩合并优化是指在合并操作的时候,我们可以通过比较即将合并的两棵树的深度大小,把深度较小的合并到深度较大的树上,这样就可以防止树退化成一条链了。

路径压缩优化:

在查询操作的时候,我们往往是通过一个结点递归查找到父结点,在递归的过程中我们可以改变树的形状来防止树退化成链。意思就是把树上的每个结点的父结点都设置成根结点。使树成为一棵深度为1的树。

这两种优化操作一同使用的话可以使并查集的查询操作达到常数级的时间复杂度,效率非常高,但是按秩合并优化需要开销额外的空间来存储每棵树的秩。

初始化、合并、查询和两种优化的操作代码如下:

#include <iostream>
using namespace std;
class   DisjointSet
{
public:
int *father;  //记录父节点
int *rank;  //记录秩
DisjointSet(int size)
{
father = new int[size];
rank = new int[size];
for(int i = 0; i < size; i++)
{
father[i] = i; //初始化时每个父节点是自己本身
rank[i] = 0; //初始化时每个结点的秩都为0
}
}
~DisjointSet()
{
delete []father;
delete []rank;
}
int find_set(int node)  //查找根结点
{
if(father[node] != node)
{
father[node] = find_set(father[node]);  //压缩路径优化
}
return father[node];
}
bool merge(int node1, int node2) //合并操作
{
int ancestor1 = find_set(node1);
int ancestor2 = find_set(node2);
if(ancestor1 != ancestor2)
{
//比较两棵树之间秩的大小
if(rank[ancestor1]>rank[ancestor2])
{
father[ancestor2] = ancestor1;
}
else
{
father[ancestor1] = ancestor2;
rank[ancestor2] = max(rank[ancestor2], rank[ancestor1]+1); //更新秩
}
return true;
}
return false;
}
};
int main()
{
int n, m;   //n表示元素个数,m表示有关系的元素个数
int count = 0;
cin>>n>>m;
DisjointSet dus(n);
for(int i = 0; i < m; i++)
{
int a, b;
cin>>a>>b;
dus.merge(a, b);
}
for(int i =0; i < n; i++)
{
if(dus.father[i] == i)  //输出集合的个数
{
count++;
}
}
cout<<count<<endl;
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: