加权无向图和最小生成树

加权无向图

加权无向图是一种为每条边关联一个权重值或是成本的图模型。这种图能够自然地表示许多应用。在一副航空图中,边表示航线,权值则可以表示距离或是费用。在一副电路图中,边表示导线,权值则可能表示导线的长度即成 本,或是信号通过这条先所需的时间。此时我们很容易就能想到,最小成本的问题,例如,从西安飞纽约,怎样飞才能使时间成本最低或者是金钱成本最低? 在下图中,从顶点0到顶点4有三条路径,分别为0-2-3-4,0-2-4,0-5-3-4,那我们如果要通过那条路径到达4顶点最好呢?此时就要考虑,那条路径的成本最低。

在下图中,从顶点0到顶点4有三条路径,分别为0-2-3-4,0-2-4,0-5-3-4,那我们如果要通过那条路径到达4顶点最好呢?此时就要考虑,那条路径的成本最低。

image-20210830090358433

加权无向图边的表示

加权无向图中的边我们就不能简单的使用v-w两个顶点表示了,而必须要给边关联一个权重值,因此我们可以使用对象来描述一条边。

类名 Edge implements Comparable
构造方法 Edge(int v,int w,double weight):通过顶点v和w,以及权重weight值构造一个边对象
成员方法 1.public double weight():获取边的权重值
2.public int either():获取边上的一个点
3.public int other(int vertex)):获取边上除了顶点vertex外的另外一个顶点
4.public int compareTo(Edge that):比较当前边和参数that边的权重,如果当前边权重大,返回 1,如果一样大,返回0,如果当前权重小,返回-1
成员变量 1.private final int v:顶点一
2.private final int w:顶点二
3.private final double weight:当前边的权重

代码实现:

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 v;
    }

    public int other(int vertex) {
        if (vertex == v)
            return w;
        else
            return v;
    }

    @Override
    public int compareTo(Edge o) {
        return Double.compare(this.weight, o.weight);
    }

    @Override
    public String toString() {
        return "Edge{" +
                "v=" + v +
                ", w=" + w +
                ", weight=" + weight +
                '}';
    }
}

加权无向图的实现

api设计:

类名 EdgeWeightedGraph
构造方法 EdgeWeightedGraph(int V):创建一个含有V个顶点的空加权无向图
成员方法 1.public int V():获取图中顶点的数量
2.public int E():获取图中边的数量
3.public void addEdge(Edge e):向加权无向图中添加一条边e
4.public Queue adj(int v):获取和顶点v关联的所有边
5.public Queue edges():获取加权无向图的所有边
成员变量 1.private final int V: 记录顶点数量
2.private int E: 记录边数量
3.private Queue[] adj: 邻接表

代码实现:

public class EdgeWeightedGraph {

    private final int V;

    private int E;

    private Queue<Edge>[] adj;


    public EdgeWeightedGraph(int V) {
        this.V = V;
        this.E = 0;
        this.adj = new Queue[V];

        for (int i = 0; i < adj.length; i++) {
            adj[i] = new Queue<>();
        }
    }

    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].enqueue(e);
        adj[w].enqueue(e);
        E++;
    }

    public Queue<Edge> adj(int v) {
        return adj[v];
    }

    /**
     * @author wen.jie
     * @date 2021/8/30 9:40
     * 获取加权无向图中的所有边
     */
    public Queue<Edge> edges(){
        Queue<Edge> allEdges = new Queue<>();
        for (int v = 0; v < V; v++) {
            //遍历v顶点的邻接表
            for (Edge e : adj(v)) {
                if (e.other(v) < v)
                    allEdges.enqueue(e);
            }
        }
        return allEdges;
    }
}

最小生成树

定义和相关约定

图的生成树是它的一棵含有其所有顶点的无环连通子图,一副加权无向图的最小生成树它的一棵权值(树中所有边的权重之和)最小的生成树

image-20210830095659174

约定:

只考虑连通图。最小生成树的定义说明它只能存在于连通图中,如果图不是连通的,那么分别计算每个连通图子图 的最小生成树,合并到一起称为最小生成森林。

image-20210830095859574

所有边的权重都各不相同。如果不同的边权重可以相同,那么一副图的最小生成树就可能不唯一了,虽然我们的算法可以处理这种情况,但为了好理解,我们约定所有边的权重都各不相同。

最小生成树原理

树的性质

  1. 用一条边接树中的任意两个顶点都会产生一个新的环;

image-20210830100110768

  1. 从树中删除任意一条边,将会得到两棵独立的树;

image-20210830100129407

切分定理

切分定理

要从一副连通图中找出该图的最小生成树,需要通过切分定理完成。

切分:将图的所有顶点按照某些规则分为两个非空且没有交集的集合。

横切边:连接两个属于不同集合的顶点的边称之为横切边。

例如我们将图中的顶点切分为两个集合,灰色顶点属于一个集合,白色顶点属于另外一个集合,那么效果如下:

image-20210830100336884

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

image-20210830100550252

