Kruskal算法,通过收集边的方式来形成最小生成树,一开始将每个顶点看成一颗树,那么形成最小生成树就是是森林合并为树的一个过程,每次都选择一条权重最小的边,同样也是贪心算法的体现,但是要避免形成环。这里就提出了两个问题:
(1)如何保证每次选择的边的权重都是最小的?
(2)如何判断待收集的边,是否会与已经收集的边形成环?
对于问题(1):有两种方法,一种是对边的权重进行从小到大的排序,另外一种则是使用最小堆来实现(效率更好)
针对问题(2):可以通过并查集来判断
这里通过排序的方式来进行简单的说明,对所有的边的权重进行从小到大排序后:
(v1,v4)、(v6,v7)、(v1,v2)、(v3,v4)、(v2,v4)、(v1,v3)、(v4,v7)、(v3,v6)、(v5,v7)、(v4,v5)、(v4,v6)、(v2,v5)
每次都收集权重最小的边,并判断待收集的边,是否会与已经收集的边构成环,直到收集到(n-1)条边为止,其中n表示的顶点个数:
(1)收集(v1,v4)
(2)收集(v6,v7)
(3)收集(v1,v2)
(4)收集(v3,v4)
(5)收集(v4,v7),为什么不收集(v2,v4)、(v1,v3)呢,因为收集它们会构成环
(6)收集(v5,v7),为什么不收集(v3,v6)、(v4,v5)、(v4,v6)、(v2,v5)呢,因为收集它们会构成环,至此,收集了6条边,一共7个顶点,所以已经收集完了。
Java代码实现:
思路:
(1)对边的权重从小到大排序
(2)判断待收集的边,是否会与已经收集的边构成环
(3)合并森林为树
(4)如果有n-1条边,就退出了
//Kruskal算法:求最小生成树
public void kruskal() {
ArrayList<Edge> list=new ArrayList<>();
//初始化边
for(int i=0;i<vertexList.length;i++) {
for(int j=0;j<vertexList.length;j++) {
//如果两个顶点有边
if(mGraph[i][j]!=0 && mGraph[i][j]<Integer.MAX_VALUE) {
list.add(new Edge(i,j,mGraph[i][j]));
}
}
}
//对边按权值排序
Collections.sort(list, new Comparator<Edge>() {
@Override
public int compare(Edge o1, Edge o2) {
return o1.w-o2.w; //权值小的在前
}
});
//初始化并查集,parent[i]=-1;表示这棵树只有它自己,一开始是n棵树
for(int i=0;i<parent.length;i++) {
parent[i]=-1;
}
//下面才是kruskal算法
//list.size()就是边的数量
int u,v,num=0,sum=0,index=0;
char[] result=new char[2*vertexList.length-2]; //记录结果的数组,边的顺序
System.out.println("下面是kruskal算法:");
for(int i=0;i<list.size();i++) {
Edge e=list.get(i);
u=e.u;
v=e.v;
//如果顶点不属于同一个集合
if(findRoot(u)!=findRoot(v)) {
sum+=e.w;
result[index++]=vertexList[u].value;
result[index++]=vertexList[v].value;
num++;
union(u, v);
}
//如果有n-1条边,就退出了
if(num==vertexList.length-1) {
break;
}
}
//打印边的信息
System.out.println("kruskal包括的边依次是:");
for(int i=0;i<result.length;i+=2) {
System.out.println(result[i]+"--"+result[i+1]);
}
System.out.println("kruskal的最小权值:"+sum);
}
看看关键代码:
//如果顶点不属于同一个集合
if(findRoot(u)!=findRoot(v)) {
sum+=e.w;
result[index++]=vertexList[u].value;
result[index++]=vertexList[v].value;
num++;
union(u, v);
}
判断顶点是否属于同一个集合,使用的是并查集的思想,findRoot方法如下:
//查找某个顶点属于哪个集合
public int findRoot(int v) {
int root; //集合的根节点
for(root=v;parent[root]>=0;root=parent[root]);
//路径压缩
while(root!=v) {
int tmp=parent[v];
parent[v]=root;
v=tmp;
}
return root;
}
初始化时,parent数组都为 -1 ,表示只有自己一个顶点,负数表示的是根节点,大小表示的是树中有多少个结点
所以,只要找到parent数组为负数的,那就是根结点,如果是同一个根结点,当然就是同一个集合了(在最小生成树中了)
这里还有一个关键的方法,就会合并森林的过程union( int u, int v),其实,这里可以随便哪一个作为根结点,但是,为了更新查找,需要使树更矮才行
//将两个不同集合的元素进行合并,使两个集合中任两个元素都连通
void union( int u, int v)
{
int r1 = findRoot(u), r2 = findRoot(v); //r1 为 u 的根结点,r2 为 v 的根结点
int tmp = parent[r1] + parent[r2]; //两个集合结点个数之和(负数)
//如果 R2 所在树结点个数 > R1 所在树结点个数(注意 parent[r1]是负数)
if( parent[r1] > parent[r2] ) //优化方案――加权法则
{
parent[r1] = r2;
parent[r2] = tmp;
}
else
{
parent[r2] = r1;
parent[r1] = tmp;
}
}
所以,这里会找结点数量多的根结点作为新的根结点,使形成的树更矮,并且更新结点数量
完整代码,请参考:https://blog.csdn.net/qiuxinfa123/article/details/83719789