入门——并查集

网上有很多大佬,写的十分清楚,看他们的博客就能入门,我就不卖弄了。这里就先引用一个:

https://blog.csdn.net/niushuai666/article/details/6662911

以下是补充说明:

(个人领悟仅供参考)

并查集(Union-Find)是一种数据结构的处理方法,主要是处理点与点之间的联系。如果有很多数据之间有简单的关系,你可以把这些数据抽象成一个个点,那么你可以对他们的关系用作并查集。

比如说,有很多人互相认识,他们能通过朋友来结交朋友的朋友,那么给你n个人,告诉你这n个人谁认识谁,然后问你a和b能否成为朋友。再比如几组数,告诉你这两个数有关系,最后问有多少不同种关系,然后还要分别输出和。这些时候你就能使用并查集。

知道什么时候用并查集了,然后是为什么要用并查集。简单的说,就是为了节省查询的时间。像刚才说的找朋友,输入的数据你要存起来,最暴力的做法是有关系就连一起,组成n条关系链表,然后每条链上有m个人,这样等到最后询问的时候,遍历所有的链表,还要根据链表里的数据跳到另一个链表去检索……说都说不清楚,实现起来就更加麻烦了,再顺着这个错误的想法实现下去说不定就要用上字典树了。如果是刚才说的要输出有关系数的和,那暴力的做法也就可以放弃了,根本无从下手——你没有办法处理n条链上相同的数,就算用bool vis标记的话……好了彻底忘掉暴力做法吧,咱们有更好用的算法,就是今天的主角并查集。

并查集,可以分为“”和““两个过程,精髓就在于每次输入数据时候,要把有关系的数据”并"到一起,这样就能够在“查”的时候快很多了。

像刚才的找朋友,最后输入的数据肯定是要有好几种关系,分成好几堆人。你们这些人都认识对吧?就算有人说:我不认识她,我认识的是她朋友的朋友的朋友。但是根据题意只要能通过朋友认识的都是朋友,所以这两个人也是有关系的,那么这一堆人就可以随便派一个代表出来,说这个人不管怎么找关系都能和所有人攀上朋友,所以我们就让他代表我们的关系网,只要认识他咱们就是朋友。同样,其他堆也都派个代表出来,所以在想知道这两个人是不是朋友,只要让这两个人说出自己关系网的代表就行了,如果相同,那就是朋友,如果不同,那就是路人。特别的,这些代表就是自己就是自己的代表,这种自己是自己的代表的人每个关系网肯定就一个。

一般的,我们会开一个数组pre[]来记录每个数据的祖先(就是刚才说的代表,我习惯叫祖先),然后在输入的时候,因为每次输入两个数据都是要建立这两个数据的联系,判断这俩个数据的祖先是否相同(这是查的过程),如果不同,那么就把他俩的祖先统一(这是并的过程),来让这两个人有关系。等到全部数据输入完,这些数据的关系也都捋顺了,接下来要找任意两个数据a,b是否有关系,就只要判断pre[a]和pre[b]是否相等就行了。

查和并的代码最好写成函数,方便重复调用。

int uf_find(int x) //这就是查的函数
{
    if(pre[x] == x) //咱们在程序的开头会进行初始化,让每一个数的祖先都是他自己,这样最开始就都没有联系了
        return x;   //自己的祖先就是自己的那个数,肯定就是这个关系下所有数的祖先
    return (pre[x] = uf_find(pre[x]));  //不是的话就递归去找,这一行一定要这样写,每次递归的时候顺带把每个pre[x]更新一遍,以防超时
}

void uf_unite(int x, int y) //这是并的函数
{
    x = uf_find(x);         //找x的祖先
    y = uf_find(y);         //找y的祖先
    if(x != y)              //不相等就并起来
        pre[x] = y;         //你写成pre[y] = x;也没问题,这都无所谓。一定是要让一个数的祖先为另一个数
}

并查集思想理解起来肯定没问题,就是代码可能有点难懂,这个代码经过无数代人的优化才形成现在这样,简洁得十分优雅。我举个例子方便你理解。如果有一关系是1,2,3,4,5,这组关系中让1为祖先,还有一关系是6,7,8,9,10,这组关系中让6为祖先,然后再给你一组输入是4,8,这样两组关系就变成同一组关系了,经过“并”,现在6的祖先就不是自己了,而是1。可是,7,8,9,10的祖先现在还是6,我们的更新祖先是再查里面的,所以应用并查集的代码不管在什么时候,想要找这个数的祖先都要用uf_find函数,而不能直接=pre[x]。

猜你喜欢

转载自www.cnblogs.com/jindianli/p/12298890.html