7.森林与并查集

森林概念

由若干棵互不相交的树组成的数据结构被称为森林。在数据结构里,森林是由若干棵互补相交的树组成的。

森林的遍历

森林有两种遍历方法,分别是先序遍历和后序遍历。请注意,森林是没有和树的中序遍历对应的遍历方法的,因为中序遍历只存在于二叉树中,而森林中的树都是一般树,无法区分左右孩子,自然也就无法进行中序遍历了。二叉树是树的特例,而树也可以看作森林的特例。

森林的先序遍历的规则:

  • 访问森林中第一棵树的根节点
  • 先序遍历森林中第一棵树的根节点的各个子树所构成的森林
  • 先序遍历森林中,除第一棵树外其余树构成的森林

森林的后序遍历的规则:

  • 后序遍历森林中第一棵树的根节点的各个子树所构成的森林
  • 访问森林中第一棵树的根节点
  • 后序遍历森林中除第一棵树外其余树构成的森林

什么是并查集

在计算机科学中,并查集(Merge-Find Set),也被称为不相互交集合(Disjoint Set),是用于解决若干的不相交集合的如下几种操作的统称:

  1. MAKE-SET(x):初始化操作,建立一个只包含元素x的集合
  2. UNION(x,y): 合并操作,将包含x和y的集合合并为一个新的集合
  3. FIND-SET(x): 查询操作,计算x所在的集合
Quick-Find算法
  1. 基于染色的思想,一开始所有点的颜色不同
  2. 连接两个点的操作,可以看成将一种颜色的点染成另一种颜色
  3. 如果两个点颜色一样,证明联通,否则不连通
  4. 这种方法叫做并查集的:【Quick-Find算法】

quick_find算法的联通判断时间复杂度为O(1),合并操作的时间复杂度为O(n);

特性

  1. quick-find算法的联通判断非常快,可视合并操作非常慢
  2. 本质上问题中只是需要知道一个点与哪些点的颜色相同。
  3. 而若干点的颜色可以通过间接指向同一个节点
  4. 合并操作时,实际上是将一棵树作为另一棵树的子树。
Quick-Union算法

通常我们会用有根树来表示集合,树中的每一个结点都对应集合的一个成员,每棵树表示一个集合。每个成员都有一条指向父结点的边,整个有根树通过这些指向父结点的边来维护。每棵树的根就是这个集合的代表,并且这个代表的父结点是它自己。通过这样的表示方法,我们将不相交的集合转化为一个森林,也叫不相交森林。

通常并查集初始化操作是对每个元素都建立一个只包含该元素的集合,这意味着每个成员都是自身所在集合的代表,所以我们只需要将所有成员的父结点设为它自己就好了。

在不相交森林中,并查集的查询操作,指的是查找出指定元素所在有根树的根结点是谁。我们可以通过每个指向父结点的边回溯到结点所在有根树的根,也就是对应集合的代表元素。

并查集的合并操作需要用到查询操作的结果。合并两个元素所在的集合,需要首先求出两个元素所在集合的代表元素,也就是结点所在有根树的根结点。接下来将其中一个根结点的父亲设置为另一个根结点,这样我们就把两棵有根树合并成一棵了。

并查集的查询操作最坏的情况下的时间复杂度为O(n),其中N为总元素个数。最坏情况发生时,每次合并对应到森林上都是一个点连到一条链的一端。可以看出,此时森林相当于退化为一个链表。在这种情况下,如果每次都查询链的最底端,也就是最远离根的位置的元素时,复杂度便是O(n)了。

这种情况下,Quick-Union联通判断的时间复杂度与树高有关,合并操作的时间复杂度也取决于树高。操作退化的具体表现为:

  1. 极端情况下会退化成一条链
  2. 将结点数量多的接到少的上面,导致了退化
  3. 将树高深的接到浅的上面,导致了退化

为了改善时间效率,可以通过启发式合并方法,将包含较少结点的树接到包含较多结点的树根上,可以防止树退化成一条链。另外,我们也可以通过路径压缩的方法来进一步减少均摊复杂度。同时使用这两种优化方法,可以将每次操作的时间复杂度优化至接近常数级。

weighted quick-union 算法 在合并两颗子树的时候,谁的结点数量多,谁就是父节点。

优化后的Quick-Union联通判断的时间复杂度为log(N),合并操作的时间复杂度也为log(N).

例题

