最小生成树Kruskal算法的实现原理
到底什么是最小生成树
最小生成树算法应用范围比较广,例如在网络的铺设当中难免会出现环路,需要要生成树算法来取出网络中的环,防止网络风暴的发生。那到底什么是最小生成树呢?我这里就不给严谨的定义了,这种定义网上一搜一大堆,但是往往严谨的定义都不太容易理解。下面我就给出一个更容易理解的定义。
在理解“最小生成树”之前,我们需要理解什么是“生成树”。
生成树的概念:在一个连通图中取出这个图的全部顶点和一部分边形成的一个无环图就是这个连通图的生成树。例如在下图中,任意取出图中的一条边都使得这个图中没有环路,也就形成了一棵树。
有人可能会说了,右边生成的图也不像是一棵树呀,其实没有环的图就是一颗树,只不过树中每个节点的度都是不定的。
理解完生成树的概念后,我想最小生成树的概念就很容易理解了:在一个连通图的所有生成树中,边的权重之和最小的那棵树称为最小生成树。话不多说,给朕来张图:
在所有生成树中只有这棵树的权重最小,为6。
因为这个图太简单了,我们一眼就可以看出来去除A<——>B的的这条边后就是最小生成树,那如果图变得非常复杂,就比如这样:
此时我们就需要去寻求最小生成树算法的帮助了。其实算法也就是解决特定问题的一种套路或者说规律,如果自己找不出一些问题解决的思路,就可以去学习前人总结出来的规律了。
Kruskal算法的实现思路
最小生成树算法有多种,例如:Kruskal,Prim。我们这里来对Prim算法不做解释。
第一步:按照边的权重对边进行排序
第二步:从上至下依次取出边,每次取的边如果在树中形成了环路则必须丢弃。直到最后一条顶点被连接在树中则最小生成树生成。
在取出(D,E)这条边时由于与(C,D)、(C,E)产生了回路,所以丢弃这条边。
这里我们就来到了问题的重点了,我们到底如何判断新加进来的边到底会不会形成一个环路呢?这里我们可以使用并查集的。
判断回路的产生
初始状态我们将图中每一个结点都看成一颗树,就比如这样:
此时我们取出排序边集中的第一条边(C,D),我们发现C、D两个节点来自不同的树,这就说明这条边的加入不会形成环路。此时我么需要将D树置成C的子树。就比如这样:
此时我们取出第二条边(C,A),我们发现C、A两个节点属于不同的树,所以(C,A)边的加入不会成环,我们将这条边加到最小生成树的边集当中,此时我们将需要将A树置为C的子树,就比如这样:
我们取出第三条边(C,E),发现C、E仍然属于两个不同的树,所以我们依旧将这条边加入最小生成树的边集当中。然后将E树置为C树的子树,就比如这样:
重点来了,我们取出第四条边(D,E),发现这两个节点都来自同一个树,说明如果我们将(D,E)边加入到生成树的边集中就会形成环路,所以(D,E)这条边就需要舍弃。
我们取出第五条边:(A,B),发现A、B属于不同的节点,这就代表这个边的加入不会成环,我们将这条边加入最小生成树的边集当中。这是我们发现边集中边的数量加一刚好等于节点数,这也就说明,每个节点都已将包含在最小生成树的边集当中了,也就不需要继续向下取排序边集了。
此时我们将边集中的边重构成一张图,也就是一个最小生成树了:
上面就是整个Kruskal的思路
Kruskal算法的实现
/**
* 图的表示形式有多种,例如:邻接表、邻接矩阵、边集等。我们这里使用边集来表示图
*/
public class Hello {
public static class Edge {
String start;
String end;
int distance;
@Override
public String toString() {
return start + "——>" + end;
}
public Edge(String start, String end, int distance) {
this.start = start;
this.end = end;
this.distance = distance;
if (graphNode.get(start) == null) {
graphNode.put(start, new Node(start));
nodeNum++;//图的节点数加一
}
if (graphNode.get(end) == null) {
graphNode.put(end, new Node(end));
nodeNum++;
}
}
}
public static class Node {
String content;
Node parent;
public Node(String content) {
this.content = content;
}
}
public static List<Edge> edges;
public static Set<Edge> tree = new HashSet<>();//用边集表示最小生成树
public static int nodeNum = 0;//图的节数点
public static Map<String, Node> graphNode = new HashMap<>();
public static void main(String[] args) {
//生成一个带权图(用边集表示图)
buildEdges();
//对图的边集进行排序
edges.sort(new Comparator<Edge>() {
@Override
public int compare(Edge o1, Edge o2) {
return o1.distance - o2.distance;
}
});
//产生最小生成树
for (Edge edge : edges) {
if (tree.size() + 1 == nodeNum) {//如果 边树+1等于图的节点数,就证明最小生成树已经生成了,也就不用遍历后面的带权边了
break;
}
if (isOk(edge)) {
tree.add(edge);
}
}
//打印最小生成树的边
for (Edge edge : tree) {
System.out.println(edge.toString());
}
}
/**
* 为边集里面添加元素,也就是构建一个图
*/
public static void buildEdges() {
edges = new ArrayList<>();
edges.add(new Edge("C", "D", 1));
edges.add(new Edge("C", "A", 1));
edges.add(new Edge("C", "E", 2));
edges.add(new Edge("A", "B", 3));
edges.add(new Edge("D", "E", 3));
edges.add(new Edge("B", "C", 5));
edges.add(new Edge("B", "E", 6));
edges.add(new Edge("B", "D", 7));
edges.add(new Edge("A", "D", 2));
edges.add(new Edge("A", "E", 9));
}
/**
* 获取并查集中指定元素的根节点
*
* @param node
* @return
*/
public static Node getRootNode(Node node) {
Node root = node;
while (root.parent != null) {
root = root.parent;
}
if (node != root) {
node.parent = root;
}
return root;
}
/**
* 判断这条边集是否能够加到最小生成树的边集中。
*
* @param edge
* @return true表示可以成为最小生成树的一条边
*/
public static boolean isOk(Edge edge) {
Node node1 = graphNode.get(edge.start);
Node node2 = graphNode.get(edge.end);
Node root1 = getRootNode(node1);
Node root2 = getRootNode(node2);
if (root1 != root2) {//如果边的两个点当前不属于一个集,那么这条边作为树的一条边就不会形成环路。
root2.parent=root1;//node2所在的树并成node1子树(两棵树合并)
return true;
}
return false;
}
}
运行结果: