您的位置:首页 > 其它

十七、图算法之最小生成树

2016-04-18 22:08 381 查看
#最小生成树



说白了就是连接所有点的然后权值最小的无环连通子图

主要有两种算法:Prim算法Kruskal算法

先做一些约定:

只考虑连通图(不连通你分开算就行了)
边的权重不一定是表示距离
边的权重可能是0或者负数(无环,负数就不影响)
所有边的权重都各不相同(造成最小生成树不唯一)


原理

回顾树的重要性质

用一条边连接树中的任意两个顶点都会产生一个新的环
从树中删除一条边将会得到两个独立地树


切分定理

切分定理这条性质会把加权图中的所有顶点分成两个集合,检查横跨两个集合的所有边并识别哪条边应属于图的最小生成树

定义:

图的切分是将图的所有顶点分为两个非空且不重复的两个集合。横切边是一条连接两个属于不同集合的顶点的边


通常给定一个集合,我们会隐式指定它的补集就是另一个集合,然后横切边就是连接两个集合的边

定理:

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

因为两个集合要连通,当然选权重最小的边


注意:在假设所有的边的权重均不相同的前提下,每幅图都只有一棵唯一的最小生成树

证明:

假设它存在两颗最小生成树a, b我们找到这两个方案不同的边中最小的一条边x, (既x在一个生成树上,不在另一个生成树上)。假设x在a里面,那我们考虑把x塞到b里面去,这样b+x里面肯定有一个环,环上至少有一条边比x大(如果每条边都比x小,那根据x的定义这些边肯定也都在a里面(因为找的是不同边),那a就有环了,矛盾),把这条边去掉之后还是一个生成树(你考虑把环去掉一条边变成了链,但是原来联通的现在也还是联通的)。但是这颗生成树比b小(加进了一条x,去掉了一条比x大的边),所以b不是最小生成树,矛盾故最小生成树是唯一的

每次切分产生的横切边并不一定只有那个权重最小的属于最小生成树中的边,实际可能有多条,但那个最小的肯定属于最小生成树中的边

最小生成树的贪心算法

切分算法算是贪心算法的一种,因为每次都是找最小

将图的初始下所有边均设为黑色,找到一种切分,它产生的横切边均不为黑色。将它权重最小的横切边标记为黑色。反复,直到标记了V-1条黑色边为止。这样就会将含有V个顶点的任意加权连通图中属于最小生成树的边标记为黑色


加权无向图的API

Edge类:

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) {
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 %.5f", v, w, weight);
}
}


加权无向图EdgeWeightedGraph:

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 EdgeWeightedGraph(int V, int E) {
this(V);
if (E < 0) throw new IllegalArgumentException("Number of edges must be nonnegative");
for (int i = 0; i < E; i++) {
int v = StdRandom.uniform(V);
int w = StdRandom.uniform(V);
double weight = Math.round(100 * StdRandom.uniform()) / 100.0;
Edge e = new Edge(v, w, weight);
addEdge(e);
}
}
public EdgeWeightedGraph(In in) {
this(in.readInt());
int E = in.readInt();
if (E < 0) throw new IllegalArgumentException("Number of edges must be nonnegative");
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 EdgeWeightedGraph(EdgeWeightedGraph G) {
this(G.V());
this.E = G.E();
for (int v = 0; v < G.V(); v++) {
// reverse so that adjacency list is in same order as original
Stack<Edge> reverse = new Stack<Edge>();
for (Edge e : G.adj[v]) {
reverse.push(e);
}
for (Edge e : reverse) {
adj[v].add(e);
}
}
}
public int V() {
return V;
}
public int E() {
return E;
}
// throw an IndexOutOfBoundsException unless 0 <= v < V
private void validateVertex(int v) {
if (v < 0 || v >= V)
throw new IndexOutOfBoundsException("vertex " + v + " is not between 0 and " + (V-1));
}
public void addEdge(Edge e) {
int v = e.either();
int w = e.other(v);
validateVertex(v);
validateVertex(w);
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();
}
public Iterable<Edge> edges() {
Bag<Edge> list = new Bag<Edge>();
for (int v = 0; v < V; v++) {
int selfLoops = 0;
for (Edge e : adj(v)) {
if (e.other(v) > v) {
list.add(e);
}
// only add one copy of each self loop (self loops will be consecutive)
else if (e.other(v) == v) {
if (selfLoops % 2 == 0) list.add(e);
selfLoops++;
}
}
}
return list;
}
public String toString() {
StringBuilder s = new StringBuilder();
s.append(V + " " + E + NEWLINE);
for (int v = 0; v < V; v++) {
s.append(v + ": ");
for (Edge e : adj[v]) {
s.append(e + "  ");
}
s.append(NEWLINE);
}
return s.toString();
}
}


