数据结构与算法(12):最小生成树

原理

树的两条基本性质:

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

切分定理

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

最小生成树的贪心算法

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

加权无向图的数据类型

public class Edge implements Comparable<Edge>{
    private int v, w;
    private double weight;
    Edge(int v, int w, double weight){
        this.v = v;
        this.w = w;
        this.weight = weight;
    }
    public double weight(){
        return weight;
    }
    public int either(){
        return v;
    }
    public int other(int v){
        return this.v == v ? w : this.v;
    }
    @Override
    public int compareTo(Edge o) {
        return (int)Math.floor(weight - o.weight);// 注意浮点数的比较
    }
}
public class EdgeWeightGraph {
    private int V;
    private int E;
    private Bag<Edge>[] adj;
    EdgeWeightGraph(int V){
        adj = new Bag[V];
        for(int i = 0; i < V; i++){
            adj[i] = new Bag<Edge>();
        }
    }
    public int V(){
        return V;
    }
    public int E(){
        return E;
    }
    public void addEdge(int v, int w, double weight){
        adj[v].add(new Edge(v, w, weight));
        E++;
    }
    Iterable<Edge> adj(int v){
        return adj[v];
    }
    public EdgeWeightGraph reverse(){
        EdgeWeightGraph copy = new EdgeWeightGraph(V);
        for(int v = 0; v < V; v++){
            for(Edge e : adj(v)){
                copy.addEdge(e.other(v), v, e.weight());
            }
        }
        return copy;
    }
}

Prim算法

原理

Prim算法的每一步都会为一棵生长中的树添加一条边。开始这棵树只有一个顶点,然后会向它添加V-1条边,每次总是将下一条连接树中的顶点与不在树中的顶点且权重最小的边加入树中(即由树中的顶点所定义的切分中的一条横切边)。

数据结构

  • 顶点。使用一个顶点索引的布尔数组marked[],如果顶点v在树中,那么marked[v]的值为true。
  • 边。选择以下两种数据结构之一:一条队列mst来保存最小生成树中的边,或者一个由顶点索引的Edge对象的数组edgeTo[],其中edgeTo[v]为将v连接到树中的Edge对象。
  • 横切边:使用一条优先队列MinPQ来根据权重比较所有边。

Prim算法的延时实现

我们从优先队列中取出一条边并将它添加到树中(如果它还没有失效的话),再把这条边的另一个顶点也添加到树中,然后用新顶点作为参数调用visit()方法来更新横切边的集合。weight()方法可以遍历树的所有边并得到它们的权重之和(延时实现)或是用一个运行时的变量统计总权重(即时实现)。

import java.util.LinkedList;
import java.util.Queue;
public class LazyPrimMST {
    private boolean[] marked;
    private Queue<Edge> pq;
    LazyPrimMST(EdgeWeightGraph G){
        marked = new boolean[G.V()];
        pq = new LinkedList<Edge>();
        visit(G, 0);
        while (!pq.isEmpty()){
            Edge e = pq.poll();
            int v = e.either(), w = e.other(v);
            if(marked[v] && marked[w]) continue;
            if(!marked[v]) visit(G, v);
            if(!marked[w]) visit(G, w);
        }
    }
    private void visit(EdgeWeightGraph G, int v){
        marked[v] = true;
        for(Edge e : G.adj(v)){
            int w = e.other(v);
            if(!marked[w]){
                pq.add(e);
            }
        }
    }
    public Iterable<Edge> edges(){
        return pq;
    }
    public double weight(){
        double weight = 0;
        for(Edge e : pq){
            weight += e.weight();
        }
        return weight;
    }
}

Prim算法的即时实现

可以从如下两个方面改进LazyPrimMST:

  • 从优先队列中删除失效的边,这样优先队列就只含有树顶点和非树顶点之间的横切边。
  • 我们不需要再优先队列找那个保存所有从w到树顶点的边——而只需要保存其中权重最小的那条,在将v添加到树中后检查是否需要更新这条权重最小的边(因为v-w的权重可能更小)

PrimMST类将LazyPrimMST中的marked[]和mst[]替换为两个顶点索引的数组edgeTo[]和distTo[],它们具有如下性质:

  • 如果顶点v不在树中但至少含有一条边和树相连,那么edgeTo[v]是将v和树连接的最短边,distTo[v]为这条边的权重。
  • 所有这类顶点v都保存在一条索引优先队列中,索引v关联的值是edgeTo[v]的边的权重。
import Sort.IndexMinPQ;
import java.util.LinkedList;
import java.util.Queue;
public class PrimMST {
    private boolean[] marked;
    private IndexMinPQ<Double> pq;
    private Edge[] edgeTo;
    private double[] distTo;
    PrimMST(EdgeWeightGraph G){
        int V = G.V();
        marked = new boolean[V];
        pq = new IndexMinPQ<Double>(V);
        for(int v = 0; v < V; v++){
            distTo[v] = Double.POSITIVE_INFINITY;
        }
        distTo[0] = 0.0;
        pq.insert(0, 0.0);
        visit(G, 0);
        while (!pq.isEmpty()){
            visit(G, pq.delMin()); // 将最近的顶点添加到树中
        }
    }
    private void visit(EdgeWeightGraph G, int v){
        marked[v] = true;
        for(Edge e : G.adj(v)){
            int w = e.other(v);
            if(marked[w]) continue;
            if(e.weight() < distTo[w]){
                edgeTo[w] = e;
                distTo[w] = e.weight();
                if(pq.contain(w)) pq.change(w, distTo[w]);
                else pq.insert(w, distTo[w]);
            }
        }
    }
    public Iterable<Edge> edges(){
        Queue<Edge> mst = new LinkedList<Edge>();
        for(Edge e : edgeTo) {
            if (e != null) {
                mst.add(e);
            }
        }
        return mst;
    }
    public double weight(){
        double weight = 0;
        for(Edge e : edges()){
            weight += e.weight();
        }
        return weight;
    }
}

Kruskal算法

算法按照边的权重顺序(从小到大)处理它们,将边加入最小生成树中,加入的边不会与已经加入的边构成环,直到树中含有V-1条边为止。
我们使用一条优先队列来将边按照权重排序,用一个union-find数据结构来识别会形成环的边,以及一条队列来保存最小生成树的所有边。

package Graph;

import Sort.MinPQ;
import UnionFind.WeightedQuickUnionUF;

import java.util.LinkedList;
import java.util.Queue;

public class KruskalMST {
    private boolean[] marked;
    private Queue<Edge> mst;
    KruskalMST(EdgeWeightGraph G){
        marked = new boolean[G.V()];
        mst = new LinkedList<Edge>();
        MinPQ<Edge> pq = new MinPQ<Edge>();
        for(Edge e:G.edges()) pq.insert(e);
        WeightedQuickUnionUF uf = new WeightedQuickUnionUF(G.V()); 
        while (!pq.isEmpty()){
            Edge e = pq.delMin();
            int v = e.either(), w = e.other(v);
            if(uf.connected(v, w)) continue;
            uf.union(v, w);
            mst.add(e);
        }
    }
    public Iterable<Edge> edges(){
        return mst;
    }
    public double weight(){
        double weight = 0;
        for(Edge e : mst){
            weight += e.weight();
        }
        return weight;
    }
}

你可能感兴趣的:(数据结构与算法)