并查集知识点
以为自己已经会用并查集了呢,碰到了一个带全路径并查集的问题,发现自己居然都没太理解具体思想是什么
啊渣渣
一、举个例子
在讲并查集之前我们先举一个例子。(这个例子是出自别人之手,大都知道的例子)
话说江湖上散落着各式各样的大侠,有上千个之多。他们没有什么正当职业,整天背着剑在外面走来走去,碰到和自己不是一路人的,就免不了要打一架。但大侠们有一个优点就是讲义气,绝对不打自己的朋友。而且他们信奉“朋友的朋友就是我的朋友”,只要是能通过朋友关系串联起来的,不管拐了多少个弯,都认为是自己人。这样一来,江湖上就形成了一个一个的群落,通过两两之间的朋友关系串联起来。而不在同一个群落的人,无论如何都无法通过朋友关系连起来,于是就可以放心往死了打。但是两个原本互不相识的人,如何判断是否属于一个朋友圈呢?
我们可以在每个朋友圈内推举出一个比较有名望的人,作为该圈子的代表人物,这样,每个圈子就可以这样命名“齐达内朋友之队”“罗纳尔多朋友之队”……两人只要互相对一下自己的队长是不是同一个人,就可以确定敌友关系了。
但是还有问题啊,大侠们只知道自己直接的朋友是谁,很多人压根就不认识队长,要判断自己的队长是谁,只能漫无目的的通过朋友的朋友关系问下去:“你是不是队长?你是不是队长?”这样一来,队长面子上挂不住了,而且效率太低,还有可能陷入无限循环中。于是队长下令,重新组队。队内所有人实行分等级制度,形成树状结构,我队长就是根节点,下面分别是二级队员、三级队员。每个人只要记住自己的上级是谁就行了。遇到判断敌友的时候,只要一层层向上问,直到最高层,就可以在短时间内确定队长是谁了。由于我们关心的只是两个人之间是否连通,至于他们是如何连通的,以及每个圈子内部的结构是怎样的,甚至队长是谁,并不重要。所以我们可以放任队长随意重新组队,只要不搞错敌友关系就好了。于是,门派产生了。
二、引入并查集
1、主要函数以及数组定义
用到的数组为 pre[MAX],用来存 i 的上级(即父亲节点)
(1)、初始化函数
int fat[N];
void init(int n)
{
for(int i=1; i<=n; i++)
fat[i]=i;//初始化每个人的上级都是自己
}
(2)、查找祖先函数 —— 朴素做法(适合数据量较小的情况)
1)、循环版
int find(int x) //查找x的掌门
{
int root = x;
while(pre[root] != root) //如果root 的上级不是root 自己(也就是说找到的大侠不是掌门)
root = pre[root]; //root 就接着找他的上级,直到找到掌门为止
return root; //返回掌门
}
2)、递归版
// 此递归版的利用了路径压缩
int find(int x)
{
if(x != pre[x])
pre[x] = find(pre[x]);
return pre[x];
}
(3)、合并函数 —— 意思可以理解为两个点之间连一条线,这样他们原先所在的板块的所有点就都可以互通了
void UNION(int a,int b) // 想让a与b做朋友
{
int root = find(a); // 然后去找他们的上级去判断他们的上级是不是一个人
int root1 = find(b);
if(root != root1) // 如果上级不是一个人,则让两人属于一个掌门
pre[root] = root1;
}
2、提升—— 引入路径压缩与按秩合并
(1)路径压缩:就是在递归找到根节点的时候,把当前节点到根节点间所有节点的父节点都设置为根节点。
(解决了树深度很大的情况,路径压缩之后查找节省了时间)
1)循环版: “两步一跳”
// 路径压缩的意思就是让 x 与 根节点 之间的结点的父亲节点都设置为根节点
// 可以理解为 两步一跳
int find(int x)
{
int root = x,temp = x,t;
while(root != pre[root]) // 首先找到根节点 root
root = pre[root];
while(temp != root){ // 还没到根节点之前
t = pre[temp]; // 用 t 记录下 temp 的父亲节点
pre[temp] = root; // t 与根节点相连
temp = t; // 继续下一次两步一跳,将这次的 t 传给 temp
}
return root;
}
2)递归版:
int find(int x)
{
if(x != pre[x]) // 只要不是根节点,就把父亲节点指向父亲节点的父亲节点
pre[x] = find(pre[x]);
return pre[x];
}
(2)、按秩合并:就是将短的连到长的上树枝上去
该方法使用秩来表示树高度的上界,在合并时,总是将具有较小秩的树根指向具有较大秩的树根。
简单的说,就是总是将比较矮的树作为子树,添加到较高的树中。
void UNION(int a,int b)
{
int root = find(a);
int root1 = find(b);
if(root == root1)
return ;
// a 的深度比 b 的深度大,所以将 b 的父亲节点设置为 a 的父亲节点
if(rank[root] > rank[root1])
pre[root1] = root;
else{
if(rank[root] == rank[root1]) // 如果深度相同的话加入一个之后,深度增加一个
rank[root1]++;
pre[root] = root1;
}
}