注意:一次切分产生的多个横切边中,权重最小的边不一定是所有横切边中唯一属于图的最小生成树的边。

贪心算法

贪心算法是计算图的最小生成树的基础算法,它的基本原理就是切分定理,使用切分定理找到最小生成树的一条边,不断的重复直到找到最小生成树的所有边。如果图有V个顶点,那么需要找到V-1条边,就可以表示该图的最小生成树。

image-20210830100819594

image-20210830101025806

image-20210830101140811

image-20210830101523316

image-20210830101542080

计算图的最小生成树的算法有很多种,但这些算法都可以看做是贪心算法的一种特殊情况,这些算法的不同之处在于保存切分和判定权重最小的横切边的方式。

Prim算法

我们学习第一种计算最小生成树的方法叫Prim算法,它的每一步都会为一棵生成中的树添加一条边。一开始这棵树只有一个顶点,然后会向它添加V-1条边,每次总是将下一条连接树中的顶点与不在树中的顶点且权重最小的边加入到树中。

Prim算法的切分规则:把最小生成树中的顶点看做是一个集合,把不在最小生成树中的顶点看做是另外一个集合。

image-20210830104359797

Prim算法API设计

类名 PrimMST
构造方法 PrimMST(EdgeWeightedGraph G):根据一副加权无向图,创建最小生成树计算对象;
成员方法 1.private void visit(EdgeWeightedGraph G, int v):将顶点v添加到最小生成树中,并且更新数据
2.public Queue edges():获取最小生成树的所有边
成员变量 1.private Edge[] edgeTo: 索引代表顶点,值表示当前顶点和最小生成树之间的最短边
2.private double[] distTo: 索引代表顶点,值表示当前顶点和最小生成树之间的最短边的权重
3.private boolean[] marked:索引代表顶点,如果当前顶点已经在树中,则值为true,否则为 false
4.private IndexMinPriorityQueue pq:存放树中顶点与非树中顶点之间的有效横切边

Prim算法的实现原理

Prim算法始终将图中的顶点切分成两个集合,最小生成树顶点和非最小生成树顶点,通过不断的重复做某些操作,可以逐渐将非最小生成树中的顶点加入到最小生成树中,直到所有的顶点都加入到最小生成树中。

我们在设计API的时候,使用最小索引优先队列存放树中顶点与非树中顶点的有效横切边,那么它是如何表示的呢?我们可以让最小索引优先队列的索引值表示图的顶点,让最小索引优先队列中的值表示从其他某个顶点到当前顶点的边权重。

8代表八个顶点,16代表16条边

image-20210830105458369

初始化状态,先默认0是最小生成树中的唯一顶点,其他的顶点都不在最小生成树中,此时横切边就是顶点0的邻接表中0-2,0-4,0-6,0-7这四条边,我们只需要将索引优先队列的2、4、6、7索引处分别存储这些边的权重值就可以表示了。

现在只需要从这四条横切边中找出权重最小的边,然后把对应的顶点加进来即可。所以找到0-7这条横切边的权重最小,因此把0-7这条边添加进来,此时0和7属于最小生成树的顶点,其他的不属于,现在顶点7的邻接表中的边也成为了横切边,这时需要做两个操作:

1、0-7这条边已经不是横切边了,需要让它失效: 只需要调用最小索引优先队列的delMin()方法即可完成;

2、2和4顶点各有两条连接指向最小生成树,需要只保留一条: 4-7的权重小于0-4的权重,所以保留4-7,调用索引优先队列的change(4,0.37)即可, 0-2的权重小于2-7的权重,所以保留0-2,不需要做额外操作。

image-20210830105957708

代码实现:

/**
 * @author wen.jie
 * @date 2021/8/30 11:00
 * prim算法
 */
public class PrimMST {

    //索引代表顶点,值表示当前顶点和最小生成树之间的最短边
    private Edge[] edgeTo;

    //索引代表顶点,值表示当前顶点和最小生成树之间的最短边的权重
    private double[] distTo;

    //索引代表顶点,如果当前顶点已经在树中,则值为true,否则为 false
    private boolean[] marked;

    //存放树中顶点与非树中顶点之间的有效横切边
    private IndexMinPriorityQueue<Double> pq;

    public PrimMST(EdgeWeightedGraph G) {
        this.edgeTo = new Edge[G.V()];
        this.distTo = new double[G.V()];
        Arrays.fill(distTo, Double.POSITIVE_INFINITY);
        this.marked = new boolean[G.V()];
        pq = new IndexMinPriorityQueue<>(G.V());

        //默认让顶点0进入到树中
        distTo[0] = 0.0;
        pq.insert(0, 0.0);
        //遍历索引最小优先队列,拿到最小边和N切边对应的顶点,把该顶点加入到最小生成树中
        while (!pq.isEmpty()) {
            visit(G, pq.delMin());
        }
    }

