并查集--算法,优化,变种

一、定义
并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。
基础的并查集能实现以下三个操作:1.建立集合;2.查找某个元素是否在一给定集合内(或查找一个元素所在的集合); 3.合并两个集合.“并”“查”“集”三字由此而来。
并查集能解决的问题一般可以转化为这样的形式:初始时n个元素分属不同的n个集合,通过不断的给出元素间的联系,要求实时的统计元素间的关系(即是否存在直接或间接的联系)。
并查集本身不具有结构,可以用数组、链表以及树等实现。最常用的是数组实现。

二、实现
数组实现:
建立标记数组father,用father[i]表示元素i所属集合的标记。

并查集--算法,优化,变种_第1张图片

图示

1.建立集合
开始时每个元素各自独立,可以把每个元素所属集合标记为其自身序号。

void make()
{
	int i;
	for(i=1;i<=n;i++)
    		father[i]=i;
}

2.合并集合&查找元素所属集合
find函数返回的是元素所属集合的根结点(别忘了并查集是树型的数据结构)。方法就是循环或递归,寻找当前结点的父结点,父结点的父结点,父结点的父结点的父结点......直到找到一个结点的父结点是它自己,那么它就是根结点。

int find(int x)//非递归写法
{
	while(father[x]!=x) x=father[x];
	return x;
}
int find(int x)//递归写法
{
	if(father[x])!=x)	return find(father[x]);
	return x;
}

因此,比较两个元素x,y是否是同一集合的方法就是比较find(x)是否等于find(y)。

bool judge(int x,int y)
{
    x=find(x);
    y=find(y);
    if(x==y)
        return true;
    else
        return false;
}

合并集合的方法就是将其中一个点所在集合的根结点的父结点设定为另一个点所在集合的根结点。

void union1(int x,int y)//union是关键字,不能用,函数名可以随便换一个方便的
{
    x=find(x);
    y=find(y);
    father[y]=x;
}

优化:
1.路径压缩
并查集--算法,优化,变种_第2张图片

前面的做法就是将元素的父亲结点指来指去地指,当这棵树是链的时候,可见判断两个元素是否属于同一集合需要O(n)的时间。
举个例子:在前面方法的第5步后,如果要查询第3、5元素所在集合的根结点,那么每次都需要查询father[3](father[5])、father[2](father[4])、father[1]的值。
于是,路径压缩产生了作用。
路径压缩就是在找完根结点之后,在递归回来的时候顺便把路径上元素的父亲结点都设为根结点。

int find(int x)//递归写法
{
    if(father[x]!=x)
        father[x]=find(father[x]);
    return father[x];
}
int find(int x)//非递归写法,不太好记但是更快,列几组数据试一下也不难理解
{
	int r=x,q;
	while(r!=father[r])
		r=father[r];
	while(x!=r)
	{
		q=father[x];
		father[x]=r;
		x=q;
	}
	return r;
}
(这个优化平时都可以用,但是对于某些题会造成麻烦,例如加权并查集,这样的题需要特殊处理)
2.按秩合并(启发式合并)
在合并两个集合(就是两棵树)的时候,如果待合并的树的深度不相同,那么就有两种选择:一种是以深度较小的树的根结点为新的根结点,另一种是以深度较大的树的根结点为新的根结点。而事实上,选择以深度较大的树的根结点为新的根结点较好,因为这样的话新生成的树深度会更小,可以防止树的退化(退化指越来越接近链表,即深度大而分支少),使资源利用更合理。而合并时这样选择,就叫做“按秩合并”。
按秩合并的基本思想是将深度较小的树指到深度较大的树的根上。
按秩合并需要新开一个数组depth来记录深度。depth[x]是(("以x为根结点的树"的某个叶结点到x的最长路径上)边的数目)的一个最大值。(即以x为根结点的树的树高)
(这个优化比较麻烦,简单题一般不用)
void make()
{
	int i;
	for(i=1;i<=n;i++)
	{
		father[i]=i;
		depth[i]=0;//如果初值为0则可以省略 
	}
}
void union1(int x,int y)
{
	int fx=find(x),fy=find(y);
	if(depth[fx]>depth[fy])
		father[fy]=fx;
	else
	{
		father[fx]=fy;
		if(depth[fx]==depth[fy])
			depth[fy]++;
	}
}
三、并查集的经典应用
①求无向图最小生成树的Kruskal算法
Kruskal将一个连通块当做一个集合(连通块指无向图中相互连通的一些点)。对于一张有n个点的无向图,首先将所有的边按从小到大排序,并认为每一个点都是孤立的,分属于n个独立的集合。然后按顺序枚举每条边。如果这条边连接两个不同的集合,那么就将这条边加入最小生成树,这两个不同的集合就合并成了一个;如果这条边连接的两个点属于同一集合,就跳过。直到选了n-1条边为止。具体参见Kruscal算法。
四、并查集的变种
①带权并查集(加权并查集)
有的时候,不仅需要像普通并查集一样记录一些元素之间有无关系,还需要记录它们之间有怎样的关系,这时候就需要引入加权并查集。
通常情况下,用一个数组r来记录这些关系,r[i]表示元素i与父结点的关系。至于是什么关系,还要根据具体要求来看。
在find(x)函数中进行路径压缩的同时,许多结点的父结点会改变,这时就需要根据实际情况调整权值以保证其正确性。
在union1(x,y)函数中,(不妨设将y集合并入x集合)由于y的父结点的改变,需要调整y对应的权值,但不需要调整y的子结点对应的权值,因为子结点权值会在find(子结点)时得到调整。
典型例题:
1.银河英雄传说

一道裸的加权并查集的题
2.食物链
一道变式题
②种类并查集 --建立补集法
有的时候,元素之间有一些关系,但是关系可以分为几类,要求记录它们之间有怎样的关系,这时可以用建立补集法。
如果有n个元素从a[1]到a[n],它们之间的关系有关系1和关系2两种,那么可以建立数组fa,fa[i]表示与i关系为关系1的元素的集合的序号,fa[i+n]表示与i关系为关系2的元素的集合的序号。当读入i和j为关系1时,则合并i、j所在集合还有i+n、j+n所在集合;当读入i和j为关系2时,合并i、j+n所在集合还有i+n和j所在集合。
典型例题:
1.团队
2.还是食物链(通过上面两个变种的描述,可以看出它们的适用范围有很大重叠)

你可能感兴趣的:(数据结构--并查集)