题目描述
所谓一个朋友圈子,不一定其中的人都互相直接认识。例如:小张的朋友是小李,小李的朋友是小王,那么他们三个人属于一个朋友圈。现在给出一些人的朋友关系,人按照从 1 到 n 编号在这中间会进行询问某两个人是否属于一个朋友圈,请你编写程序,实现这个过程。

输入
第一行输入两个整数 n,m(1≤n≤10000,3≤m≤100000),分别代表人数和操作数。
接下来 m 行,每行三个整 a,b,c(a∈[1,2], 1≤b,c≤n)
当 a=1 时,代表新增一条已知信息,b,c 是朋友
当 a=2 时,代表根据以上信息,询问 b,c是否是朋友

输出
对于每个 a=2 的操作,输出『Yes』或『No』代表询问的两个人是否是朋友关系。

Quick-Find算法演示
/*************************************************************************
	> File Name: 071.练习题1:朋友圈.c
	> Author: 陈杰
	> Mail: 15193162746@163.com
	> Created Time: 2021年04月07日 星期三 16时28分40秒
  Quick-Find基本操作
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
typedef struct UnionSet{
    int *color;
    int n;
} UnionSet;
UnionSet *init(int n) {
    UnionSet *u = (UnionSet *)malloc(sizeof(UnionSet));
    u->color = (int *)malloc(sizeof(int) * (n + 1));
    u->n = n;
    for(int i = 1; i <= n; i++) u->color[i] = i;
    return u;
}
int find(UnionSet *u, int x) {
    return u->color[x];
}
int merge(UnionSet *u, int a, int b) {
    if(find(u, a) == find(u, b)) return 0;
    for(int i = 1; i <= u->n; i++) {
        if(u->color[i] != u->color[a]) continue;
        u->color[i] = u->color[b];
    }
    return 1;
}
void clear(UnionSet *u) {
    if(u == NULL) return;
    free(u->color);
    free(u);
}
int main() {
    int n, m;
    scanf("%d %d", &n, &m);
    UnionSet *u = init(n);
    for(int i = 0; i < m; i++) {
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);
        if(a == 1) merge(u, b, c);
        else printf("%s\n", find(u, b) == find(u, c) ? "Yes" : "No");
    }
    clear(u);
    return 0;
}
Quick-Union算法演示
/*************************************************************************
	> File Name: 071.练习题1:朋友圈.c
	> Author: 陈杰
	> Mail: 15193162746@163.com
	> Created Time: 2021年04月07日 星期三 16时28分40秒
  Quick-Union基本操作
 ************************************************************************/
#include<stdio.h>
#include<stdlib.h>
typedef struct UnionSet{
    int *father;
    int n;
} UnionSet;
UnionSet *init(int n) {
    UnionSet *u = (UnionSet *)malloc(sizeof(UnionSet));
    u->father = (int *)malloc(sizeof(int) * (n + 1));
    u->n = n;
    for(int i = 1; i <= n; i++) u->father[i] = i;
    return u;
}
int find(UnionSet *u, int x) {
    if(u->father[x] == x) return x;
    return find(u, u->father[x]);
}
int merge(UnionSet *u, int a, int b) {
    int fa = find(u, a);
    int fb = find(u, b);
    if(fa == fb) return 0;
    u->father[fa] = fb;
    return 1;
}
void clear(UnionSet *u) {
    if(u == NULL) return;
    free(u->father);
    free(u);
}
int main() {
    int n, m;
    scanf("%d %d", &n, &m);
    UnionSet *u = init(n);
    for(int i = 0; i < m; i++) {
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);
        if(a == 1) merge(u, b, c);
        else printf("%s\n", find(u, b) == find(u, c) ? "Yes" : "No");
    }
    clear(u);
    return 0;
}
weighted quick-union 算法

算法优化:统计树高,在合并两颗子树的时,谁的结点数量多,谁就是父节点
由于我们任选一个根节点指向另一个根节点,因此,最后可能导致某一棵树过长甚至退化成链表的情况。可以用启发式合并策略来解决这个问题,这里我们就先简单的介绍下这种方法——按秩合并

并查集安秩合并的算法流程如下:

  1. 利用一个数组保存每个节点所在树的节点总数,即保存每个节点的秩。
  2. 分别获得传入的两个节点所在的树的根节点
  3. 比较两个根节点是否相同,相同则返回0,结束合并操作。
  4. 若两个根节点的秩不同,比较他们的秩的大小。
  5. 将秩较小的根节点的父指针指向秩较大的根节点。
  6. 更新合并后的根节点的秩,返回1,结束合并操作
