目录
0 引入
通过加权无向图结合最小生成树相关算法,可以解决最小成本问题,并找到最小成本对应的顶点和边。
1 图的最小生成树定义及相关约定
图的生成树:图的生成树是它的一个含有其所有顶点的无环连通子图。
图的最小生成树:图的权值(所有边的权重之和)最小的生成树。

约定:最小生成树只存在于连通图中,如果图不是连通的,那么分别计算每个连通子图的最小生成树,再合并到一起形成最小生成森林。

同时为了便于理解,约定所有边的权重都不相同(如果有权重相同的边,最小生成树可能就不唯一了)。
2 最小生成树原理
2.1 性质
-
用一条边连接树中的任意两个顶点都会产生环;

-
从树中删除任意一条边,将会得到两棵独立的树;

2.2 切分定理
要从一个连通图中找出该图的最小生成树,需要通过切分定理来完成。
切分:将图的所有顶点按照某些规则分为两个非空且没有交集的集合;
横切边:连接两个不同集合的顶点的边称为横切边;
例如将下图中的顶点切分为两个集合,灰色顶点属于一个集合,白色顶点属于另一个集合。

切分定理:在一副加权图中,给定任意的切分,它的横切边中的权重最小者必然属于图的最小生成树。

3 贪心思想
- 贪心算法是计算图的最小生成树的基础算法;
- 先利用切分定理找到最小生成树的一条边,然后不断重复直到找到最小生成树的所有边;
- 如果图有V个顶点,那么需要找到V-1条边,就可以表示该图的最小生成树;









计算图的最小生成树的算法有很多种,但这些算法都可以看做是贪心算法的一种特殊情况,这些算法的不同之处在于保存切分和判定权重最小的横切边的方式。
下面介绍Prim算法和kruskal算法。
4 Prim算法
4.1 算法步骤
- 将最小生成树中的顶点作为一个集合,非最小生成树中的顶点作为另外一个集合。1 索引最小优先队列存储所有的横切边;2 edgeTo数组存储当前顶点和最小生成树之间的最短边;3 distTo数组表示当前顶点和最小生成树之间的最短边的权重,初始值为无穷大;4 marked数组标识当前顶点是否在最小生成树中,如果在,值为true,如果不在,值为false;
- 选取起点0,令distTo[0] = 0.0,并将该点加入到索引最小优先队列中pq.insert(0, 0.0);
- 调用索引最小优先队列的delMin(),将当下最小横切边对应的顶点加入最小生成树的集合中(标记该顶点:marked数组中该顶点值设为true)。同时遍历该顶点的邻接表,更新edgeTo和distTo,并新增或更新横切边(更新索引最小优先队列);
- 重复3,直到索引最小优先队列为空,最终edgeTo数组里存储的就是最小生成树的边;
例
- 先默认0是最小生成树中的唯一顶点,其他顶点都不在最小生成树中,此时横切边就是顶点0的邻接表中0-2,0-4,0-6,0-7这四条边,于是索引优先队列的2、4、6、7索引处分别存储这些边的权重值;

- 从这四条横切边中找出权重最小的边,然后把对应的顶点加进来:由于0-7这条横切边的权重最小,因此把顶点7加进来;
- 于是顶点7的邻接表中的边也成为了横切边,而0-7已经不是横切边了,让它失效:已经调用了最小索引优先队列的delMin()方法;
- 2和4各有两条横切边指向最小生成树,各保留一条:4-7权重小于0-4权重,保留4-7,调用索引优先队列的changeItem(4,0.37);0-2权重小于2-7权重,保留0-2,不需额外操作;

