您的位置:首页 > 其它

并查集算法讲解+例题

2016-03-06 01:31 375 查看
定义:并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint
Sets)的合并及查询问题。常常在使用中以森林来表示。

考虑这样一个问题:

小明马上要过生日,要邀请N个好友,而他的好友都只愿意和认识的人坐,给出朋友之间的认识关系,假设所有认识的人坐一桌,问最少需要多少桌子。

规定:若A认识B,B认识C,则A认识C,则ABC全部认识,可以坐一桌;

例如:小明的好友为 A B C D E 五人,关系式 A-B B-C D-E。则此时需要两个桌子:ABC一桌,DE一桌。

输入N表示好友人数,M表示关系数,接下来M行显示所有关系。

样例输入:

9 7
2 4
5 7
1 3
8 9
1 2
5 6
2 3


问题分析:

如果对于所有的朋友编号1~N

不难想到,用集合来表示朋友之间的关系,就样例而言,最初没有给出任何关系的时候,9个人互相都不认识,于是有9个集合:

{1},{2},{3},{4},{5},{6},{7},{8},{9}

当依次输入关系之后,就可以依次合并集合:

输入:2 4 :{1},{2,4},{3},{5},{6},{7},{8},{9}

输入:5 7:{1},{2,4},{3},{5,7},{6},{8},{9}

输入:1 3:{1,3},{2,4},{5,7},{6},{8},{9}

输入:8 9:{1,3},{2,4},{5,7},{6},{8,9}

输入:1 2:{1,2,3,4},{5,7},{6},{8,9}

输入:5 6:{1,2,3,4},{5,6,7},{8,9}

输入:2 3:{1,2,3,4},{5,6,7},{8,9}

最后有3个集合,也就是所有的朋友分为3群,也就是说,需要3张桌子,最后的答案是3.

很简单?是的,让人来合并集合确实很简单,但是如何让计算机去合并集合呢?

你当然可以暴力的合并,但是超时那是妥妥的,于是伟大的并查集算法诞生了!

没错,这就是典型的并查集问题,对此类问题我们先画一下上面的关系:







只是将之前的集合关系画成图,还画的那么丑,并没有什么用啊?

我们开这样一个数组father
;

其中,father[i] = j表示,i这个点的父结点是j

对应上面第3张图,也就是:

father[3] = 1,father[1] = 4,father[4] = 2,father[2] = 2;//想想这里为什么让father[2] = 2

father[7] = 5,father[5] = 6,father[6] = 6;

father[9] = 8,father[8] = 8;

仔细观察上面的关系,例如:father[ father[1] ] = 2;体现了1的父亲的父亲是2。然后你还会发现,上面3排的最后一个关系可以写成father[i] = i。这样的形式告诉我们,i已经是根节点了,没有必要再继续往上面找了。

这个图你懂了,但是这又有什么用呢?

不,你突然发现,用集合表示出来的关系已经可以体现出来了!

{1,2,3,4},{5,6,7},{8,9},这3个集合,以及第三张图......

似的,你发现,在图三中被一根线连起来的一坨就是一个集合,而那一坨的所有元素共有一个根节点!

于是,问题迎刃而解,你只需要把“线”连起来,然后最后找一找根节点的个数就好,然后你又看出father[i] = i就是根节点所具有的性质。

于是我们便可以开始并查集算法了:

首先,不管是集合还是图,最初的初始情况是谁也不认识谁,每个点都是单独的个体,你也可以理解成每个点都是根节点,于是我们这样初始化:

初始化代码如下:

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


找根节点:

int getfather(int x)
{
while (x != father[x])
{
x = father[x];
}
return x;
}


这个地方有人可能会说:你这个循环不就相当于一个if 吗?x!=father[x]就让x=father[x]嘛,然后就可以跳出循环了嘛!

似乎很有道理,但是你就刚刚的“father[3] = 1,father[1] = 4,father[4] = 2,father[2] = 2”来模拟一下,

假设传入的x是3,x!=father[x],于是x = father[x],此时的 x = 1,再带入while循环的判断框,咦,原来这个时候的x=1,father[x] = 4依旧不等,然后你明白了其中的奥妙。

(可能你是一眼看出,但介于本渣当初学并查集的时候看了好久才明白,这里还是提一下,希望对于新手的理解更有帮助)

合并:

void Merge(int a,int b)
{
int fa = getfather(a);
int fb = getfather(b);
if (fa != fb)
{
father[fa] = fb;
}
}


基本上掌握上面3个模板,并查集就算入门了,那么直接上一道模板题练练手

题目传送门:点击打开链接

上AC代码:

//Must so
#include<bits/stdc++.h>
#define mem(a,x) memset(a,x,sizeof(a))
#define sqrt(n) sqrt((double)n)
#define pow(a,b) pow((double)a,(int)b)
#define inf 1<<29
#define NN 1006
using namespace std;
const double PI = acos(-1.0);
typedef long long LL;
int n,m;
int father[NN];
void init()
{
for (int i = 0; i <= n; i++)
{
father[i] = i;
}
}
int getfather(int x) { while (x != father[x]) { x = father[x]; } return x; }
void Merge(int a,int b) { int fa = getfather(a); int fb = getfather(b); if (fa != fb) { father[fa] = fb; } }
int main()
{
int T;
cin>>T;
while (T--)
{
cin>>n>>m;
init();
for (int i = 0,a,b; i < m; i++)
{
scanf("%d%d",&a,&b);
Merge(a,b);
}
int ans = 0;
for (int i = 1; i <= n; i++)
{
if (father[i] == i) ans ++;
}
cout<<ans<<endl;
}
return 0;
}


做完这道简单题,于是你开是在并查集的路上越走越远,然后有一天,你发现你的并查集超时了!!!
于是,你开始知道一种名为“路径压缩”的优化方式:

什么叫“路径压缩”呢?其实就是一张图!

假设我最初给出的关系画成图是这样:



在这个图的基础上,你可能需要多次去找4的根节点,如果每次找都要通过4-3-2-1的路线去找就太傻了,于是有了下面这张图:



在每一次找到一个点的根节点之后,直接把这个点连到根节点上去,这样以后再找就会省下很多时间!

知道这个原理代码再修改起来也很简单,只需要在找爸爸的地方记录一下就好了

int getfather(int x)
{
int xx = x;//保存这个需要找爸爸的点
while (x != father[x])
{
x = father[x];
}
father[xx] = x;//直接将这个点连到根节点上去
return father[xx];
}


然后同样是之前的那个题目可以试着用路径压缩做的试试,对比一下运行时间。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: