部分内容参考http://www.cnblogs.com/SeaSky0606/p/4752941.html
Union-Find问题
1、算法作用
在一堆数据集合中找到两个触点是否被链接在一起,如果不在一起将这两个点链接在一起,并且最后得出这种链接关系一共有多少个。
2、C++实现
2.1.quick-find算法
能实现快速查找是否在一起。原理可以简单理解为:将每个点都赋予一个链接的标号,链接的过程就是把两个触点的标号设为同一个,判断两个点是否被链接就通过查看两者的链接标号是否相同。缺点是:如果要将所有触点都连接,至少要调动 n-1 次unionn(),即至少 (n+3)(n-1)~n^2 次数组访问,find()操作的时间复杂度为:O(l),Unionn()的时间复杂度为:O(N)。
代码如下
#include "test.h"
using namespace std;
//首先构造一个类以实现quick-find算法
class UF
{
int *id;//链条id数组(以触点作为数组下标)
int count;//链条数量
public:
UF(int n)//初始化n个触点
{
count=n;
id=new int[n];
for(int i=0;i<n;i++)
{
id[i]=i;
}
cout<<"初始化了"<<n<<"个触点"<<endl;
}
~UF()
{
delete [] id;
}
//-----------------以下关键代码部分-------------------------//
public:
//在p和q之间添加连接时,检查数组中id和p一样的所有触点,将这些触点的id改为q的链条标识符,这里会将数组遍历一遍
void unionn(int p, int q)
{
int pID=find(p);
int qID=find(q);
for(int i=0;i<m;i++)
{
if(find(i)==pID)
{
id[i]=qID;
}
}
count--;
}
//查找时,只需要直接返回触点对应的id即可
int find(int p)
{
return id[p];
}
//-----------------以上关键代码部分-----------------------//
bool connected(int p,int q)//如果p和q存在于同一个链接则返回true
{
return find(p)==find(q);
}
int getCount()//获取连通的链条数目
{
return count;
}
};
int main() {
cout << "Hello Myself!" << endl;
cout << "输入触点数量n:" ;
int n;
cin >>n;
cout << endl ;
UF uf(n);
while(getchar()!='y')
{
int p,q;
cout<<"输入要连接的p和q"<<endl;
cin>>p>>q;
if(uf.connected(p,q))
{
cout<<p<<"和"<<q<<"已经连接"<<endl;
continue;
}
uf.unionn(p,q);
cout<<p<<"连接上了"<<q<<endl<<"要结束连接吗?(结束请按y,继续请按回车)"<<endl;
getchar();
}
int a=uf.getCount();
cout<<"共有"<<a<<"条链接"<<endl;
cout<<endl<<endl<<endl<<endl;
system("pause");
return 0;
}
2.2.quick-union算法
unionn的复杂度为O(1),find()的复杂度为O(lgN),整体降低了一级。
关键部分代码如下(替换刚才的关键部分即可直接运行):
public:
//在p和q之间添加连接时,将q的根节点的id改为p的根节点的id,这样在查找时,直接查看q的id是否被修改,如果被修改了,查看根即可知id
void unionn(int p, int q)
{
int pID=find(p);
int qID=find(q);
if(qID==pID)
return;
id[qID]=pID;//把q的根节点并到p的根上,最开始的链接的这部分有问题,见下面注释
count--;
}
//而查找时,先确定本身的id有没有被更改,如果更改了往根部回溯查找,这个函数可以理解为一个找根的函数
int find(int p)
{
while(p!=id[p])
p= id[p]; //若找不到,则往根root回溯
return p;
}
注释:原文是将本节点q的id改为父树的根的id,实际上是错的,这样的话这个q节点会从原来的树剥离。
举例说明quick-union算法的特点:
如0,1,2三个触点对应的初始id分别为0,1,2
若1(作为p)连接到2(作为q),执行一次union后,三个id分别为0,2,2,也就是说,触点1的根是2,触点1和触点2形成了一棵树。
然后再执行一次2连接到0(注意不是0到2),执行一次union后,2号触点的id变成了0号的id,也就是三个分别为0,2,0,触点2连接到了触点0上。
这个时候,find一次触点1的id试试,会发现第一次while循环时,找到触点1的根是2,第二次while循环时,找到更上一层的根(触点2的根)是0。此时才能发现,1和0是连接的。
简单的说,这种算法的优点就是,union过程不用遍历数组所有元素,直接替换一个id即可。缺点是find过程比较复杂,假如一棵大树的根连接到了一棵小树上,查找大树的某一个节点与小树的关系时,需要while循环走很久才能找到。
于是出现了加权树算法,无论怎么union,使得小树永远连在大叔上,减少find的难度。
2.3.Weighted-Union-Find加权树算法
原理很简单,增加一个数组用来记录每棵树的节点数,节点少的(小树)不能作为节点多的树的根。
代码如下:
在int *id;下一行增加
int *id;
int *sz;//动态数组sz,用以记录树的节点数
在构造函数中增加
sz=new int[n];
for(int i=0;i<n;i++)
{
id[i]=i;
sz[i] = 1;//开始时所有的链条节点都为1(都只有1个触点)
}
同样地,析构函数中
~UF()
{
delete [] id;
delete [] sz;
}
关键代码中的unionn函数改为
void unionn(int p, int q)
{
int pID=find(p);
int qID=find(q);
if(qID==pID) return;
if (sz[pID] < sz[qID])
{ //通过节点数量,判断树的大小并将小树的根并到大树的根下
id[pID] = qID;
sz[qID] += sz[pID];//将两棵树的节点数相加得到新的节点
}
else
{
id[qID] = pID;
sz[pID] += sz[qID];
}
count--;
}
查找函数find不变,同样是根据回溯根来查找连接关系。
同样以刚才的最简单例子说明:连接触点2和触点0时,因为触点2所在的树刚才已经连接了触点1,节点比较多,因此,触点0的id会跟着触点2变,最后一步的id将会是2,2,2,这样的话,无论是从find(1)还是find(0)都会一步查找到2,两者相等满足connected已连接的条件。加权树算法减少了find的难度,又更快一点。
总结
1、Union-Find问题的应用领域很广,一步一步从quick-find到加权树算法的优化也能让人对算法的优越性产生直观的认识,可以说是很重要的一课了。
2、Union-Find是用来处理一堆数据中的元素两两连接,最后形成的关系网是什么样子的问题的一种模型。
3、quick-find算法是一种最基本的解决方案,原理是为每一个元素(这里叫触点)设置一个标识符id用来表示连接状况,每当两个触点连接时,其中一个触点所连接的所有触点的id都修改为另一个的id。这样查找两个触点对应的id就能快速得知这两个触点是否连接。但是缺点也很明显,每次需要修改的id太多。
4、quick-union算法是进一步的解决方案,原理是将所有触点看作一棵树的节点,每次连接时,只改变子节点的id与父节点一致,实现快速union,但是在find时,针对每个触点,都要追溯他的父节点一直到根部,如果两个触点同属一个根部,说明两个触点连接。
4、Weighted-Union-Find算法是quick-union算法的改进,因为quick-union可能会遇到大树并到小树上的情况,增加树的深度,使得每次find时要追溯很久,极端情况下会完全遍历一遍。而加权树则是增加了一个数组用来记录每棵树的节点数目,使小树永远并到大树上,大大减小了树的深度,提高效率。
–本文仅供个人学习笔记之用,如有错误望请指正,谢谢!