加权无向图问题--最小代价生成树(Prim算法、kruskal算法)

痴心易碎 提交于 2019-11-26 11:59:05

加权无向图的实现

加权无向图的实现最简单的方法是扩展无向图的表示方法:在邻接表的表示中,可以在链表的结点中增加一个权重域。但这里用另一个方法来实现:我们实现两个类,权重边类和无向图类。无向图类中组合权重边类来实现加权无向图。

权重边类:

public class Edge implements Comparable<Edge> {    //实现Comparable接口
    private int v;
    private int w;
    private double weight;
	
    public Edge(int v,int w,double weight)
    { this.v = v; this.w = w; this.weight = weight;}

	//获取边的一个结点和另一个节点
    public int either() {return v;}
    public int other(int vertex) {
        if(vertex == v) return w;
        else if(vertex == w) return v;
    }
    //实现接口中的compareTo()方法
    public int compareTo(Edge that) {
        if(this.weight() < that.weight()) return -1;
        else if(this.weight() > that.weight()) return 1;
        else return 0;
    }

    public double weight() {return weight;}
    public String toString() { return String.format("%d-%d %.2f", v,w,weight); }
}

加权无向图类:

public class EdgeWeightedGraph {
    private int V;//顶点数
    private int E;//边数
    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 int V() {return V;}
    public int E() {return E;}
	//添加边
    public void addEdge(Edge e) {
        //获取边的两个顶点
        int v = e.either();
        int w = e.other(v);
        //因为是无向图,互相添加边,调用的是背包的add()方法
        adj[v].add(e);   
        adj[w].add(e);
        E++;
    }
	//和v相关联的所有边
    public Iterable<Edge> adj(int v){ return adj[v]; }
    //图的所有边
    public Iterable<Edge> edges(){
        Bag<Edge> b = new Bag<Edge>();
        for(int v = 0; v <V ; v++)
            for(Edge e : adj[v])
                if(e.other(v)>v) b.add(e);
        return b;
    }
}

Prim算法:

图的生成树是它的一棵含有其所有顶点的无环连通子图,加权图的最小生成树(MST)是它的一棵权值最小的生成树。 Prim算法能够得到任意加权连通无向图的最小生成树。

切分:

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

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

切分定理是解决最小生成树问题的所有算法的基础。

数据结构:

  • 采用一个布尔数组marked[]来记录顶点是否在树中,如果顶点v在,则marked[v]为true。
  • 使用一条优先权队列来保存所有的横切边。
  • 使用一条队列保存最小生成树的边。

算法思想:

使用一个最小优先权队列保存横切边集合,每次新加进来一个结点,就将和该结点关联的所有边添加进最小优先权队列;生成最小树时,从横切边集合中取出最小边,判断是否和目前的树产生环,如果产生环,则舍弃该边;否则将该边加入最小生成树队列。

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);//从顶点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);
        }
    }
    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算法的延时实现计算一个含V个顶点和E条边的连通加权无向图的最小生成树所需空间与E成正比,所需时间与ElogE成正比(最坏情况)。

Prim算法即时实现:

要改进LazyPrimMST,可以尝试从优先队列中删除用不到的边。关键在于,我们关注的只是连接树顶点和非树顶点中权重最小的边。当我们将顶点v加入树中,只可能使非树顶点w到最小生成树更近了。简而言之,我们不必保存所有从w到树顶点的边, 只需保存最小的那条即可。在v添加进树中时遍历v的邻接表检查是否需要更新权重最小的边。

引进两个顶点索引数组edgeTo[]和distTo[],它们有如下性质:

  • 如果顶点v不在树中但至少含有一条边和树相连,那么edgeTo[v]将是v和树连接的最短的边,distTo[v]为这条边的权重。
  • 所有这类v都保存在一条索引优先队列中,索引v关联的值是edgeTo[v]的边的权重。
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);//顶点0初始化pq
        while(!pq.isEmpty())
            visit(G,pq.delMin());
        }
	
    public void visit(EdgeWeightedGraph G,int v) {
        marked[v] = true;
        for(Edge e: G.adj(v)) {
            int w = e.other(v);
            if(marked[w]) continue;//v-w失效
            if(e.weight()<distTo[w]) {
                edgeTo[w] = e;//最佳边Edge变为e
                distTo[w] = e.weight();//更新distTo[]
                if(pq.contains(w))    pq.change(w, distTo[w]);
                else pq.insert(w, distTo[w]);
            }
        }
    }
}

Prim算法的即时实现计算一个含有V个顶点和E条边的连通加权无向图的最小生成树所需空间和V成正比,所需时间和ElogV成正比(最坏情况)。

Kruskal算法:

数据结构:

  • 用一条优先队列将边按照权重从小到大排序
  • union-find算法来识别会形成环的边
  • 用一条队列来保存最小生成树的所有边

Kruskal算法的计算一个含V个顶点和E条边的连通加权无向图的最小生成树所需空间与E成正比,所需时间与ElogE成正比(最坏情况)。

算法思想:

将边都添加进最小优先权队列中,每次从中取出最小的边,检查会不会与已经选出的边构成环(使用union-find算法),如果构成环,则弃掉这条边,否则将这条边加入最小生成树队列。循环执行直到最小优先权队列为空。

算法实现:

public class KruskalMST {
    private Queue<Edge> mst;    //用来保存最小代价生成树的队列
	
    public KruskalMST(EdgeWeightedGraph G) {
        mst = new Queue<Edge>();
        MinPQ<Edge> pq = new MinPQ<Edge>();    //最小优先权队列
        for(Edge e: G.edges())
            pq.insert(e);//将所有边添加进优先队列
        UF uf = new UF(G.V());    //union-find算法
		
        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.qu_union(v,w);//合并分量
            mst.enqueue(e);//将边添加进树中
        }
    }
    public Iterable<Edge> edges(){ return mst; }
}
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!