Prim算法

prim算法每一步都会为一棵生长的树添加一条边。一开始这棵树只有一个顶点,每次总是将下一条连接树中的顶点与不在树中的顶点且权重最小的边加入树中。

Prim算法能够得到任意加权无向图的最小生成树


数据结构方面:

有一个标记数组marked[],在树中的点为true,反之为false
横切边用优先队列存储


如果新加点和已存在于优先队列存储的某些边中时,让优先队列中的这些边失效。

步骤

开始树中只有一个点,然后找与该点相连的边,加入优先队列,从优先队列中选权值最小的那个边(这个最小边出队,注意剩下的边还保留在优先队列中,仍作为备选边),然后把所选边另一个的点加入树中,对于新加入的点,找 剩下的 未访问的 那些点中 和 新加入的这个点 有连接的点,同样将 和新加点 有连接的 边 加入优先队列(已在树中的点就不算了) 。

注意:每次新加点时,优先队列中的某些边已经连接了新加点,就让那些边失效

复杂度:

空间与E成正比,所需时间与ElogE成正比

Prim算法的延时实现:

所谓延时:就是先不让边失效(等无效边出了优先队列后判断是不是无效就行了,无效就跳过去,见下面的continue)

内部使用了优先队列

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

// check optimality conditions
assert check(G);
}
// run Prim's algorithm
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;
}

// check optimality conditions (takes time proportional to E V lg* V)
private boolean check(EdgeWeightedGraph G) {
// check weight
double totalWeight = 0.0;
for (Edge e : edges()) {
totalWeight += e.weight();
}
if (Math.abs(totalWeight - weight()) > FLOATING_POINT_EPSILON) {
System.err.printf("Weight of edges does not equal weight(): %f vs. %f\n", totalWeight, weight());
return false;
}
// check that it is acyclic
UF uf = new UF(G.V());
for (Edge e : edges()) {
int v = e.either(), w = e.other(v);
if (uf.connected(v, w)) {
System.err.println("Not a forest");
return false;
}
uf.union(v, w);
}
// check that it is a spanning forest
for (Edge e : G.edges()) {
int v = e.either(), w = e.other(v);
if (!uf.connected(v, w)) {
System.err.println("Not a spanning forest");
return false;
}
}
// check that it is a minimal spanning forest (cut optimality conditions)
for (Edge e : edges()) {

// all edges in MST except e
uf = new UF(G.V());
for (Edge f : mst) {
int x = f.either(), y = f.other(x);
if (f != e) uf.union(x, y);
}

// check that e is min weight edge in crossing cut
for (Edge f : G.edges()) {
int x = f.either(), y = f.other(x);
if (!uf.connected(x, y)) {
if (f.weight() < e.weight()) {
System.err.println("Edge " + f + " violates cut optimality conditions");
return false;
}
}
}

}

return true;
}
}


Prim算法的即时实现

当我们将顶点v添加到树中时,对于每个非树顶点w产生的变化只可能使得w到最小生成树的距离更近了(基本思想)。


简而言之,我们不需要再优先队列中保存所有从w到树顶点的边,而只需要保存其中权重最小的那条,将v添加到树中后检查是否需要更新这条权重最小的边(因为v-w的权重可能更小)。

