如何理解Kruskal算法(Java版)

Kruskal算法用于解决最小生成树问题。它的思想简单来说就是,把每一条边的权值排序后,从小到大依次选取不构成回路的边,当我们选取了n-1条不构成回路的边时,构成的图即为最小生成树。(n为顶点数)

他的算法思想其实很简单,也很容易理解,但写出一个具体代码却远比上面这一句话要更费精力。首先排序,这比较简单,任何一种排序都行。但后面那一句,我们要如何知道自己选取的边没有构成回路呢?

对于每条边,我们分别获取其两个顶点的终点并判断它们是否相同,如果不同则将它们加入最小生成树的边集合中。

边的两个顶点的终点是否相同 可以转变成讨论 两个顶点是否属于同一个连通分量

连通分量:无向图中的极大连通子图。

首先这是我的kruskal实现的代码:

public void kruskal() {
int index = 0;
int[] ends = new int[edgeNum];// 存储顶点的终点
EData[] rets = new EData[edgeNum];// 存储最小生成树的边
EData[] edges = getEdges();// 获取图的所有边,并用一维数组表示,便于堆排序
heapSort(edges);// 堆排序

// 排序后,遍历排好序的边数组,对于每条边,我们分别获取其两个顶点的终点,
// 并判断它们是否相同,如果不同则将它们加入最小生成树的边集合中。
for (int i = 0; i < edgeNum; i++) {
int p1 = getPosition(edges[i].start);
int p2 = getPosition(edges[i].end);

int m = getEnd(ends, p1);
int n = getEnd(ends, p2);

if (m != n) {
ends[m] = n;
rets[index++] = edges[i];
}
}
for (int i = 0; i < index; i++) {
System.out.println(rets[i]);
}
}

排序采用的是大顶堆的堆排序,其他排序都行,只要最终一维数组是从小到大就行。

我的一维数组是一个EData类型的,所以再提供以下EData类的EData的构造方法:   

public EData(char start, char end, int weight) {
this.start = start;
this.end = end;
this.weight = weight;
}

现在着重对排序之后的选取不构成回路的边的算法做出具体解释。

假设我们的数据为:

        顶点:A, B, C, D, E

        边及其权值: AD: 1    AB: 2    BC: 3    AC: 3    BD: 4    CD: 5

我们首先从第一条边AD开始判断。

我们获取其起点和终点在顶点数组中的位置索引,即p1=0(A)和p2=3(D)。然后,我们获取p1和p2在ends数组中的终点,即m=0和n=3。因为m不等于n,所以我们将m的终点设为n,表示A和D属于同一个连通分量,并将边AD加入最小生成树的边集合中。

现在图变成了下面的子图形式,但要注意左边的图是我们判断连通分量和终点的,右边的图才是我们找出的放进最小生成树的边。

接着,我们继续考虑下一条边AB:2。

我们获取其起点和终点在顶点数组中的位置索引,即p1=0(A)和p2=1(B)。然后,我们获取p1和p2在ends数组中的终点,即m=3和n=3。因为m等于n,说明A和B已经属于同一个连通分量,所以我们不将边AB加入最小生成树的边集合中。

现在图变成了下面的子图形式,但要注意左边的图是我们判断连通分量和终点的,右边的图才是我们找出的放进最小生成树的边。

第三条边也是同样的方式,结果如下:

 

前三条边其实都非常顺利的没有构成回路被放进的我们最小生成树的集合中,那么第四条边其实就需要我们进行判断了。

当我们起点为A时,会发现我们的m在getEnd中不断循环直到找到我们想要的终点ends[1]=2, A依次经过了D、B、C,也就是如我们之前所画的左边的图一样的顺序。而这恰恰说明了我们判断终点或者说判断连通分量的过程。


经过刚才的分析,我们其实已经可以理解:

        终点和最小生成树的边实际上没有直接的关系。在这段代码中,我们使用ends数组来记录每个顶点所在的连通分量的代表元素(也可以理解为根节点)。初始时,每个顶点自成一个连通分量,ends[i]的值为0。

         在Kruskal算法中,我们需要判断每条边的两个顶点是否已经属于同一个连通分量,以避免形成环路。如果两个顶点的终点相同,说明它们已经在同一个连通分量中,连接它们会形成环路,因此我们不将这条边加入最小生成树的边集合中。

         因此,这段代码中的ends数组实际上是用来帮助判断是否形成环路的,并不直接与最小生成树的边集合有关。当判断两个顶点的终点不同时,我们将它们的终点设为相同,表示它们属于同一个连通分量。