加权无向图与最小生成树(Prim算法和Kruskal算法)

本文详细介绍了Prim算法和Kruskal算法,用于在加权无向图中找到最小生成树,包括算法原理、步骤、API设计与Java实现。通过贪心策略对比两种求解最小生成树的方法。

0 引入

通过加权无向图结合最小生成树相关算法,可以解决最小成本问题,并找到最小成本对应的顶点和边。

1 图的最小生成树定义及相关约定

图的生成树:图的生成树是它的一个含有其所有顶点的无环连通子图。

图的最小生成树:图的权值(所有边的权重之和)最小的生成树。

在这里插入图片描述
约定:最小生成树只存在于连通图中,如果图不是连通的,那么分别计算每个连通子图的最小生成树,再合并到一起形成最小生成森林。

在这里插入图片描述
同时为了便于理解,约定所有边的权重都不相同(如果有权重相同的边,最小生成树可能就不唯一了)。

2 最小生成树原理

2.1 性质

  1. 用一条边连接树中的任意两个顶点都会产生环;
    在这里插入图片描述

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

在这里插入图片描述

2.2 切分定理

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

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

横切边:连接两个不同集合的顶点的边称为横切边;

例如将下图中的顶点切分为两个集合,灰色顶点属于一个集合,白色顶点属于另一个集合。

在这里插入图片描述

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

在这里插入图片描述

3 贪心思想

  1. 贪心算法是计算图的最小生成树的基础算法;
  2. 先利用切分定理找到最小生成树的一条边,然后不断重复直到找到最小生成树的所有边;
  3. 如果图有V个顶点,那么需要找到V-1条边,就可以表示该图的最小生成树;

在这里插入图片描述

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

下面介绍Prim算法和kruskal算法。

4 Prim算法

4.1 算法步骤

  1. 将最小生成树中的顶点作为一个集合,非最小生成树中的顶点作为另外一个集合。1 索引最小优先队列存储所有的横切边;2 edgeTo数组存储当前顶点和最小生成树之间的最短边;3 distTo数组表示当前顶点和最小生成树之间的最短边的权重,初始值为无穷大;4 marked数组标识当前顶点是否在最小生成树中,如果在,值为true,如果不在,值为false;
  2. 选取起点0,令distTo[0] = 0.0,并将该点加入到索引最小优先队列中pq.insert(0, 0.0);
  3. 调用索引最小优先队列的delMin(),将当下最小横切边对应的顶点加入最小生成树的集合中(标记该顶点:marked数组中该顶点值设为true)。同时遍历该顶点的邻接表,更新edgeTo和distTo,并新增或更新横切边(更新索引最小优先队列);
  4. 重复3,直到索引最小优先队列为空,最终edgeTo数组里存储的就是最小生成树的边;

  1. 先默认0是最小生成树中的唯一顶点,其他顶点都不在最小生成树中,此时横切边就是顶点0的邻接表中0-2,0-4,0-6,0-7这四条边,于是索引优先队列的2、4、6、7索引处分别存储这些边的权重值;

在这里插入图片描述

  1. 从这四条横切边中找出权重最小的边,然后把对应的顶点加进来:由于0-7这条横切边的权重最小,因此把顶点7加进来;
  2. 于是顶点7的邻接表中的边也成为了横切边,而0-7已经不是横切边了,让它失效:已经调用了最小索引优先队列的delMin()方法;
  3. 2和4各有两条横切边指向最小生成树,各保留一条:4-7权重小于0-4权重,保留4-7,调用索引优先队列的changeItem(4,0.37);0-2权重小于2-7权重,保留0-2,不需额外操作;

在这里插入图片描述

  1. 不断重复上面的动作,就可以把所有的顶点添加到最小生成树中;

4.2 API设计

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

4.3 Java代码演示

import java.util.Deque;
import java.util.LinkedList;

public class PrimMST {
    //Edge数组表示当前顶点和最小生成树之间的最短边
    private Edge[] edgeTo;
    //distTo数组表示当前顶点和最小生成树之间的最短边的权重,初始值为无穷大
    private double[] distTo;
    //marked数组标识当前顶点是否在最小生成树中,如果在,值为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()];
        for (int i = 0; i < distTo.length; i++) {
            distTo[i] = Double.POSITIVE_INFINITY;
        }
        this.marked = new boolean[G.V()];
        pq = new IndexMinPriorityQueue<>(G.V());

