之前我用克鲁斯卡尔(Kruskal)算法求出了图的最小生成树(还记得是如何求出最小生成树的吗?就是把图的所有边拿出来,按权重从小到大排列,依次拿边尝试放到图中。如果边放进去不形成环,则放进去。如果边放进去会形成环,则这条边舍弃,然后继续尝试下一条边,直到图中所有点连通,共需要图顶点数-1条边),因为当时还不知道不相交集类这么个数据结构与算法,所以在判断一条边是接受还是舍弃的时候,用的邻接矩阵的的广度优先遍历算法(BFS)来判断两个顶点是否是连通的(点我查看)。现在有了不相交集类这么个利器,实现克鲁斯卡尔(Kruskal)算法就更加方便了。
我们在往图中一条一条添加边的过程中,刚开始可以看做所有的顶点都是不等价的。然后加入一条边,也就是将该边的两个顶点合并,然后这两个顶点就等价了,也就是连通了。每次加入边之前需要判断该边的两个顶点是否等价,也就是是否连通。如果等价,则代表两个顶点已经连通了,该边需要舍弃,否则就有环了。如果不等价,则代表两个顶点不连通,则接受该边,然后再将该边的两个顶点求并。直到接受的边等于顶点数-1,或者所有的边都已经经过以上算法的计算,算法结束,所有组成图的最小生成树的边已全部得到。
我们再梳理一下不相交集类和图的对应关系:
- 图的所有顶点,就对应不相交集类的所有元素
- 如果图的两个顶点连通,就对应不相交集类的两个元素的等价
- 如果图的两个顶点不连通,也就对应不相交集类的两个元素不等价
- 在求最小生成树的过程中,两个顶点判定为不连通(不等价),然后加入这条边,就对应不相交集类的两个元素的求并操作
- 最后用克鲁斯卡尔(Kruskal)算法求出了最小生成树,图的意义就是所有顶点均连通了,不相交集类的意义就是所有元素都等价了
- 所以,图的顶点的连通关系,就一一对应不相交集类的等价关系。合并操作就是加边,加边就是对应合并。当然,如果已经生成最小生成树,还继续加边,则对应合并操作(虽然此后的加边对应的合并,两个顶点的根顶点相同,不进行合并操作)依然有效。也就是说,无论加多少边,不相交集类依然可以通过等价关系,判定两个顶点是否连通。甚至可以通过之前对不相交集类的统计方法(点 这里 查看博文的arrangeDisjointSet()方法),求出哪些顶点是连通的,最后得到整个图的所有顶点的连通关系
我们还是拿那个示例图来举例子,我们要求下面这张图的最小生成树:
下面的这张图,就是上面示例图的最小生成树:
根据示例图的最小生成树,我们可以得到,V1-V7这七个顶点已经连通,但是连通他们的边的权重和是最小的。你找不到更小的连通图,连通边的权重和比上面的图还要小了。连通他们的最小生成树的组成边为:
- V1 - V4,权重为1
- V6 - V7,权重为1
- V1 - V2,权重为2
- V3 - V4,权重为2
- V4 - V7,权重为4
- V5 - V7,权重为6
从上面的答案,我们也可以看出,最小生成树组成的边的数量,就是顶点数-1,也验证了之前的定理。如何用不相交集类作为辅助工具类,用克鲁斯卡尔(Kruskal)算法求出上面示例图的最小生成树呢?实现的代码如下,算法的思想和精髓都在代码和其间的详细注释中:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @author LiYang
* @ClassName KruskalByDisjointSet
* @Description 用不相交集类(DisjointSet),实现克鲁斯卡尔(Kruskal)算法
* @date 2019/11/18 14:20
*/
public class KruskalByDisjointSet {
/**
* 图的边类
* 刚开始需要将所有的边按权重,从小到大排列,
* 然后依次尝试加入到图中,直到生成最小生成树
*/
static class Edge implements Comparable<Edge> {
//顶点1的下标(到时候对应String[] vertexName)
private int vertex1;
//顶点2的下标(到时候对应String[] vertexName)
private int vertex2;
//边的权重
private int weight;
/**
* 全参数的构造方法
* @param vertex1 顶点1的下标
* @param vertex2 顶点2的下标
* @param weight 边的权重
*/
public Edge(int vertex1, int vertex2, int weight) {
this.vertex1 = vertex1;
this.vertex2 = vertex2;
this.weight = weight;
}
public int getVertex1() {
return vertex1;
}
public int getVertex2() {
return vertex2;
}
public int getWeight() {
return weight;
}
/**
* 重写compareTo方法,按边的权重从小到大排序
* @param o
* @return
*/
@Override
public int compareTo(Edge o) {
return this.weight - o.weight;
}
}
/**
* 不相交集工具类,按高度来合并
* 实现代码根之前的不相交集类一样
* 这里作为内部类,辅助实现Kruskal算法
*/
static class DisjointSetUnionByHeight {
//不相交集类(并查集)的顶点数组
private int[] vertexes;
/**
* 不相交集类(并查集)的构造方法,入参顶点个数
* @param vertexNum 顶点个数
*/
public DisjointSetUnionByHeight(int vertexNum) {
if (vertexNum <= 0) {
throw new IllegalArgumentException("顶点个数要大于零");
}
//实例化不相交集类(并查集)的顶点数组
this.vertexes = new int[vertexNum];
//初始化顶点树的高度都为-1(如果是根,值就是负数,连通顶点组成的树
//的高度是多少,则根元素就是负几)
for (int i = 0; i < vertexes.length; i++) {
this.vertexes[i] = -1;
}
}
/**
* 查询不相交集类(并查集)的顶点个数
* @return 顶点个数
*/
public int size() {
return vertexes.length;
}
/**
* 查询不相交集类(并查集)的某个顶点的根元素
* 输入的是下标查,如果两个元素的根元素相同,
* 则这两个元素就是等价的。实际中还会有一个
* 与vertexes等长的数组,装的是顶点的名字,
* vertexes只是相当于代号,记录连通关系,
* 二者通过下标,来映射真实顶点
* @param vertexIndex 待查询的顶点下标
* @return 该顶点的根顶点
*/
public int find(int vertexIndex) {
//如果记录小于0,那就是根顶点
if (vertexes[vertexIndex] < 0) {
//返回根顶点
return vertexIndex;
//如果记录不小于0,那还不是根,
//是等价森林中的上一个节点
} else {
//递归向上继续寻找根
return find(vertexes[vertexIndex]);
}
}
/**
* 将不相交集类(并查集)的两个顶点进行连通操作
* 注意,两个顶点连通,代表这两个顶点所在的子树
* 全部变成一个图中的大的子树。如果这
* 两个顶点本来就连通,则不进行连通操作,舍弃该边。
* 注意,这里同样是入参下标,下标映射真实顶点
* 此实现类,根据树的高度来决定谁合并到谁上面,
* 矮的树的根节点,会作为大的树的根节点的子节点
* @param vertexIndex1 顶点下标1
* @param vertexIndex2 顶点下标2
*/
public void union(int vertexIndex1, int vertexIndex2) {
int root1 = find(vertexIndex1);
int root2 = find(vertexIndex2);
//如果两个顶点本就连通
if (root1 == root2) {
//不作处理
return;
}
//比高度:如果root1比root2的树要高
if (vertexes[root1] < vertexes[root2]) {
//将较矮的root2合并到较高的root1上
vertexes[root2] = root1;
//比高度:如果root2比root1的树要高
} else if (vertexes[root2] < vertexes[root1]) {
//将较矮的root1合并到较高的root2上
vertexes[root1] = root2;
//比高度:如果root1和root2一样高
} else {
//将root1合并到root2上
vertexes[root1] = root2;
//root2的高度增加1
root2 --;
}
}
}
/**
* 用不相交集类作为辅助,用克鲁斯卡尔(Kruskal)算法求出示例图的最小生成树
* @param edgeList 图的所有边的List
* @param vertexNum 图的顶点的个数
* @param vertexName 图的顶点的
* @return 组成示例图的最小生成树的所有边
*/
public static List<Edge> runKruskalByDisjointSet(List<Edge> edgeList, int vertexNum, String[] vertexName) {
//最后组成示例图的最小生成树的边的集合
List<Edge> findKruskalEdges = new ArrayList<>();
//将所有的边,按照权重从小到大排序
Collections.sort(edgeList);
//创建不相交集类的实例,本例我们用按高度求并的不相交集类,并初始化每个顶点都不连通
DisjointSetUnionByHeight disjointSet = new DisjointSetUnionByHeight(vertexNum);
//按权重从小到大,遍历每一条边
for (Edge edge : edgeList) {
//找到当前边的第一个顶点的根顶点
int rootVertex1 = disjointSet.find(edge.getVertex1());
//找到当前边的第二个顶点的根顶点
int rootVertex2 = disjointSet.find(edge.getVertex2());
//如果当前边的两个顶点,不连通(不在等价集合里就不连通)
if (rootVertex1 != rootVertex2) {
//接受当前边,并作为最小生成树中的一条边
findKruskalEdges.add(edge);
//合并当前边的两个顶点(也就是连通这两个顶点)
disjointSet.union(edge.getVertex1(), edge.getVertex2());
//输出算法的操作过程
System.out.println(String.format("边: (%s -- %s), 权重: %d : 接受",
vertexName[edge.getVertex1()], vertexName[edge.getVertex2()],edge.getWeight()));
//如果当前边的两个顶点是连通的
} else {
//舍弃当前边,也就是最小生成树不该包含当前边,并输出算法的操作过程
System.out.println(String.format("边: (%s -- %s), 权重: %d : 舍弃",
vertexName[edge.getVertex1()], vertexName[edge.getVertex2()],edge.getWeight()));
}
//如果接收的边的数量已经达到顶点数-1了
if (findKruskalEdges.size() == vertexNum - 1) {
//表示所有顶点均已连通,最小生成树已求得
break;
}
}
//结束用不相交集类作为辅助的克鲁斯卡尔(Kruskal)算法
//并返回最终组成最小生成树的所有边
return findKruskalEdges;
}
/**
* 验证用不相交集类作为辅助的克鲁斯卡尔(Kruskal)算法
* 求出示例图的最终组成最小生成树的所有边
* @param args
*/
public static void main(String[] args) {
//将示例图中的所有边,初始化并放入edgeList
List<Edge> edgeList = new ArrayList<>();
edgeList.add(new Edge(0, 3, 1));
edgeList.add(new Edge(5, 6, 1));
edgeList.add(new Edge(0, 1, 2));
edgeList.add(new Edge(2, 3, 2));
edgeList.add(new Edge(1, 3, 3));
edgeList.add(new Edge(0, 2, 4));
edgeList.add(new Edge(3, 6, 4));
edgeList.add(new Edge(2, 5, 5));
edgeList.add(new Edge(4, 6, 6));
//示例图的顶点数为7
int vertexNum = 7;
//示例图的顶点下标对应的顶点名称
String[] vertexName = {"V1", "V2", "V3", "V4", "V5", "V6", "V7"};
//运行不相交集类作为辅助的克鲁斯卡尔(Kruskal)算法,
//得到示例图的最小生成树的所有组成边
List<Edge> acceptedEdgeList = runKruskalByDisjointSet(edgeList, vertexNum, vertexName);
//空行
System.out.println();
//查看组成示例图的最小生成树的所有边
for (Edge edge : acceptedEdgeList) {
System.out.println(String.format("示例图的最小生成树的组成边:(%s -- %s)",
vertexName[edge.getVertex1()], vertexName[edge.getVertex2()]));
}
}
}
其中,图的边的类,以及不相交集类辅助类,都是以内部类实现的,运行 KruskalByDisjointSet 类的main方法,初始化示例图的边的参数和其他参数,运行 runKruskalByDisjointSet() 的算法驱动方法,输出结果如下(控制台输出,包括求示例图的最小生成树的算法运行过程中边的接受和舍弃的判断,以及最后求得的示例图的最小生成树的所有组成边的信息展示):
边: (V1 -- V4), 权重: 1 : 接受
边: (V6 -- V7), 权重: 1 : 接受
边: (V1 -- V2), 权重: 2 : 接受
边: (V3 -- V4), 权重: 2 : 接受
边: (V2 -- V4), 权重: 3 : 舍弃
边: (V1 -- V3), 权重: 4 : 舍弃
边: (V4 -- V7), 权重: 4 : 接受
边: (V3 -- V6), 权重: 5 : 舍弃
边: (V5 -- V7), 权重: 6 : 接受
示例图的最小生成树的组成边:(V1 -- V4)
示例图的最小生成树的组成边:(V6 -- V7)
示例图的最小生成树的组成边:(V1 -- V2)
示例图的最小生成树的组成边:(V3 -- V4)
示例图的最小生成树的组成边:(V4 -- V7)
示例图的最小生成树的组成边:(V5 -- V7)
根据控制台的输出结果,可以说明:上述代码实现的算法已成功求得了示例图的最小生成树,而且用不相交集类作为辅助工具类,也很好地帮助判断了算法过程中边接受或舍弃操作的选择!