最小生成树详解

895 阅读2分钟

生成树

也称为"支撑树",连通图的极小连通子图,它有图中全部n个顶点,恰好有n-1条边,如图: 生成树

最小生成树

所有生成树中,总权值最小的那颗,适用于有权的连通图(无向). 最小生成树 如果图的每一条边的权值互不相同,那么最小生成树只有一个,否则可能会有多个最小生成树. 求最小生成树一般有两个算法:

  • Prim算法
  • Kruskal算法

切分定理

切分:把图中的节点分为两个部分,称为一个切分,如图: 切分定理 上图是个切分Cut = (S,T),S = {A,B,E},T = {C,E} 横切边:如果一个边的两个顶点,分别属于切分的两部分,这个边称为横切边,如上图的边BC,BD,DE就是横切边. 切分定理:给定任意切分,横切边中权值最小的必然属于最小生成树.

Prim算法

基于切分定理,每次切分,最小的横切边就是最小生成树的一部分,切分流程如图: 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是节点数量). 如果加入该边会与生成树形成环,则不加入该边. 从第三条边开始,可能会与生成树形成环. Kruskal算法流程 代码:

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;
	}

判断加入一条边是否会形成环,我们用并查集来判断,对并查集不熟悉的可以看我的之前的文章数据结构与算法-并查集. 源码看这里