        //默认让顶点0进入树中,但0顶点目前没有与树中其他的顶点相连接,因此初始化distTo[0]=0.0
        distTo[0] = 0.0;
        //使用顶点0和权重0初始化pq
        pq.insert(0,0.0);

        //遍历有效边队列
        while (!pq.isEmpty()){
            //找到权重最小的横切边对应的顶点,加入到最小生成树中
            visit(G,pq.delMin());
        }
    }

    //将顶点v添加到最小生成树中,并且更新数据
    private void visit(EdgeWeightedGraph G, int v) {
        //把顶点v添加到最小生成树中
        marked[v] = true;
        //更新数据
        for (Edge e : G.adj(v)) {
            //获取e边的另外一个顶点(当前顶点是v)
            int w = e.other(v);
            //判断另外一个顶点是不是已经在树中,如果在树中,则不做任何处理,如果不在树中,更新数据
            if (marked[w]){
                continue;
            }
            //如果v-w边e的权重比目前distTo[w]权重小,则需要修正数据
            if (e.weight() < distTo[w]){
                //把顶点w距离最小生成树的边修改为e
                edgeTo[w] = e;
                //把顶点w距离最小生成树的边的权重修改为e.weight()
                distTo[w] = e.weight();
                //如果pq中存储的有效横切边已经包含了w顶点,则需要修正最小索引优先队列w索引关联的权重值
                if (pq.contains(w)){
                    pq.changeItem(w, e.weight());
                }else{//如果pq中存储的有效横切边不包含w顶点,则需要向最小索引优先队列中添加v-w和其权重值
                    pq.insert(w,e.weight());
                }
            }
        }
    }

    //获取最小生成树的所有边
    public Deque<Edge> edges() {
        //创建队列对象
        Deque<Edge> allEdges = new LinkedList<>();
        //遍历edgeTo数组,拿到每一条边,如果不为null,则添加到队列中
        for (int i = 0; i < edgeTo.length; i++) {
            if (edgeTo[i]!=null){
                allEdges.offer(edgeTo[i]);
            }
        }
        return allEdges;
    }
}
public class Test {
    public static void main(String[] args) {
        EdgeWeightedGraph g = new EdgeWeightedGraph(8);
        Edge e1 = new Edge(4,5,0.35); Edge e2 = new Edge(4,7,0.37);
        Edge e3 = new Edge(5,7,0.28); Edge e4 = new Edge(0,7,0.16);
        Edge e5 = new Edge(1,5,0.32); Edge e6 = new Edge(0,4,0.38);
        Edge e7 = new Edge(2,3,0.17); Edge e8 = new Edge(1,7,0.19);
        Edge e9 = new Edge(0,2,0.26); Edge e10 = new Edge(1,2,0.36);
        Edge e11 = new Edge(1,3,0.29); Edge e12 = new Edge(2,7,0.34);
        Edge e13 = new Edge(6,2,0.40); Edge e14 = new Edge(3,6,0.52);
        Edge e15 = new Edge(6,0,0.58); Edge e16 = new Edge(6,4,0.93);

        g.addEdge(e1);g.addEdge(e2);g.addEdge(e3);g.addEdge(e4);
        g.addEdge(e5);g.addEdge(e6);g.addEdge(e7);g.addEdge(e8);
        g.addEdge(e9);g.addEdge(e10);g.addEdge(e11);g.addEdge(e12);
        g.addEdge(e13);g.addEdge(e14);g.addEdge(e15);g.addEdge(e16);

        //构建PrimMST对象
        PrimMST mst = new PrimMST(g);
        //获取最小生成树的边
        Deque<Edge> edges = mst.edges();
        //打印输出
        for (Edge edge : edges) {
            System.out.println(edge.either() + "-" + edge.other(edge.either()) + ":" + edge.weight());
        }
    }
}
1-7:0.19
0-2:0.26
2-3:0.17
4-5:0.35
5-7:0.28
6-2:0.4
0-7:0.16

注:

5 Kruskal算法

Kruskal算法是另外一种计算加权无向图的最小生成树的算法。