    //将顶点v添加到最小生成树中,并且更新数据
    private void visit(EdgeWeightedGraph 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.contains(w)) {
                    pq.changeItem(w, e.weight());
                }else {
                    pq.insert(w, e.weight());
                }
            }
        }
    }

    //获取最小生成树的所有边
    public Queue<Edge> edges() {
        Queue<Edge> allEdges = new Queue<>();
        for (Edge edge : edgeTo) {
            if (edge != null) {
                allEdges.enqueue(edge);
            }
        }
        return allEdges;
    }

}

测试:

    String[] strs = new String[]{
            "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"
    };


    @Test
    public void test() {
        EdgeWeightedGraph G = new EdgeWeightedGraph(8);
        for (String str : strs) {
            String[] split = str.split(" ");
            Edge e = new Edge(Integer.parseInt(split[0]), Integer.parseInt(split[1]), Double.parseDouble(split[2]));
            G.addEdge(e);
        }

        PrimMST primMST = new PrimMST(G);
        for (Edge edge : primMST.edges()) {
            System.out.println(edge);
        }
    }

image-20210830141556782

kruskal算法

kruskal算法是计算一副加权无向图的最小生成树的另外一种算法,它的主要思想是按照边的权重(从小到大)处理它们,将边加入最小生成树中,加入的边不会与已经加入最小生成树的边构成环,直到树中含有V-1条边为止。

kruskal算法和prim算法的区别:

Prim算法是一条边一条边的构造最小生成树,每一步都为一棵树添加一条边。kruskal算法构造最小生成树的时候也是一条边一条边地构造,但它的切分规则是不一样的。它每一次寻找的边会连接一片森林中的两棵树。如果一副加权无向图由V个顶点组成,初始化情况下每个顶点都构成一棵独立的树,则V个顶点对应V棵树,组成一片森林, kruskal算法每一次处理都会将两棵树合并为一棵树,直到整个森林中只剩一棵树为止。

image-20210830141905542

kruskal算法API设计

类名 KruskalMST
构造方法 KruskalMST(EdgeWeightedGraph G):根据一副加权无向图,创建最小生成树计算对象;
成员方法 KruskalMST(EdgeWeightedGraph G):根据一副加权无向图,创建最小生成树计算对象;
成员变量 1.private Queue mst:保存最小生成树的所有边
2.private UF_Tree_Weighted uf: 索引代表顶点,使用uf.connect(v,w)可以判断顶点v和顶点w是否在 同一颗树中,使用uf.union(v,w)可以把顶点v所在的树和顶点w所在的树合并
3.private MinPriorityQueue pq: 存储图中所有的边,使用最小优先队列,对边按照权重进行排序

kruskal算法的实现原理

在设计API的时候,使用了一个MinPriorityQueue pq存储图中所有的边,每次使用pq.delMin()取出权重最小的边,并得到该边关联的两个顶点v和w,通过uf.connect(v,w)判断v和w是否已经连通,如果连通,则证明这两个顶点在同一棵树中,那么就不能再把这条边添加到最小生成树中,因为在一棵树的任意两个顶点上添加一条边,都会形成环,而最小生成树不能有环的存在,如果不连通,则通过uf.connect(v,w)把顶点v所在的树和顶点w所在的树合并成一棵树,并把这条边加入到mst队列中,这样如果把所有的边处理完,最终mst中存储的就是最小生树的所有边。

image-20210830142248370

image-20210830142310895

image-20210830142345090

以此类推,最后就可以找到最小生成树。

image-20210830142422224

代码实现

/**
 * @author wen.jie
 * @date 2021/8/30 14:26
 * Kruskal算法
 */
public class KruskalMST {

    //保存最小生成树的所有边
    private Queue<Edge> mst;

    //索引代表顶点,使用uf.connect(v,w)可以判断顶点v和顶点w是否在 同一颗树中,使用uf.union(v,w)可以把顶点v所在的树和顶点w所在的树合并
    private UF_Tree_Weighted uf;

    //存储图中所有的边,使用最小优先队列,对边按照权重进行排序
    private MinPriorityQueue<Edge> pq;

    public KruskalMST(EdgeWeightedGraph G) {
        mst = new Queue<>();
        uf = new UF_Tree_Weighted(G.V());
        pq = new MinPriorityQueue<>(G.E());
        for (Edge edge : G.edges()) {
            pq.insert(edge);
        }

        kruskal(G);
    }

    private void kruskal(EdgeWeightedGraph G) {
        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))
                continue;
            uf.union(v, w);
            mst.enqueue(e);
        }
    }

    public Queue<Edge> edges() {
        return mst;
    }

}

测试:

    @Test
    public void test() {
        EdgeWeightedGraph G = new EdgeWeightedGraph(8);
        for (String str : strs) {
            String[] split = str.split(" ");
            Edge e = new Edge(Integer.parseInt(split[0]), Integer.parseInt(split[1]), Double.parseDouble(split[2]));
            G.addEdge(e);
        }
        KruskalMST kruskalMST = new KruskalMST(G);
        for (Edge edge : kruskalMST.edges()) {
            System.out.println(edge);
        }
    }

image-20210830143543821

本文所有代码均已上传:https://gitee.com/wj204811/algorithm

原文地址:https://www.cnblogs.com/wwjj4811/p/15205839.html