生成树
也称为"支撑树",连通图的极小连通子图,它有图中全部n个顶点,恰好有n-1条边,如图:
最小生成树
所有生成树中,总权值最小的那颗,适用于有权的连通图(无向).
如果图的每一条边的权值互不相同,那么最小生成树只有一个,否则可能会有多个最小生成树.
求最小生成树一般有两个算法:
- Prim算法
- Kruskal算法
切分定理
切分:把图中的节点分为两个部分,称为一个切分,如图:
上图是个切分Cut = (S,T),S = {A,B,E},T = {C,E}
横切边:如果一个边的两个顶点,分别属于切分的两部分,这个边称为横切边,如上图的边BC,BD,DE就是横切边.
切分定理:给定任意切分,横切边中权值最小的必然属于最小生成树.
Prim算法
基于切分定理,每次切分,最小的横切边就是最小生成树的一部分,切分流程如图:
代码:
private Set<EdgeInfo<V, E>> prim() {
Iterator<Vertex<V, E>> iterator = vertices.values().iterator();
//图为空,直接返回
if (!iterator.hasNext()) {
return null;
}
//图不为空,随机返回一个节点
Vertex<V, E> vertex = iterator.next();
//返回的最小生成树边
Set<EdgeInfo<V, E>> edgeInfos = new HashSet<>();
//已经加入最小生成树的节点集合
Set<Vertex<V, E>> addedVertices = new HashSet<>();
//加入第一个节点
addedVertices.add(vertex);
//由节点的所有出度,生成一个最小堆,获取最小权值的边
MinHeap<Edge<V, E>> minHeap = new MinHeap<>(vertex.outEdges, edgeComparator);
//获取图总节点数
int vertices = verticesSize();
while (!minHeap.isEmpty() && addedVertices.size() < vertices) {
//取出权值最小的边
Edge<V, E> edge = minHeap.remove();
//判断是否已经添加到最小生成树
if (addedVertices.contains(edge.to)) {
continue;
}
//加入最小生成树种
edgeInfos.add(edge.info());
addedVertices.add(edge.to);
//把该节点的终点节点出度加入最小堆中
minHeap.addAll(edge.to.outEdges);
}
return edgeInfos;
}
Kruskal算法
按照边的权重顺序从小到大将边加入生成树中,直到生成树中含有n-1条边为止(n是节点数量).
如果加入该边会与生成树形成环,则不加入该边.
从第三条边开始,可能会与生成树形成环.
代码:
private Set<EdgeInfo<V, E>> kruskal() {
int edgeSize = vertices.size() - 1;
if (edgeSize == -1) {
return null;
}
//返回的最小生成树边
Set<EdgeInfo<V, E>> edgeInfos = new HashSet<>();
//最小堆,用于获取权重最小的边
MinHeap<Edge<V, E>> heap = new MinHeap<>(edges, edgeComparator);
//并查集用于判断是否会形成环
UnionFind<Vertex<V, E>> uf = new UnionFind<>();
//并查集初始化
vertices.forEach((V v, Vertex<V, E> vertex) -> {
uf.makeSet(vertex);
});
while (!heap.isEmpty() && edgeInfos.size() < edgeSize) {
//获取权重最小的边
Edge<V, E> edge = heap.remove();
//会形成环
if (uf.isSame(edge.from, edge.to)) {
continue;
}
edgeInfos.add(edge.info());
//加入同一个集合中
uf.union(edge.from, edge.to);
}
return edgeInfos;
}
判断加入一条边是否会形成环,我们用并查集来判断,对并查集不熟悉的可以看我的之前的文章数据结构与算法-并查集. 源码看这里