虽说做项目会让人有成就感,但是算法这种考验基础的东西不能落下。更何况我算法那么差呢。最近在刷学校的OJ准备CCF,刷到了并查集,写下这篇文章供自己以后和后来人查阅。如有纰漏,敬请指教。
并查集是一种树形的数据结构,用于处理一些不相交集合的合并和查询问题。常常在使用中以森林来表示
例题
Description
今天是Ignatius的生日,他邀请了许多朋友。现在是吃晚饭的时间,Ignatius想知道他至少需要准备多少桌。必须注意的是,并非所有的朋友都相互认识对方,有的人不愿意和陌生人坐在一桌。针对此问题的一个重要的规则是,如果我告诉你A知道B,B知道C,这意味着,A和C认识对方,这样他们就可以留在一个桌子。但是如果我告诉你,A知道B,B知道C,D知道E,那么ABC可以坐在一起,DE就得另外再坐一桌了。你的任务是请根据输入的朋友之间的关系,帮助Ignatius 求出需要安排多少桌。
Input
输入第一行是一个数字T(1≤T≤25),表示一共有多少组数据。随后跟有T组数据,每一组数据的第一行是两个数字N和M(1≤N,0≤M≤1000)。N代表有多少个朋友,并且朋友从1到N编号。然后有M行输入,每一行输入包含两个int A和B(A!=B),代表A和B互相认识。两组数据之间有一行空行。
Output
对于每一组数据,只需要输出Ignatius至少需要准备多少张台。不要输出任何空格。
样例见OJ
初步分析问题实质上是一个图论中判断两个点是否存在连通子图的问题。但用图的数据结构的最大问题就是,我们无法存下多至(M=)2 000 000条边的图,后面关于算法时效等诸多问题就免谈了。
用图表示关系过于“奢侈”了。其实本题只是一个对分离集合(并查集)操作的问题。
在C++中,具体的实现思路是这样的:
具体给出一个实例
对于上面的这些步骤,我们需要几个数据结构和函数来处理(先不考虑路径压缩)。
- int pre[totalFriends]:存储一个节点的父节点是什么。在本题中最直观的图应该是个无向图。但是为了合并连通的集合,我们将无向图转换为树(对于结果来说,效果是一样的)。
- void find(int x):查找给出的x的根节点
- void join(int x, int y):联合x和y所在的集合(链接根节点)
先贴出自己写的代码,在代码中会有详尽的注释
#include <iostream>
#include <map>
#include <stdlib.h>
using namespace std;
//并查集
//我写的这段代码有问题 三次更改后AC
//不能直接压缩路径
//查询函数
int find(int pre[], int &node){
if(pre[node] != node){
return node;
}
return find(pre, pre[node]);
}
//联合函数
void join(int pre[], int first, int second){
int firstRoot = find(pre, first);
int secondRoot = find(pre, second);
if(firstRoot != secondRoot){
pre[secondRoot] = firstRoot;
//两个节点具有关系,但是根节点不一致,表示原来的集合是两个不同的集合。
//将两个集合的根节点设置成同一个即可将图连通
}
}
int main(){
int totalCases;
cin>>totalCases;
int result[totalCases];
for(int i = 0; i < totalCases; i++){
int totalFriends;
int totalRelationships;
cin>>totalFriends>>totalRelationships;
int pre[totalFriends + 1]; //0不使用
for(int j = 1; j <= totalFriends; j++){
pre[j] = j;
}
for(int j = 0; j < totalRelationships; j++){
int first;
int second;
cin>>first>>second;
// pre[second] = find(pre, first); //不能直接压缩路径,会造成根节点多余的问题
join(pre, first, second);
}
int totalDesk = 0;
for(int j = 1; j <= totalFriends; j++){
if(pre[j] == j){
totalDesk++;
}
}
result[i] = totalDesk;
}
for(int i = 0; i < totalCases; i++){
cout<<result[i]<<endl;
}
system("pause");
}
在本题当中,由于不需要查询两个节点之间的关系,只需要输出至少要准备多少张台(有多少个非联通集合,进一步讲就是有多少个根节点),所以对产生的树进行路径压缩是不必要的。
但是考虑下面这种情况,如果要查询两个人是否能够坐在同一张桌子,那么就要对代表这两个朋友的节点进行查询,如果存在于同一个集合那么他们可以坐在一起,否则就不能。
而由于我们不知道通过上面的代码构造的树是什么样子的,有可能是正常的树,也有可能构造的树比较极端导致树退化成链表,那么查询带来的时间开销将会非常大。那么我们就需要对构造的树进行路径压缩处理。
示意图如下
上面说过,如果想要知道有多少个非联通集合,那么只需要查询有多少个根节点就可以了。而如果想要判断两个节点是否在同一集合内,只要判断他们两个的根节点是否相同即可。那么我们就可以对我们构造的数进行优化,将树的高度减少为2,每个子节点都直接和根节点联通。这样查询的时间消耗将会被大大减少。
给出路径压缩函数
//路径压缩函数
void zip(int pre[], int totalFriends){
for(int i = 1; i <= totalFriends; i++){
pre[i] = find(pre, i);
}
}
上面的知识只能够简单用于判断有多少个非联通集合或者两个节点是否处于一个集合内,而对于比较复杂的问题可能需要带权并查集来解决。希望自己将来能够学到相关的知识。
参考链接:
https://www.cnblogs.com/xzxl/p/7226557.html
https://baike.baidu.com/item/并查集/9388442?fr=aladdin
https://blog.csdn.net/whiskey_wei/article/details/70146305
陈思聪
2019年3月1日 12:08:24