用一个EdgeTo数组保存每个点到树的距离(相当于把树看成一个整体),每加一个点就更新这个距离,选取点时,也是选到树距离最小的点

复杂度: 空间和V相关,时间和ElogV成正比

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

// check optimality conditions
assert check(G);
}
// 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;
}
// check optimality conditions (takes time proportional to E V lg* V)
private boolean check(EdgeWeightedGraph G) {

// check weight
double totalWeight = 0.0;
for (Edge e : edges()) {
totalWeight += e.weight();
}
if (Math.abs(totalWeight - weight()) > FLOATING_POINT_EPSILON) {
System.err.printf("Weight of edges does not equal weight(): %f vs. %f\n", totalWeight, weight());
return false;
}

// check that it is acyclic
UF uf = new UF(G.V());
for (Edge e : edges()) {
int v = e.either(), w = e.other(v);
if (uf.connected(v, w)) {
System.err.println("Not a forest");
return false;
}
uf.union(v, w);
}

// check that it is a spanning forest
for (Edge e : G.edges()) {
int v = e.either(), w = e.other(v);
if (!uf.connected(v, w)) {
System.err.println("Not a spanning forest");
return false;
}
}
// check that it is a minimal spanning forest (cut optimality conditions)
for (Edge e : edges()) {
// all edges in MST except e
uf = new UF(G.V());
for (Edge f : edges()) {
int x = f.either(), y = f.other(x);
if (f != e) uf.union(x, y);
}
// check that e is min weight edge in crossing cut
for (Edge f : G.edges()) {
int x = f.either(), y = f.other(x);
if (!uf.connected(x, y)) {
if (f.weight() < e.weight()) {
System.err.println("Edge " + f + " violates cut optimality conditions");
return false;
}
}
}
}
return true;
}
}


Kruskal算法

Kruskal算法能够计算任意加权无向图的最下生成树

按照边的权重排序,加入时要判断不能构成环。将这些边逐渐由一片森林构成一棵树

Prim算法是一条边一条边地构造最小生成树,但不同于Prim算法,每次添加的边不一定是加在同一棵树上,但最终,这些树会一步一步融合

复杂度: 空间和E成正比,时间和ElogE成正比

内部使用了优先队列和union-find算法

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);
}
// run greedy algorithm
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();
}
}
// check optimality conditions
assert check(G);
}
public Iterable<Edge> edges() {
return mst;
}
public double weight() {
return weight;
}
// check optimality conditions (takes time proportional to E V lg* V)
private boolean check(EdgeWeightedGraph G) {
// check total weight
double total = 0.0;
for (Edge e : edges()) {
total += e.weight();
}
if (Math.abs(total - weight()) > FLOATING_POINT_EPSILON) {
System.err.printf("Weight of edges does not equal weight(): %f vs. %f\n", total, weight());
return false;
}
// check that it is acyclic
UF uf = new UF(G.V());
for (Edge e : edges()) {
int v = e.either(), w = e.other(v);
if (uf.connected(v, w)) {
System.err.println("Not a forest");
return false;
}
uf.union(v, w);
}
// check that it is a spanning forest
for (Edge e : G.edges()) {
int v = e.either(), w = e.other(v);
if (!uf.connected(v, w)) {
System.err.println("Not a spanning forest");
return false;
}
}
// check that it is a minimal spanning forest (cut optimality conditions)
for (Edge e : edges()) {

// all edges in MST except e
uf = new UF(G.V());
for (Edge f : mst) {
int x = f.either(), y = f.other(x);
if (f != e) uf.union(x, y);
}

// check that e is min weight edge in crossing cut
for (Edge f : G.edges()) {
int x = f.either(), y = f.other(x);
if (!uf.connected(x, y)) {
if (f.weight() < e.weight()) {
System.err.println("Edge " + f + " violates cut optimality conditions");
return false;
}
}
}

}

return true;
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: