实验名称
最小生成树算法-Kruskal算法
实验目的
1.掌握并查集的合并优化和查询优化;
2.掌握Kruskal算法。
3.能够针对实际问题,能够正确选择贪心策略。
4.能够针对选择的贪心策略,证明算法的正确性。
5.能够根据贪心策略,正确编码。
6.能够正确分析算法的时间复杂度和空间复杂度
实验内容
采用Kruskal算法生成最小生成树,并采用并查集的合并优化和查询优化。
实验环境
操作系统:win 10;
编程语言:Java,JDK1.8;
开发工具:IDEA;
实验过程
算法简介
Kruskal算法是一种用来寻找最小生成树的算法,由Joseph Kruskal在1956年发表。用来解决同样问题的还有Prim算法和Boruvka算法等。三种算法都是贪婪算法的应用。和Boruvka算法不同的地方是,Kruskal算法在图中存在相同权值的边时也有效。
算法步骤
Kruskal算法又称加边算法,初始最小生成树的边数是0,每次迭代都从边的集合中选取最小代价的边,我们称他为最小代价边,加入到最小生成树的边集合中。
- 将图中所有的边按照权值大小从小到大排序。
- 把N个顶点看成独立的森林。
- 从排好序的边集合中取出当前最小的边,所选连接的顶点是v,w,如果两个顶点添加到最小生成树中后不会构成环,那就加入,反之跳过本次循环。
- 重复步骤三,直到所有顶点添加完成或者最小生成树的边的数量=顶点数-1。
代码实现
代码总体分为三部分,分别是最小生成树的生成,并查集的构建,对边权值的处理。
并查集用来快速判断两个元素加入到生成树中会不会构成环,如果两个元素加入到并查集中是属于同一个分组,代表构成环,所以直接continue;反之,将这条边加到独立的最小生成树中,边数++,并更新总权值,如果边数==顶点数-1;退出循环。权值处理主要是通过排序将边的权值按照从小到大的顺序添加到最小生成树中,排序可以选择内置的快排或者堆排。
package org.qianyan.algorithm;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class Kruskal {
/**
* 最小生成树的权值之和
*/
private int mstCost;
//获取最小生成树的总权值
public int getMstCost() {
return mstCost;
}
/**
* 最小生成树的边的列表
*/
private List<int[]> mst;
//获取最小生成树的边的集合
public List<int[]> getMst() {
return mst;
}
/**
* @param V 总结点数
* @param edges 每条边的定义:[起始点, 终点, 权值]
*/
public Kruskal(int V, int[][] edges) {
//E代表一共多少条边
int E = edges.length;
//边数小于顶点数是构不成最小生成树
if (E < V - 1) {
throw new IllegalArgumentException("参数错误");
}
mst = new ArrayList<>(E - 1);
// 体现了贪心的思想,从权值最小的边开始考虑 这里采用了排序的思想,来获取最小边
Arrays.sort(edges, Comparator.comparingInt(o -> o[2]));
//创建一个并查集
UnionFind unionFind = new UnionFind(V);
// 当前找到了多少条边
int count = 0;
for (int[] edge : edges) {
// 如果形成了环,就继续考虑下一条边
if (unionFind.isConnected(edge[0], edge[1])) {
continue;
}
//如果没有形成环,将两个顶点连接在一个集合
unionFind.union(edge[0], edge[1]);
//加上这条边的权值
this.mstCost += edge[2];
//最小生成树加上这条边
mst.add(new int[]{edge[0], edge[1], edge[2]});
//当前最小生成树的边数++
count++;
//循环结束条件,v个顶点有v-1条边构成最小生成树
if (count == V - 1) {
break;
}
}
}
/**
* 并查集
*/
private class UnionFind {
//每个并查集中的节点都有一个“大哥”
private int[] parent;
//并查集中的分组数
private int count;
//N代表并查集中节点的总数
private int N;
public UnionFind(int N) {
this.N = N;
this.count = N;
this.parent = new int[N];
for (int i = 0; i < N; i++) {
//各个节点的初始父节点都是他们自己
parent[i] = i;
}
}
/**
* 查找指定元素在哪一个分组,也就是节点跟随最终的“大哥”是谁
*
* @param x
* @return
*/
public int find(int x) {
while (x != parent[x]) {
x = parent[x];
}
return x;
}
/**
* 合并两个元素所在的分组,将两个顶点的大哥统一
*
* @param x
* @param y
*/
public void union(int x, int y) {
//找到他们各自的祖节点,
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return;
}
//合并之后分组总数--
parent[rootX] = rootY;
count--;
}
public int getCount() {
return count;
}
//判断是不是同一个分组,也就是两个元素是不是同一个大哥的小弟
public boolean isConnected(int x, int y) {
return find(x) == find(y);
}
}
public static void main(String[] args) {
int N = 7;
//edges[0] 表示起始顶点,edges[1]表示终止定点,edges[2]代表这条边的权值
int[][] edges = {{0, 1, 4},
{0, 5, 8},
{1, 2, 8},
{1, 5, 11},
{2, 3, 3},
{2, 6, 2},
{3, 4, 3},
{4, 5, 8},
{4, 6, 6},
{5, 6, 7},
};
//顶点是0-6,一共10条边
Kruskal kruskal = new Kruskal(N, edges);
//总权值
int mstCost = kruskal.getMstCost();
System.out.println("最小生成树的权值之和:" + mstCost);
List<int[]> mst = kruskal.getMst();
System.out.println("最小生成树的边的列表:");
for (int[] edge : mst) {
System.out.println("[起始顶点:" + edge[0] + "-终止顶点" + edge[1] + "]" + ",权值:" + edge[2]);
}
}
}
复杂度分析
并查集查找空间复杂度是O(n),合并和判断是不是同一个集合时间复杂度是O(1);
Kruskal算法的时间复杂度:O(E log E),这里 E 是图的边数;
空间复杂度:O(V),这里 V 是图的顶点数,并查集需要 V 长度的数组空间。