并查集包括两个操作:合并集合(union)、查找代表元(find)。
简单来说呢,就是找朋友、划分朋友圈的过程。比如现在有100个人,我们已知一些人是朋友关系,例如已知张三和赵四是朋友,赵四和王五是朋友,那么朋友的朋友也是朋友,自然而然,张三和王五也是朋友了。有些人是不管拖了多少中间人都不可能建立朋友关系的人,那就不是一个朋友圈的。
怎么判断两个人是不是一个朋友圈的呢?
我们可以在一个朋友圈里推选出一个交际花,大家都认识他,只要两个谈话,person1:我认识那谁谁!person2:哎呀,那么巧,我也认识那谁谁。鉴定完毕:你们就是一个圈的。
那么100个人分朋友圈的过程就可以归成两步。通过已知的朋友关系,合并集合,已知person1,person2认识,那就union(person1,person2)。
如果person1,person2认识,person2,person3认识,这时已经union(person1,person2)和union(person2,person3),再出现person1,person3认识,还需要重复合并吗?当然没有必要,这时查找代表元就起作用了,对于已合并到一个集合的,我们都知道自己的代表元,如果我们查找两个人的代表元一样,说明两人之间已有一条连线(可以建立朋友关系),就不用再重复加入了。第二个操作就是,在加入集合之前查找代表元,判断是不是已建立关系,不用再重复建立了。
我觉得最能体现并查集思想的是,图的连通性。比如:K算法求最小主树。
K算法求最小主树的过程是:先将边集按从大到小排序,逐个加入边,如果加入的此边不会形成环,则加入此边,否则不加入。
那么我们就可以用并查集解决它。对于要加入的这条边,我们查询这条边的两个点是否为同一个代表元,是的话,就说明,这两个点已在一个集合,相互之间已经能够连接到达,那么再加入一条两个点之间的边,就会形成环。
如果两个点不是同一个代表元,就可以合并入最小主树集。
细节解释在代码中注释。
import java.util.Scanner;
public class Main
{
static class Edge
{
int from;
int to;
int weigth;
public Edge(int from, int to, int weigth)
{
this.from = from;
this.to = to;
this.weigth = weigth;
}
}
public static void main(String args[])
{
//测试
Scanner sc = new Scanner(System.in);
int N = sc.nextInt(); //节点个数
int M = sc.nextInt(); //边个数
Edge[] edges = new Edge[M];
for(int i=0;i<M;i++)
{
int f = sc.nextInt();
int t = sc.nextInt();
int w = sc.nextInt();
edges[i] = new Edge(f,t,w);
}
//这里其实应该加一个排序操作,按边的权值大小对边排序,为了方便,我就把输入改成了就是排好序的边
int ans = minTree(edges,N);
System.out.println(ans);
}
static int minTree(Edge[] edges,int N) //edges为边集,N是节点个数
{
int sum=0; //主树的最大权值
int[] parent = new int[N+1]; //记录每个点的代表元
for(int i=1;i<=N;i++)
{
parent[i]=i; //初始情况下,每个点都以自己为代表元,自己就是一个集合
}
for(int i=0;i<edges.length;i++) //逐个边加入,一条边一条边并入集合
{
int f = edges[i].from;
int t = edges[i].to;
//查代表元
int root1 = find(parent,f);
int root2 = find(parent,t);
if(root1!=root2) //代表元不同,则可并入
{
parent[root2]=root1; //一个代表元并入另一个之下,两个集合也就合并了
//说明这条边加入了最小主树集合,那就加上这条边的权值
sum+=edges[i].weigth;
//为了输出能看出包括了哪几个边,这里输出一下并入了最小主树的边
System.out.println(f+" -> "+t+" : "+edges[i].weigth);
}
}
return sum; //返回
}
static int find(int parent[],int node)
{
int index = node;
while(node!=parent[node])
{
node = parent[node]; //不停的往上级找代表元
}
while(index!=node) //将此节点找代表元过程中经过的点,压缩,归入代表元的下一级直接管理
{
int temp = parent[index];
parent[index]=node; //经过的点的父级直接改为代表元
index = temp;
}
return node;
}
}
测试输入:
6
7
1 2 9
2 3 7
1 3 6
3 5 5
4 5 5
1 5 3
3 6 2
测试输出:
1 -> 2 : 9
2 -> 3 : 7
3 -> 5 : 5
4 -> 5 : 5
3 -> 6 : 2
28