- 不断重复上面的动作,就可以把所有的顶点添加到最小生成树中;
4.2 API设计
| 类名 | PrimMST |
|---|---|
| 构造方法 | PrimMST(EdgeWeightedGraph G):根据一个加权无向图,创建最小生成树计算对象 |
| 成员方法 | 1. private void visit(EdgeWeightedGraph G, int v):将顶点v添加到最小生成树中,并且更新数据; 2. public Deque edges():获取最小生成树的所有边; |
| 成员变量 | 1. private Edge[] edgeTo:索引代表顶点,值表示当前顶点和最小生成树之间的最短边; 2. private double[] distTo:索引代表顶点,值表示当前顶点和最小生成树之间的最短边的权重; 3. private boolean[] marked:索引代表顶点,如果当前顶点已经在树中,则值为true,否则为false; 4. private indexMinPriorityQueue pq:存放树中顶点与非树中顶点之间的有效横切边; |
4.3 Java代码演示
import java.util.Deque;
import java.util.LinkedList;
public class PrimMST {
//Edge数组表示当前顶点和最小生成树之间的最短边
private Edge[] edgeTo;
//distTo数组表示当前顶点和最小生成树之间的最短边的权重,初始值为无穷大
private double[] distTo;
//marked数组标识当前顶点是否在最小生成树中,如果在,值为true,如果不在,值为false
private boolean[] marked;
//存放树中顶点与非树中顶点之间的有效横切边
private IndexMinPriorityQueue<Double> pq;
//根据一副加权无向图,创建最小生成树计算对象
public PrimMST(EdgeWeightedGraph G) {
this.edgeTo = new Edge[G.V()];
this.distTo = new double[G.V()];
for (int i = 0; i < distTo.length; i++) {
distTo[i] = Double.POSITIVE_INFINITY;
}
this.marked = new boolean[G.V()];
pq = new IndexMinPriorityQueue<>(G.V());
//默认让顶点0进入树中,但0顶点目前没有与树中其他的顶点相连接,因此初始化distTo[0]=0.0
distTo[0] = 0.0;
//使用顶点0和权重0初始化pq
pq.insert(0,0.0);
//遍历有效边队列
while (!pq.isEmpty()){
//找到权重最小的横切边对应的顶点,加入到最小生成树中
visit(G,pq.delMin());
}
}
//将顶点v添加到最小生成树中,并且更新数据
private void visit(EdgeWeightedGraph G, int v) {
//把顶点v添加到最小生成树中
marked[v] = true;
//更新数据
for (Edge e : G.adj(v)) {
//获取e边的另外一个顶点(当前顶点是v)
int w = e.other(v);
//判断另外一个顶点是不是已经在树中,如果在树中,则不做任何处理,如果不在树中,更新数据
if (marked[w]){
continue;
}
//如果v-w边e的权重比目前distTo[w]权重小,则需要修正数据
if (e.weight() < distTo[w]){
//把顶点w距离最小生成树的边修改为e
edgeTo[w] = e;
//把顶点w距离最小生成树的边的权重修改为e.weight()
distTo[w] = e.weight();
//如果pq中存储的有效横切边已经包含了w顶点,则需要修正最小索引优先队列w索引关联的权重值
if (pq.contains(w)){
pq.changeItem(w, e.weight());
}else{//如果pq中存储的有效横切边不包含w顶点,则需要向最小索引优先队列中添加v-w和其权重值
pq.insert(w,e.weight());
}
}
}
}
//获取最小生成树的所有边
public Deque<Edge> edges() {
//创建队列对象
Deque<Edge> allEdges = new LinkedList<>();
//遍历edgeTo数组,拿到每一条边,如果不为null,则添加到队列中
for (int i = 0; i < edgeTo.length; i++) {
if (edgeTo[i]!=null){
allEdges.offer(edgeTo[i]);
}
}
return allEdges;
}
}
public class Test {
public static void main(String[] args) {
EdgeWeightedGraph g = new EdgeWeightedGraph(8);
Edge e1 = new Edge(4,5,0.35); Edge e2 = new Edge(4,7,0.37);
Edge e3 = new Edge(5,7,0.28); Edge e4 = new Edge(0,7,0.16);
Edge e5 = new Edge(1,5,0.32); Edge e6 = new Edge(0,4,0.38);
Edge e7 = new Edge(2,3,0.17); Edge e8 = new Edge(1,7,0.19);
Edge e9 = new Edge(0,2,0.26); Edge e10 = new Edge(1,2,0.36);
Edge e11 = new Edge(1,3,0.29); Edge e12 = new Edge(2,7,0.34);
Edge e13 = new Edge(6,2,0.40); Edge e14 = new Edge(3,6,0.52);
Edge e15 = new Edge(6,0,0.58); Edge e16 = new Edge(6,4,0.93);
g.addEdge(e1);g.addEdge(e2);g.addEdge(e3);g.addEdge(e4);
g.addEdge(e5);g.addEdge(e6);g.addEdge(e7);g.addEdge(e8);
g.addEdge(e9);g.addEdge(e10);g.addEdge(e11);g.addEdge(e12);
g.addEdge(e13);g.addEdge(e14);g.addEdge(e15);g.addEdge(e16);
//构建PrimMST对象
PrimMST mst = new PrimMST(g);
//获取最小生成树的边
Deque<Edge> edges = mst.edges();
//打印输出
for (Edge edge : edges) {
System.out.println(edge.either() + "-" + edge.other(edge.either()) + ":" + edge.weight());
}
}
}
1-7:0.19
0-2:0.26
2-3:0.17
4-5:0.35
5-7:0.28
6-2:0.4
0-7:0.16
注:
- 需要用到的类Edge、EdgeWeightedGraph,可在加权无向图的定义及实现(Java)_sc179的博客-CSDN博客中找到;
- 类IndexMinPriority可在索引最小优先队列的实现_sc179的博客-CSDN博客中找到;
5 Kruskal算法
Kruskal算法是另外一种计算加权无向图的最小生成树的算法。
5.1 算法步骤
- 使用最小优先队列PriorityQueue< Edge > pq = new PriorityQueue<>();存储所有边,每次使用pq.delMin()取出权重最小的边,并得到该边关联的两个顶点v和w;
- 通过并查集UF_Tree_Weighted uf = new UF_Tree_Weighted(G.V()),调用uf.connect(v,w)判断v和w是否已经连通,如果连通,则证明这两个顶点在同一棵树中,那么就不能把这条边添加到最小生成树中,因为在最小生成树的任意两个顶点上添加一条边,都会形成环,而最小生成树不能有环的存在;如果不连通,则通过uf.connect(v,w)把顶点v所在的树和顶点w所在的树合并成一棵树,并把这条边加入到Deque< Edge > mst队列中;
- 直到mst队列中含有V-1条边,最终mst中存储的就是最小生成树的所有边;
5.1 API设计
| 类名 | KruskalMST |
|---|---|
| 构造方法 | KruskalMST(EdgeWeightedGraph G):根据一个加权无向图,创建最小生成树计算对象 |
| 成员方法 | 1. public Deque edges():获取最小生成树的所有边; |
| 成员变量 | 1. private Deque mst:保存最小生成树的所有边; 2. private UF_Tree_Weighted uf:索引代表顶点,使用uf.connect(v,w)可以判断顶点v和顶点w是否在同一棵树中,使用uf.union(v,w)可以把顶点v所在的树和顶点w所在的树合并; 3. private MinPriorityQueue pq:存储图中所有的边,使用最小优先队列,对边按照权重进行排序; |