5.1 算法步骤

  1. 使用最小优先队列PriorityQueue< Edge > pq = new PriorityQueue<>();存储所有边,每次使用pq.delMin()取出权重最小的边,并得到该边关联的两个顶点v和w;
  2. 通过并查集UF_Tree_Weighted uf = new UF_Tree_Weighted(G.V()),调用uf.connect(v,w)判断v和w是否已经连通,如果连通,则证明这两个顶点在同一棵树中,那么就不能把这条边添加到最小生成树中,因为在最小生成树的任意两个顶点上添加一条边,都会形成环,而最小生成树不能有环的存在;如果不连通,则通过uf.connect(v,w)把顶点v所在的树和顶点w所在的树合并成一棵树,并把这条边加入到Deque< Edge > mst队列中;
  3. 直到mst队列中含有V-1条边,最终mst中存储的就是最小生成树的所有边;

5.1 API设计

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.2 Java代码演示

import java.util.Deque;
import java.util.LinkedList;
import java.util.PriorityQueue;

public class KruskalMST {
    //使用队列保存最小生成树的所有边
    private Deque<Edge> mst;
    //使用并查集,索引代表顶点,uf.connect(v,w)可以判断顶点v和顶点w是否在同一颗树中,使用uf.union(v,w)可以把顶点v所在的树和顶点w所在的树合并
    private UF_Tree_Weighted uf;
    //存储图中所有的边,使用最小优先队列,对边按照权重排序
    private PriorityQueue<Edge> pq;

    //根据一副加权无向图,创建最小生成树计算对象
    public KruskalMST(EdgeWeightedGraph G) {
        //初始化mst
        this.mst = new LinkedList<>();
        //初始化uf
        this.uf = new UF_Tree_Weighted(G.V());
        //初始化pq
        this.pq = new PriorityQueue<>();
        //把图中所有的边存储到pq中
        for (Edge e : G.edges()) {
            pq.offer(e);
        }
        //遍历pq队列,拿到最小权重的边,进行处理
        while(!pq.isEmpty() && mst.size() < G.V()-1){
            //找到权重最小的边
            Edge e = pq.poll();
            //找到该边的两个顶点
            int v = e.either();
            int w = e.other(v);

            //判断这两个顶点是否已经在同一颗树中,如果在同一颗树中,则不对该边做处理,如果不在一棵树中,则让这两个顶点属于的两棵树合并成一棵树
            if (uf.connected(v,w)){
                continue;
            }
            uf.union(v,w);
            //让边e进入到mst队列中
            mst.offer(e);
        }
    }
    //获取最小生成树的所有边
    public Deque<Edge> edges() {
        return mst;
    }
}
import java.util.Deque;

public class Test {
    public static void main(String[] args) {
        EdgeWeightedGraph g = new EdgeWeightedGraph(8);
        Edge e1 = new Edge(4,5,0.35); Edge e2 = new Edge(4,7,0.37);
        Edge e3 = new Edge(5,7,0.28); Edge e4 = new Edge(0,7,0.16);
        Edge e5 = new Edge(1,5,0.32); Edge e6 = new Edge(0,4,0.38);
        Edge e7 = new Edge(2,3,0.17); Edge e8 = new Edge(1,7,0.19);
        Edge e9 = new Edge(0,2,0.26); Edge e10 = new Edge(1,2,0.36);
        Edge e11 = new Edge(1,3,0.29); Edge e12 = new Edge(2,7,0.34);
        Edge e13 = new Edge(6,2,0.40); Edge e14 = new Edge(3,6,0.52);
        Edge e15 = new Edge(6,0,0.58); Edge e16 = new Edge(6,4,0.93);

        g.addEdge(e1);g.addEdge(e2);g.addEdge(e3);g.addEdge(e4);
        g.addEdge(e5);g.addEdge(e6);g.addEdge(e7);g.addEdge(e8);
        g.addEdge(e9);g.addEdge(e10);g.addEdge(e11);g.addEdge(e12);
        g.addEdge(e13);g.addEdge(e14);g.addEdge(e15);g.addEdge(e16);

        //构建PrimMST对象
        KruskalMST mst = new KruskalMST(g);
        //获取最小生成树的边
        Deque<Edge> edges = mst.edges();
        //打印输出
        for (Edge edge : edges) {
            System.out.println(edge.either() + "-" + edge.other(edge.either()) + ":" + edge.weight());
        }
    }
}
0-7:0.16
2-3:0.17
1-7:0.19
0-2:0.26
5-7:0.28
4-5:0.35
6-2:0.4

注:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hellosc01

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值