最小生成树
要求无向图(可以有重边)
Kruskal 算法
(加边法 -> 并查集法):
- 将图中所有边按照从小到大顺序进行排序,放入最小堆中
- 遍历每条边,对该边上两节点进行检查:如果处于不同点集,则合并,并将该边加入结果集;否则跳过该边。
Prim 算法
(加点法):
数据结构:
已访问点集:HashSet<Node>
待添加边集:PriorityQueue<Node>
结果边集:ArrayList<Edge>
算法:
- 随机往已访问点集中加入图上的一个点,并将它关联的所有边加入待添加边集(用最小堆保存)
- 从待添加边集中取出最小的边,即出队,检查该边指向的节点是否在已访问点集中:若不存在,则将该边加入结果边集,同时将指向的节点加入已访问点集,并将该点连接的所有边添加进待添加边集
- 重复第 2 步操作,直至待添加边集为空
思考:这两个算法都是贪心算法。每个算法都是尝试将最小边纳入结果集中。虽然 Kruskal 叫做加边法,但我觉得叫并查集法更为合适,因为每纳入一条边时,都会考虑边连接的两点是否在同一集合中。若处于同一集合则将集合合并。同样,Prim 算法虽然叫加点法,但它的本质仍然是加边,通过贪心加最小的边,只不过它每次只把一个邻接点而不是邻接点所处的集合进行合并。由此,两算法的区别,主要是图中顶点使用数据结构上的区别。Kruskal 用并查集保存,而 Prim 用 HashSet 保存。
注意:虽然 MST(Minimum Spanning Tree) 算法要求输入图为无向图,但由于用户误把有向图输入给算法,则该两种算法产生结果将不同:若在有向图上运行 Kruskal 算法,则仍能得到对应无向图的正确结果,因为它没有边指向的顶点这个概念;而让 Prim 算法在有向图上运行则不能得到对应无向图的正确结果。这一点在图的构建时应特别注意。
public List<Edge> kruskal() {
Queue<Edge> priorityQueue = new PriorityQueue<>(((o1, o2) -> o1.weight - o2.weight));
priorityQueue.addAll(this.edges);
UnionFind<Node> unionFind = new UnionFind<>(new ArrayList<>(this.nodes.values())); // 并查集保存顶点
List<Edge> res = new ArrayList<>();
while (!priorityQueue.isEmpty()) {
Edge edge = priorityQueue.poll();
Node from = edge.from;
Node to = edge.to;
boolean isTheSame = unionFind.find(from, to);
if (!isTheSame) {
unionFind.union(from, to);
res.add(edge);
}
}
return res;
}
public List<Edge> prim() {
Set<Node> knownNodesSet = new HashSet<>(); // HashSet 保存顶点
PriorityQueue<Edge> edgePriorityQueue = new PriorityQueue<>(((o1, o2) -> o1.weight - o2.weight));
List<Edge> res = new ArrayList<>();
// init: pick one node to knownNodesSet
for (Node node : this.nodes.values()) {
knownNodesSet.add(node);
edgePriorityQueue.addAll(node.edges);
while (!edgePriorityQueue.isEmpty()) {
Edge edge = edgePriorityQueue.poll();
// try to add toNode of this edge to set
Node toNode = edge.to;
if (!knownNodesSet.contains(toNode)) {
knownNodesSet.add(toNode);
edgePriorityQueue.addAll(toNode.edges);
res.add(edge);
}
}
break;
}
return res;
}