前言
小白发言,整理资料总结了一下并查集的做法步骤,有错误之处敬请指正。
并查集题目常见背景
- 想查清楚有几个犯罪团伙,会给出强盗人数和线索的条数。(题干内容n,m)
- 线索指的是:xx和xxx是同伙为一个线索。例如:给出A是B的同伙,此时可以根据这个线索判断A,B的boss是不是一个人,不是的话进行合并。
并查集题目的主要目的
- 查找——挨个问团中人员他们老大是谁,由老大来定几个独立的团伙。
- 合并——利用题中的线索将各团伙人员规整,团里每个人都指向老大。
并查集的两个原则(包含路径优化)
- 靠左法则:如果不是一个boss,将左边的认为是最大的boss来确定二者合并的最大boss。
- 擒贼先擒王:即不停地去找上级,直到找到老大为止,其实就是去找犯罪团伙的最高领导人。(通过将每个人的上级设置为最终大BOSS来达到路径优化的目的。)
并查集题目的基本思路
1、首先进行初始化,每个人对应的boss设置为自己。
2、 之后根据题目给出的条件(比如1号强盗与2号强盗是同伙),将每条同伙信息通过合并函数(merge)进行合并,(通过查找各自的boss来确定,如果不是一个boss,根据靠左法则,即将左边的认为是最大的boss来确定二者合并的最大boss)
3、 最后可以通过遍历每个强盗存储的大boss名(其他小喽啰的boss不是自己,而大boss的boss是自己来确定是一个团伙,即确定boss个数来确定有几个团伙)来确定有几个独立的团伙。
代码
#include<stdio.h>
int f[1000]={0},n,m,k;
//这里是初始化,每个人对应的boss设置为自己。
void init(){
for(int i=0;i<n;i++){
f[i]=i;
}
}
//这是找爹的递归函数,不停地去找爹,直到找到祖宗为止,其实就是去找犯罪团伙的最高领导人,擒贼先擒王"原则。
//int getf(int v)
//{
// if(f[v]==v)
// return v;
//else{
//这里是路径压缩,每次在函数返回的时候,顺带把路上遇到的人的"BOSS"改为最后找到的祖宗编号,也就是犯罪团伙的最高领导人编号。这样可以提高今后找到犯罪团伙的最高领导人(其实就是树的祖先)的速度。
// f[v]=getf(f[v]);
// return f[v];
// }
//}
//写法二:(思路与上述一样,代码改进)(也一般称为find函数)
int getf(int v) {
return v == f[v] ? v : getf(f[v]);
}
//这里是合并两子集合的函数
void merge(int v,int u)
{
int t1,t2;
t1=getf(v);
t2=getf(u);
if(t1!=t2)//判断两个结点是否在同一个集合中,即是否为同一个祖先。
{
f[t2]=t1;
//“靠左”原则,左边变成右边的BOSS。即把右边的集合,作为左边集合的子集合。
//经过路径压缩以后,将u的根的值即f[t2]也赋值为v的祖先t1。
}
}
// 判断两个节点是不是同一个集合的(不是必写项)
bool same(int x, int y) {
return find(x) == find(y);
}
int main()
{
int i,x,y;
scanf("%d %d",&n,&m); //读取人数和线索数
init(); //初始化每个人的boss
for(i=0;i<m;i++)
{
scanf("%d %d",&x,&y);
merge(x,y); //根据线索来合并犯罪团伙
}
//最后扫描有多少个独立的犯罪团伙
for(i=0;i<n;i++)
{
if(f[i]==i) //即找到几个老大
sum++;
}
printf("%d\n",sum);
return 0;
}
具体问题具体分析:
- 题目分析——需要找的是省份个数,也即查找的是团伙个数。
- 本题给出的是一个n*n的矩阵,与线索有所区别。
- 题中为一个邻接矩阵,邻接矩阵中行列表示存在一条连接,表示可达。即题目中写的 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连(该条信息( isConnected[i][j] = 1 )等同于一条线索)。将每条线索进行合并即可得到有几个独立团伙。
int f[1000]={0},n=0;
void init(){
for(int i=0;i<n;i++){
f[i]=i;
}
}
int getf(int v) {
return v == f[v] ? v : getf(f[v]);
}
void merge(int v,int u)
{
int t1,t2;
t1=getf(v);
t2=getf(u);
if(t1!=t2)//判断两个结点是否在同一个集合中,即是否为同一个祖先。
{
f[t2]=t1;
}
}
int findCircleNum(int** isConnected, int isConnectedSize, int* isConnectedColSize){
n=isConnectedSize;
init(); //初始化每个人的boss
for(int i=0;i<n;i++){
for (int j = i + 1; j < n; j++) {
if(isConnected[i][j]==1)
{merge(i,j);} //根据线索来合并犯罪团伙
}
}
int sum=0;
for(int i=0;i<n;i++)
{
if(f[i]==i) //即找到几个老大
sum++;
}
return sum;
}
并查集优化思路
- 根据上面的靠左原则,可能会出现深度(树的高度)不断变大的情况,因此可以记录两者树的深度,优先将高度低的节点指向高度高的根节点。
- 优化后的程序如下:
#include<stdio.h>
int f[1000]={0}rank[1000]={0},n,m,k;
//这里是初始化,每个人对应的boss设置为自己。
void init(){
for(int i=0;i<n;i++){
f[i]=i;
rank[i]=1; //以该元素为根节点的树的深度就为1
}
}
int getf(int v) {
return v == f[v] ? v : getf(f[v]); //查询该元素的根节点
}
//这里是合并两子集合的函数
void merge(int v,int u)
{
int t1,t2;
t1=getf(v);
t2=getf(u);
// 将原来的靠左法则进行修改,利用两者树的深度来选定合并后的根节点。
// 这里合并的时候判断树的深度,将其深度小的节点指向深度大的根节点
if (rank[t1] < rank[t2]){
parent[t1] = t2; //t1的根节点的深度小,所以将t1的根节点的boss改成t2的根节点
}
else if (rank[t1] > rank[t2]){
parent[t2] = t1; //如果t2的根节点的深度小,t2的根节点的boss改成t1的根节点
}
else{ // rank[pRoot] == rank[qRoot] 相等的情况下,随便指向,但是高度需要维护
parent[t2] = t1; //深度相同时,优先使用靠左法则
rank[t1] += 1; // 相当于t1根节点的深度增加了1
}
}
// 判断两个节点是不是同一个集合的(不是必写项)
bool same(int x, int y) {
return find(x) == find(y);
}
int main()
{
int i,x,y;
scanf("%d %d",&n,&m); //读取人数和线索数
init(); //初始化每个人的boss
for(i=0;i<m;i++)
{
scanf("%d %d",&x,&y);
merge(x,y); //根据线索来合并犯罪团伙
}
//最后扫描有多少个独立的犯罪团伙
for(i=0;i<n;i++)
{
if(f[i]==i) //即找到几个老大
sum++;
}
printf("%d\n",sum);
return 0;
}
总结
- 以上就是一个最基本的并查集模板。
- 本篇只对并查集的做题步骤做一个总结,若想理解并查集工作原理,可参见下面参考文献中大佬们的博文。
参考文献
并查集—解密犯罪团伙
并查集详解 ——图文解说,简单易懂(转)
并查集,不就一并和一查?
夜深人静写算法(五)- 并查集
并查集——你一看就明白就会用
Union-Find 并查集算法详解
Union-Find 算法怎么应用?
数据结构与算法:学习并查集