加权图是一种为每条边关联一个权值的图模型,这种图能够表示许多应用,好比在一副航空图中,边表示航线,权值就能够表示距离或费用;在一副电路图中,边表示导线,权值就能够表示导线的长度或成本。在这些情形中,最使人感兴趣的即是如何将成本最小化。最小生成树就是用于在加权无向图中解决这类问题的。最小生成树相关的算法在通讯、电子、水利、网络、交通灯行业具备普遍的应用。java
图的生成树是它的一颗含有其全部顶点的无环连通子图,一副加权无向图的最小生成树(Minimum spanning tree)是它的一颗权值(树中全部边的权值之和)最小的生成树。算法
图的一种切分是将图的全部顶点分为两个非空且不重复的集合。横切边是一条链接两个属于不一样集合顶点的边。
一般经过指定一个顶点集并隐式地认为它的补集为另外一个顶点集来指定一个切分。这样,一条横切边就是链接该集合的一个顶点和不在该集合中的另外一个顶点的一条边。数组
切分定理的内容为:在一副加权图中,给定任意的切分,它的横切边中的权重最小者必然属于图的最小生成树。
切分定理是最小生成树算法的理论依据。
要证实切分定理,须要知道树的两个重要性质:网络
切分定理是全部解决最小生成树问题算法的基础,这些算法都是一种贪心算法的特殊状况,贪心算法是一类在每一步选择中都采起在当前状态下最好或最优的选择,从而但愿致使结果是最好或最优的算法。解决最小生成树问题时,会使用切分定理找到最小生成树的一条边,不断重复直到找到最小生成树的全部边。这些算法之间的区别之处在于保存切分和断定权重最小的横切边的方式。数据结构
最小生成树的贪心算法:一副加权无向图中,在初始状态下全部边均为灰色,找到一种切分,它产生的横切边均不为黑色,将它权重最小的横切边标记为黑色,如此反复,直到标记了V-1条黑色边为止。app
其中V为图中顶点的数量,那么要将这些顶点所有链接,至少须要V-1条边。根据切分定理,全部被标记为黑色的边都属于最小生成树,若是黑色边的数量小于V-1,那么必然还存在不会产生黑色边的切分,只要找够V-1条黑色边,最小生成树就完成了。学习
加权无向图的数据结构没有沿用以前无向图的数据结构,而是从新定义了Edge和EdgeWeightedGraph类,分别用于表示带权重的边和加权无向图。ui
public class Edge implements Comparable<Edge> { private final int v; private final int w; private final double weight; public Edge(int v, int w, double weight) { this.v = v; this.w = w; this.weight = weight; } public double weight() { return this.weight; } public int either() { return this.v; } public int other(int vertex) { if (v == vertex) return w; if (w == vertex) return v; else throw new RuntimeException("Inconsistent edge"); } public int compareTo(Edge that) { if (this.weight() < that.weight()) return -1; else if (this.weight() > that.weight()) return 1; else return 0; } public String toString() { return String.format("%d-%d %.2f", v, w, weight); } }
either和other方法能够返回边链接的两个端点,weight表示边的权重。this
public class EdgeWeightedGraph { private static final String NEWLINE = System.getProperty("line.separator"); private final int V; // vertex private int E; // edge private Bag<Edge>[] adj; public EdgeWeightedGraph(int V) { this.V = V; this.E = 0; adj = (Bag<Edge>[]) new Bag[V]; for (int v = 0; v < V; v++) { adj[v] = new Bag<Edge>(); } } public EdgeWeightedGraph(In in) { this(in.readInt()); int E = in.readInt(); for (int i = 0; i < E; i++) { int v = in.readInt(); int w = in.readInt(); double weight = in.readDouble(); Edge e = new Edge(v, w, weight); addEdge(e); } } public int V() { return V; } public int E() { return E; } public void addEdge(Edge e) { int v = e.either(), w = e.other(v); adj[v].add(e); adj[w].add(e); E++; } public Iterable<Edge> adj(int v) { return adj[v]; } public String toString() { StringBuilder s = new StringBuilder(); s.append(V + " vertices, " + E + " edges " + NEWLINE); for (int v = 0; v < V; v++) { s.append(v + ": "); for (Edge w : adj[v]) { s.append(w + " | "); } s.append(NEWLINE); } return s.toString(); } public Bag<Edge> edges() { Bag<Edge> b = new Bag<Edge>(); for (int v = 0; v < V; v++) { for (Edge w : adj[v]) { b.add(w); } } return b; }
EdgeWeightedGraph与无向图中的Graph很是相似,只是用Edge对象替代了Graph中的整数来做为链表的结点。adj(int v)方法能够根据顶点而索引到对应的邻接表,每条边都会出现两次,若是一条边链接了顶点v和w,那么这条边会同时被添加到v和w对应的领接表中。spa
将要学习的第一种计算最小生成树的方法叫作Prim算法,它的每一部都会为一颗生长中的树添加一条边。一开始这棵树只有一个顶点,而后会向它添加V-1条边,每次老是将下一条链接树的顶点与不在树中的顶点且权重最小的边加入树中。
但如何才能高效地找到权重最小的边呢,使用优先队列即可以达到这个目的,而且保证足够高的效率。由于要寻找的是权重最小的边,因此这里将使用查找最小元素的优先队列MinPQ。
此外,Prim算法还会使用一个由顶点索引的boolean数组marked[],和一条名为mst的队列,前者用来指示已经加入到最小生成树中的顶点,队列则用来保存包含在最小生成树中的边。
每当在向树中添加了一条边时,也向树中添加了一个顶点。要维护一个包含全部横切边的集合,就要将链接这个顶点和其余全部不在树中的顶点的边加入优先队列,经过marked[]数组能够识别这样的边。须要注意的是,随着横切边的不断加入,以前加入的边中,那些链接新加入树中的顶点与其余已经在树中顶点的全部边都失效了,由于这样的边都已经不是横切边了,它的两个顶点都在树中,这样的边是不会被加入到mst队列中的。
接下来用tinyEWG.txt的数据来直观地观察算法的轨迹,tinyEWG.txt的内容以下:
8 16 4 5 0.35 4 7 0.37 5 7 0.28 0 7 0.16 1 5 0.32 0 4 0.38 2 3 0.17 1 7 0.19 0 2 0.26 1 2 0.36 1 3 0.29 2 7 0.34 6 2 0.40 3 6 0.52 6 0 0.58 6 4 0.93
它表示的图包含8个顶点,16条边,末尾的double数值表示边的权重。
下图是算法在处理tinyEWG.txt时的轨迹,每一张图都是算法访问过一个顶点以后(被添加到树中,邻接链表中的边也已经被处理完成),图和优先队列的状态。优先队列的内容被按照顺序显示在一侧,树中的新顶点旁边有个星号。
算法构造最小生成树的过程为:
算法的具体实现:
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); while (!pq.isEmpty()) { Edge e = pq.delMin(); int v = e.either(), w = e.other(v); if (marked[v] && marked[w]) continue; mst.enqueue(e); if (!marked[v]) visit(G, v); if (!marked[w]) visit(G, w); } } public 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; } // cmd /c --% java algs4.four.LazyPrimMST ..\..\..\algs4-data\tinyEWG.txt public static void main(String[] args) { In in = new In(args[0]); EdgeWeightedGraph ewg = new EdgeWeightedGraph(in); LazyPrimMST lazyPrim = new LazyPrimMST(ewg); double weight=0; for (Edge e : lazyPrim.edges()) { weight += e.weight(); StdOut.println(e); } StdOut.println(weight); } }
visit()方法的做用是为树添加一个顶点,将它标记为“已访问”,并将与它关联的全部未失效的边加入优先队列中。在while循环中,会从优先队列取出一条边,若是它没有失效,就把它添加到树中,不然只是将其从优先队列删除。而后再根据添加到树中的边的顶点,更新优先队列中横切边的集合。
Prim算法是一条边一条边地来构造最小生成树,每一步都为一棵树添加一条边。接下来要学习的Kruskal算法处理问题的方式则是按照边的权重顺序,从小到大将边添加到最小生成树中,加入的边不会与已经加入的边构成环,直到树中含有V-1条边为止。从一片由V颗单结点的树构成的森林开始,不断将两棵树合并,直到只剩下一颗树,它就是最小生成树。
一样是处理tinyEWG.txt,Kruskal算法的轨迹以下图:
【】
该算法首先会将全部的边加入到优先队列并按权重顺序排列,而后依次从优先队列拿到最小的边加入到最小生成树中,而后轮处处理1-三、1-五、2-7这三条边时,发现它们会使最小生成树造成环,说明这些顶点已经被包含到了最小生成树中,属于失效的边;接着继续处理4-5,随后1-二、4-七、0-4又被丢弃,把6-2加入树中后,最小生成树已经有了V-1条边,最小生成树已经造成,查找结束。
算法的具体实现为:
public class KruskalMST { private Queue<Edge> mst; private double _weight = 0; public KruskalMST(EdgeWeightedGraph G) { mst = new Queue<Edge>(); MinPQ<Edge> pq = new MinPQ<Edge>(); UF uf = new UF(G.V()); for (Edge e : G.edges()) { pq.insert(e); } while (!pq.isEmpty() && mst.size() < G.V() - 1) { Edge e = pq.delMin(); int v = e.either(), w = e.other(v); if (uf.connected(v, w)) continue; uf.union(v, w); mst.enqueue(e); _weight += e.weight(); } } public Iterable<Edge> edges() { return mst; } public double weight() { return _weight; } // cmd /c --% java algs4.four.KruskalMST ..\..\..\algs4-data\tinyEWG.txt public static void main(String[] args) { In in = new In(args[0]); EdgeWeightedGraph ewg = new EdgeWeightedGraph(in); KruskalMST kruskalMST = new KruskalMST(ewg); for (Edge e : kruskalMST.edges()) { StdOut.println(e); } StdOut.println(kruskalMST.weight()); } }
这里一样使用了MinPQ来为边排序,并使用了以前Union-Find算法中实现的的Quick Union数据结构,用它能够方便地识别会造成环的边,最终生成的最小生成树一样保存在名为mst的队列中。