/*************************************************************************
	> File Name: 071.练习题1:朋友圈.c
	> Author: 陈杰
	> Mail: 15193162746@163.com
	> Created Time: 2021年04月07日 星期三 16时28分40秒
  weighted Quick-Union基本操作
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#define swap(a, b){\
    __typeof(a) _temp = a;\
    a = b, b =_temp;\
}
typedef struct UnionSet{
    int *father, *size;
    int n;
} UnionSet;
UnionSet *init(int n) {
    UnionSet *u = (UnionSet *)malloc(sizeof(UnionSet));
    u->father = (int *)malloc(sizeof(int) * (n + 1));
    u->size = (int *)malloc(sizeof(int) * (n + 1));         // 新增一个数组统计结点个数
    u->n = n;
    for(int i = 1; i <= n; i++) {
        u->father[i] = i;
        u->size[i] = 1;
    }
    return u;
}
int find(UnionSet *u, int x) {
    if(u->father[x] == x) return x;
    return find(u, u->father[x]);
}
int merge(UnionSet *u, int a, int b) {
    int fa = find(u, a);
    int fb = find(u, b);
    if(fa == fb) return 0;
    if(u->size[fa] < u->size[fb]) swap(fa, fb);             // 判断结点多少
    u->father[fb] = fa;
    u->size[fa] += u->size[fb];                             // 同时更新节点数
    return 1;
}
void clear(UnionSet *u) {
    if(u == NULL) return;
    free(u->father);
    free(u->size);
    free(u);
}
int main() {
    int n, m;
    scanf("%d %d", &n, &m);
    UnionSet *u = init(n);
    for(int i = 0; i < m; i++) {
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);
        if(a == 1) merge(u, b, c);
        else printf("%s\n", find(u, b) == find(u, c) ? "Yes" : "No");
    }
    clear(u);
    return 0;
}
weighted quick-union with Path Compression算法

路径压缩优化也是为了避免树过长以及过多的单链导致查找效率过低的操作。实际上,在进行路径压缩优化时只需在查找根节点时,将待查找的父节点的父指针指向它们所在的树的根节点就好了。

/*************************************************************************
	> File Name: 071.练习题1:朋友圈.c
	> Author: 陈杰
	> Mail: 15193162746@163.com
	> Created Time: 2021年04月07日 星期三 16时28分40秒
  Path Compression Quick-Union基本操作
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
typedef struct UnionSet{
    int *father;
    int n;
} UnionSet;
UnionSet *init(int n) {
    UnionSet *u = (UnionSet *)malloc(sizeof(UnionSet));
    u->father = (int *)malloc(sizeof(int) * (n + 1));
    u->n = n;
    for(int i = 1; i <= n; i++) {
        u->father[i] = i;
    }
    return u;
}
int find(UnionSet *u, int x) {
    if(u->father[x] == x) return x;
    return u->father[x] = find(u, u->father[x]);
}
int merge(UnionSet *u, int a, int b) {
    int fa = find(u, a);
    int fb = find(u, b);
    if(fa == fb) return 0;
    u->father[fb] = fa;
    return 1;
}
void clear(UnionSet *u) {
    if(u == NULL) return;
    free(u->father);
    free(u);
}
int main() {
    int n, m;
    scanf("%d %d", &n, &m);
    UnionSet *u = init(n);
    for(int i = 0; i < m; i++) {
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);
        if(a == 1) merge(u, b, c);
        else printf("%s\n", find(u, b) == find(u, c) ? "Yes" : "No");
    }
    clear(u);
    return 0;
}
刷题时候的临时并查集
/*************************************************************************
	> File Name: temp_union.c
	> Author: 陈杰
	> Mail: 15193162746@163.com
	> Created Time: 2021年07月05日 星期一 10时46分32秒
  > 刷题过程中的临时并查集
*************************************************************************/
#include <stdio.h>
#define MAX_N 10000
int father[MAX_N + 5];
int find(int x) {
    if(father[x] == x) return x;
    return father[x] = find(father[x]);
}
void merge(int x, int y) {
    int fx = find(x);
    int fy = find(y);
    father[fx] = fy;
}
int main() {
    int n, m;
    scanf("%d %d", &n, &m);
    for(int i = 1; i <= n; i++) father[i] = i;
    for(int i = 0; i < m; i++) {
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);
        if(a == 1) merge(b, c);
        else printf("%s\n", find(b) == find(c) ? "Yes" : "No");
    }
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值