首先先看一道很简单的并查集的题目:https://vjudge.net/contest/297398#problem/A
这道题就是让你判断两两城镇之间是否联通 如果不联通就要修建一条道路
就我的理解来说,如果单独使用并查集就是为了合并有相同根结点(或者理解成有相同的性质)的这样的数据
这里我们引入一个我在网上看到的一个关于并查集的一个很有意思的故事:
并查集由一个整数型的数组和两个函数构成。数组pre[]记录了每个点的前导点是什么 函数find是查找 函数join是合并。
话说江湖上散落着各式各样的大侠,有上千个之多。他们没有什么正当职业,整天背着剑在外面走来走去,碰到和自己不是一路人的,就免不了要打一架。但大侠们有一个优点就是讲义气,绝对不打自己的朋友。而且他们信奉“朋友的朋友就是我的朋友”,只要是能通过朋友关系串联起来的,不管拐了多少个弯,都认为是自己人。这样一来,江湖上就形成了一个一个的群落,通过两两之间的朋友关系串联起来。而不在同一个群落的人,无论如何都无法通过朋友关系连起来,于是就可以放心往死了打。但是两个原本互不相识的人,如何判断是否属于一个朋友圈呢? 我们可以在每个朋友圈内推举出一个比较有名望的人,作为该圈子的代表人物,这样,每个圈子就可以这样命名“齐达内朋友之队”“罗纳尔多朋友之队”……两人只要互相对一下自己的队长是不是同一个人,就可以确定敌友关系了。 但是还有问题啊,大侠们只知道自己直接的朋友是谁,很多人压根就不认识队长,要判断自己的队长是谁,只能漫无目的的通过朋友的朋友关系问下去:“你是不是队长?你是不是队长?” 这样一来,队长面子上挂不住了,而且效率太低,还有可能陷入无限循环中。于是队长下令,重新组队。队内所有人实行分等级制度,形成树状结构,我队长就是根节点,下面分别是二级队员、三级队员。每个人只要记住自己的上级是谁就行了。遇到判断敌友的时候,只要一层层向上问,直到最高层,就可以在短时间内确定队长是谁了。由于我们关心的只是两个人之间是否连通,至于他们是如何连通的,以及每个圈子内部的结构是怎样的,甚至队长是谁,并不重要。所以我们可以放任队长随意重新组队,只要不搞错敌友关系就好了。于是,门派产生了。
pre[15]=3就表示15号大侠的上级是3号大侠。如果一个人的上级就是他自己,那说明他就是掌门人了,查找到此为止。也有孤家寡人自成一派的,比如欧阳锋,那么他的上级就是他自己。每个人都只认自己的上级。比如胡青牛同学只知道自己的上级是杨左使。张无忌是谁?不认识!要想知道自己的掌门是谁,只能一级级查上去。 find这个函数就是找掌门用的,意义再清楚不过了
int find(int x) { int r = x; while (pre[r] != r) //如果r不是掌门 { r = pre[r]; //我们就继续往上找 } return r; }
查找函数我们还可以用递归的方式来写:
int find(int x) { if (pre[x] == x) { return x; //如果是掌门就返回 } return find(pre[x]); //否则我们继续往上查找 }
再来看看join函数,就是在两个点之间连一条线,这样一来,原先它们所在的两个板块的所有点就都可以互通了。这在图上很好办,画条线就行了。但我们现在是用并查集来描述武林中的状况的,一共只有一个pre[]数组,该如何实现呢? 还是举江湖的例子,假设现在武林中的形势如图所示。虚竹小和尚与周芷若MM是我非常喜欢的两个人物,他们的终极boss分别是玄慈方丈和灭绝师太,那明显就是两个阵营了。我不希望他们互相打架,就对他俩说:“你们两位拉拉勾,做好朋友吧。”他们看在我的面子上,同意了。这一同意可非同小可,整个少林和峨眉派的人就不能打架了。这么重大的变化,可如何实现呀,要改动多少地方?其实非常简单,我对玄慈方丈说:“大师,麻烦你把你的上级改为灭绝师太吧。这样一来,两派原先的所有人员的终极boss都是师太,那还打个球啊!反正我们关心的只是连通性,门派内部的结构不要紧的。”玄慈一听肯定火大了:“我靠,凭什么是我变成她手下呀,怎么不反过来?我抗议!”抗议无效,上天安排的,最大。反正谁加入谁效果是一样的,我就随手指定了一个。这段函数的意思很明白了吧?
void join(int x,int y) { int fx = find(x),fy = find(y); if (fx != fy) { pre[fx] = fy; } }
这么一讲,最开始提及的题目是不是就会了!
AC代码:
#include <cstdio> #include <iostream> using namespace std; const int N = 1010; int fa[N], n, m; int find(int x) { /*if(fa[x] == x) { return x; } return find(fa[x]);*/ return fa[x] = (fa[x] == x) ? x: find(fa[x]); } void unite(int x, int y) { x = find(x), y = find(y); if(x < y) fa[y] = x; else fa[x] = y; } int main() { while(scanf("%d", &n) && n) { scanf("%d", &m); for(int i = 1; i <= n; i ++) fa[i] = i; int v, u; for(int i = 1; i <= m; i ++) { scanf("%d%d", &v, &u); unite(v, u); } int ans = 0; for(int i = 1; i <= n; i ++) if(find(i) == i) ans++; printf("%d\n",ans-1); } return 0; }
前面我们说的这种join函数 它的合并方法不论怎么样都是我们最开始所固定的,所以可能造成我们之后查找根结点可能复杂度会比较高,那么如何去解决这个问题呢?
这里就讲下并查集的路径压缩
压缩的时候其实就是我们引入了一个新的概念就是数的深度也是rank数组
那么判断两个集合合并的条件就根据题目要求的,是A指向B 还是 B指向A 就根据rank函数来决定!
void Union(int i,int j) { i = find(i); j = find(j); if (i == j) return ; if (rank[i] > rank[j]) { pre[j] = i; } else { if (rank[i] == rank[j]) { rank[j]++; } pre[i] = j; } }
所以总的代码:
#define N 105 int pre[N]; //每个结点 int rank[N]; //树的高度 //初始化 int init(int n) //对n个结点初始化 { for(int i = 0; i < n; i++){ pre[i] = i; //每个结点的上级都是自己 rank[i] = 1; //每个结点构成的树的高度为1 } } int find_pre(int x) //查找结点x的根结点 { if(pre[x] == x){ //递归出口:x的上级为x本身,即x为根结点 return x; } return find_pre(pre[x]); //递归查找 } bool is_same(int x, int y) //判断两个结点是否连通 { return find_pre(x) == find_pre(y); //判断两个结点的根结点(亦称代表元)是否相同 } void unite(int x,int y) { int rootx, rooty; rootx = find_pre(x); rooty = find_pre(y); if(rootx == rooty){ return ; } if(rank(rootx) > rank(rooty)){ pre[rooty] = rootx; //令y的根结点的上级为rootx } else{ if(rank(rootx) == rank(rooty)){ rank(rooty)++; } pre[rootx] = rooty; } }
参考:https://www.cnblogs.com/xzxl/p/7226557.html
原文地址:https://www.cnblogs.com/-Ackerman/p/11067481.html