Prim算法
我们要学习的第一种计算最小生成树的方法是 Prim算法,它的每一步都会为一棵生长中的树添加一条边。一开始这棵树只有一个顶点,然后会向它添加 V − 1 V-1 V−1 条边,每次总是将下一条连接树中的顶点与不在树中的顶点且权重最小的边加入树中。
Prim算法能够得到任意加权无向图的最小生成树。
Prim算法的延时实现
每当我们向树中添加了一条边之后,也向树中添加了一个顶点。要维护一个包含所有横切边的集合,就要将连接这个顶点和其他所有不在树中的顶点的边加入优先队列。
注意:新加入树中的顶点与其他已经在树中的顶点连接的边都失效了。
Prim算法的即时实现可以将这样的边从优先队列中删掉
延时实现将这些边先留在优先队列中,等到要删除它们的时候再检查边的有效性。
具体算法如下:
Prim算法的延时实现使用了一条优先队列 pq 来保存所有的横切边、一个由顶点索引的数组 marked[] 来标记树的顶点、一条队列 mst 来保存最小生成树的边。
这种延时实现会在优先队列中保留失效的边。
和实现 深度优先搜索DFS 和 广度优先搜索BFS 一样,实现会在构造函数中计算图的最小生成树,这样用例方法就可以用查询类方法获得最小生成树的各种属性。
/*
* 最小生成树的Prim算法 (延时版本)
*/
public class LazyPrimMST {
private boolean[] marked; //最小生成树的顶点
private Queue<Edge> mst; //最小生成树的边
private MinPQ<Edge> pq; //横切边(其中包括失效的边)
public LazyPrimMST(EdgeWeightedGraph G) {
pq = new MinPQ<Edge>();
marked = new boolean[G.V()];
mst = new Queue<Edge>();
visit(G,0); //假设G是连通的
while(!pq.isEmpty()) {
Edge e = pq.delMin(); //从pq中得到权重最小的边
int v = e.either(), w = e.other(v);
if(marked[v] && marked[w]) //跳过无效边
continue;
mst.enqueue(e); //将权重最小的边添加到树中
if(!marked[v]) visit(G,v); //将顶点(v或w)添加到树中
if(!marked[w]) visit(G,w);
}
}
//为树添加一个顶点v、将它标记为“已访问”,并将所有连接v和未被标记顶点的边加入优先队列pq
private void visit(EdgeWeightedGraph G, int v) {
marked[v] = true;
for(Edge e : G.adj(v)) {
if(!marked[e.other(v)])
pq.insert(e);
}
}
public Iterable<Edge> edges() { return mst; }
}
Prim算法的即时实现
要改进 LazyPrimMST,可以尝试从优先队列中删除失效的边,这样优先队列就只含有树顶点和非树顶点之间的横切边,但其实还可以删除更多的边。
我们只在优先队列中保存每个非树顶点w的一条边:将它与树中的顶点连接起来的权重最小的那条边。将w和树的顶点连接起来的其他权重较大的边迟早都会失效,所以没必要在优先队列中保存它们。
PrimMST类 将LazyPGrimMST中的 marked[] 和 mst 替换为两个顶点索引的数组 edgeTo[] 和 distTo[]。它们具有如下性质:
如果顶点v不在树中但至少含有一条边和树相连,那么 edgeTo[v] 是将v和树连接的最短边,distTo[] 为这条边的权重。
PrimMST会从优先队列中取出一条边v并检查它的邻接链表中的每条边 v-w。
如果w已经被标记过,那么这条边就已经失效了;
如果w不在优先队列中、或 v-w 的权重小于目前已知的最小值 edgeTo[w],代码会更新数组,将 v-w 作为将v和树连接的最佳选择。
/*
* 最小生成树的Prim算法 (即时版本)
*/
public class PrimMST {
private Edge[] edgeTo; //距离树最近的边
private double[] distTo; //distTo[w] = edgeTo[w].weight()
private boolean[] marked; //如果v在树中则为true
private IndexMinPQ<Double> pq; //有效的横切边
public PrimMST(EdgeWeightedGraph G) {
edgeTo = new Edge[G.V()];
distTo = new double[G.V()];
marked = new boolean[G.V()];
for(int v=0; v<G.V(); v++)
distTo[v] = Double.POSITIVE_INFINITY;
pq = new IndexMinPQ<Double>(G.V());
distTo[0] = 0.0;
pq.insert(0, 0.0);
while(!pq.isEmpty()) {
visit(G, pq.delMin()); //每次都取出权重最小的边
}
}
//将顶点v添加到树中,更新数据
private void visit(EdgeWeightedGraph G, int v) {
marked[v] = true;
for(Edge e : G.adj(v)) {
int w = e.other(v);
if(marked[w]) //v-w失效
continue;
if(e.weight() < distTo[w]) { //连接w和树的最佳边Edge变为e
edgeTo[w] = e;
distTo[w] = e.weight();
if(pq.contains(w)) pq.changeKey(w, distTo[w]);
else pq.insert(w, distTo[w]);
}
}
}
public Iterable<Edge> edges() {
Queue<Edge> mst = new Queue<Edge>();
for (int v = 0; v < edgeTo.length; v++) {
Edge e = edgeTo[v];
if (e != null) {
mst.enqueue(e);
}
}
return mst;
}
}