最小生成树
图的生成树是它的一棵含有所有顶点的无环连通子图。一幅加权无向图的最小生成树是它的一棵权值(树中所有边的权值之和)最小的生成树。
原理:
- 用一条边连接树中任意两个顶点都会产生一个新的环;
- 从树中删去一条边都会得到两棵独立的树;
切分定理
图的一种切分是将图的所有顶点分为两个非空的且不重复的两个集合。横切边是一条连接两个属于不同集合顶点的边。在一幅加权图中,给定任意的切分,它的横切边中的权重最小者必然属于图的最小生成树。
加权无向图边的定义
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) { if (v < 0) throw new IndexOutOfBoundsException("Vertex name must be a nonnegative integer"); if (w < 0) throw new IndexOutOfBoundsException("Vertex name must be a nonnegative integer"); if (Double.isNaN(weight)) throw new IllegalArgumentException("Weight is NaN"); this.v = v; this.w = w; this.weight = weight; } public double weight() { return weight; } public int either() { return v; } public int other(int vertex) { if(vertex == v) return w; else if (vertex == w) return v; else throw new IllegalArgumentException("Illegal endpoint"); } @Override public int compareTo(Edge that) { return Double.compare(this.weight, that.weight); } }
加权无向图
public class EdgeWeightedGraph { private static final String NEWLINE = System.getProperty("line.separator"); private final int V; private int E; private Bag<Edge>[] adj; public EdgeWeightedGraph(int V) { if (V < 0) throw new IllegalArgumentException("Number of vertices must be nonnegative"); 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 int V() { return V; } public int E() { return E; } public void addEdge(Edge e) { int v = e.either(); int w = e.other(v); adj[v].add(e); adj[w].add(e); E++; } public Iterable<Edge> adj(int v) { validateVertex(v); return adj[v]; } public int degree(int v) { validateVertex(v); return adj[v].size(); } }
Prim(普利姆)算法
一开始这棵树只有一个顶点,然后将它添加V-1条边,每次总是将下一条连接树中的顶点与不在树中的顶点且权重最小的边加入树中。Prim算法的延时计算一幅含有V个顶点和E条边的连通加权无向图的最小生成树所需的时间与ElongE成正比。算法简单描述如下:
- 输入:一个加权连通图,其中顶点集合为V,边集合为E;
- 初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {},为空;
- 重复下列操作,直到Vnew = V;
a)在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
b)将v加入集合Vnew中,将<u, v>边加入集合Enew中;
4输出:使用集合Vnew和Enew来描述所得到的最小生成树。
Prim算法的延时实现:
public class LazyPrimMST { private static final double FLOATING_POINT_EPSILON = 1E-12; private double weight; // total weight of MST private Queue<Edge> mst; // edges in the MST private boolean[] marked; // marked[v] = true if v on tree private MinPQ<Edge> pq; // edges with one endpoint in tree public LazyPrimMST(EdgeWeightedGraph G) { mst = new Queue<Edge>(); pq = new MinPQ<Edge>(); marked = new boolean[G.V()]; for (int v = 0; v < G.V(); v++) // run Prim from all vertices to if (!marked[v]) prim(G, v); // get a minimum spanning forest } private void prim(EdgeWeightedGraph G, int s) { scan(G, s); while (!pq.isEmpty()) { // better to stop when mst has V-1 edges Edge e = pq.delMin(); // smallest edge on pq int v = e.either(), w = e.other(v); // two endpoints assert marked[v] || marked[w]; if (marked[v] && marked[w]) continue; // lazy, both v and w already scanned mst.enqueue(e); // add e to MST weight += e.weight(); if (!marked[v]) scan(G, v); // v becomes part of tree if (!marked[w]) scan(G, w); // w becomes part of tree } } // add all edges e incident to v onto pq if the other endpoint has not yet been scanned private void scan(EdgeWeightedGraph G, int v) { assert !marked[v]; marked[v] = true; for (Edge e : G.adj(v)) if (!marked[e.other(v)]) pq.insert(e); } public Iterable<Edge> edges() { return mst; } public double weight() { return weight; } }
Prim算法的即时实现:
public class PrimMST { private static final double FLOATING_POINT_EPSILON = 1E-12; private Edge[] edgeTo; // edgeTo[v] = shortest edge from tree vertex to non-tree vertex private double[] distTo; // distTo[v] = weight of shortest such edge private boolean[] marked; // marked[v] = true if v on tree, false otherwise private IndexMinPQ<Double> pq; public PrimMST(EdgeWeightedGraph G) { edgeTo = new Edge[G.V()]; distTo = new double[G.V()]; marked = new boolean[G.V()]; pq = new IndexMinPQ<Double>(G.V()); for (int v = 0; v < G.V(); v++) distTo[v] = Double.POSITIVE_INFINITY; for (int v = 0; v < G.V(); v++) // run from each vertex to find if (!marked[v]) prim(G, v); // minimum spanning forest } // run Prim's algorithm in graph G, starting from vertex s private void prim(EdgeWeightedGraph G, int s) { distTo[s] = 0.0; pq.insert(s, distTo[s]); while (!pq.isEmpty()) { int v = pq.delMin(); scan(G, v); } } // scan vertex v private void scan(EdgeWeightedGraph G, int v) { marked[v] = true; for (Edge e : G.adj(v)) { int w = e.other(v); if (marked[w]) continue; // v-w is obsolete edge if (e.weight() < distTo[w]) { distTo[w] = e.weight(); edgeTo[w] = e; if (pq.contains(w)) pq.decreaseKey(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; } public double weight() { double weight = 0.0; for (Edge e : edges()) weight += e.weight(); return weight; } }
Kruskal(克鲁斯卡尔)算法
算法描述:克鲁斯卡尔算法需要对图的边进行访问,所以克鲁斯卡尔算法的时间复杂度只和边又关系,可以证明其时间复杂度为O(eloge)。算法过程如下:
- 将图各边按照权值进行排序
- 将图遍历一次,找出权值最小的边,(条件:此次找出的边不能和已加入最小生成树集合的边构成环),若符合条件,则加入最小生成树的集合中。不符合条件则继续遍历图,寻找下一个最小权值的边。
- 递归重复步骤1,直到找出n-1条边为止(设图有n个结点,则最小生成树的边数应为n-1条),算法结束。得到的就是此图的最小生成树。
public class KruskalMST { private static final double FLOATING_POINT_EPSILON = 1E-12; private double weight; // weight of MST private Queue<Edge> mst = new Queue<Edge>(); // edges in MST public KruskalMST(EdgeWeightedGraph G) { // more efficient to build heap by passing array of edges MinPQ<Edge> pq = new MinPQ<Edge>(); for (Edge e : G.edges()) { pq.insert(e); } UF uf = new UF(G.V()); while (!pq.isEmpty() && mst.size() < G.V() - 1) { Edge e = pq.delMin(); int v = e.either(); int w = e.other(v); if (!uf.connected(v, w)) { // v-w does not create a cycle uf.union(v, w); // merge v and w components mst.enqueue(e); // add edge e to mst weight += e.weight(); } } } public Iterable<Edge> edges() { return mst; } public double weight() { return weight; } }
不忘初心,方得始终