5.2 Java代码演示
import java.util.Deque;
import java.util.LinkedList;
import java.util.PriorityQueue;
public class KruskalMST {
//使用队列保存最小生成树的所有边
private Deque<Edge> mst;
//使用并查集,索引代表顶点,uf.connect(v,w)可以判断顶点v和顶点w是否在同一颗树中,使用uf.union(v,w)可以把顶点v所在的树和顶点w所在的树合并
private UF_Tree_Weighted uf;
//存储图中所有的边,使用最小优先队列,对边按照权重排序
private PriorityQueue<Edge> pq;
//根据一副加权无向图,创建最小生成树计算对象
public KruskalMST(EdgeWeightedGraph G) {
//初始化mst
this.mst = new LinkedList<>();
//初始化uf
this.uf = new UF_Tree_Weighted(G.V());
//初始化pq
this.pq = new PriorityQueue<>();
//把图中所有的边存储到pq中
for (Edge e : G.edges()) {
pq.offer(e);
}
//遍历pq队列,拿到最小权重的边,进行处理
while(!pq.isEmpty() && mst.size() < G.V()-1){
//找到权重最小的边
Edge e = pq.poll();
//找到该边的两个顶点
int v = e.either();
int w = e.other(v);
//判断这两个顶点是否已经在同一颗树中,如果在同一颗树中,则不对该边做处理,如果不在一棵树中,则让这两个顶点属于的两棵树合并成一棵树
if (uf.connected(v,w)){
continue;
}
uf.union(v,w);
//让边e进入到mst队列中
mst.offer(e);
}
}
//获取最小生成树的所有边
public Deque<Edge> edges() {
return mst;
}
}
import java.util.Deque;
public class Test {
public static void main(String[] args) {
EdgeWeightedGraph g = new EdgeWeightedGraph(8);
Edge e1 = new Edge(4,5,0.35); Edge e2 = new Edge(4,7,0.37);
Edge e3 = new Edge(5,7,0.28); Edge e4 = new Edge(0,7,0.16);
Edge e5 = new Edge(1,5,0.32); Edge e6 = new Edge(0,4,0.38);
Edge e7 = new Edge(2,3,0.17); Edge e8 = new Edge(1,7,0.19);
Edge e9 = new Edge(0,2,0.26); Edge e10 = new Edge(1,2,0.36);
Edge e11 = new Edge(1,3,0.29); Edge e12 = new Edge(2,7,0.34);
Edge e13 = new Edge(6,2,0.40); Edge e14 = new Edge(3,6,0.52);
Edge e15 = new Edge(6,0,0.58); Edge e16 = new Edge(6,4,0.93);
g.addEdge(e1);g.addEdge(e2);g.addEdge(e3);g.addEdge(e4);
g.addEdge(e5);g.addEdge(e6);g.addEdge(e7);g.addEdge(e8);
g.addEdge(e9);g.addEdge(e10);g.addEdge(e11);g.addEdge(e12);
g.addEdge(e13);g.addEdge(e14);g.addEdge(e15);g.addEdge(e16);
//构建PrimMST对象
KruskalMST mst = new KruskalMST(g);
//获取最小生成树的边
Deque<Edge> edges = mst.edges();
//打印输出
for (Edge edge : edges) {
System.out.println(edge.either() + "-" + edge.other(edge.either()) + ":" + edge.weight());
}
}
}
0-7:0.16
2-3:0.17
1-7:0.19
0-2:0.26
5-7:0.28
4-5:0.35
6-2:0.4
注:
- 需要用到的类Edge、EdgeWeightedGraph,可在加权无向图的定义及实现(Java)_sc179的博客-CSDN博客中找到;
- 类UF_Tree_Weighted可在并查集详解:UF——UF_Tree——UF_Tree_Weighted逐步优化_sc179的博客-CSDN博客中找到;
本文详细介绍了Prim算法和Kruskal算法,用于在加权无向图中找到最小生成树,包括算法原理、步骤、API设计与Java实现。通过贪心策略对比两种求解最小生成树的方法。
&spm=1001.2101.3001.5002&articleId=110423263&d=1&t=3&u=b96cdd79f7914daebdef5bfbc62bb3f3)

被折叠的 条评论
为什么被折叠?



