算法:通过普利姆(Prim)算法,求出图的最小生成树

22 篇文章 1 订阅
20 篇文章 0 订阅

请看如下的示例图,该图有 V1-V7 七个顶点,每个顶点之间的距离如图所示:

在这里插入图片描述

如果上面的图为七个城市的地理分布图,城市间相连的边上的数字为城市间的距离。我们要在这七个城市里面架设电线,使得每一个城市都在电网之中,怎么样架设电线,可以使得所使用的电线最短呢?

这就是图论里面的最小生成树的问题,今天我们用普利姆(Prim)算法,来求得电线架设的方案。话不多说,精髓全在代码和其间的注释里,可复制到IDE中运行:

1、图的顶点类,包含一到多个边:

import java.util.HashSet;
import java.util.Set;

/**
 * 图的顶点类
 * @Author: LiYang
 * @Date: 2019/9/8 21:32
 */
public class Vertex {

    //顶点的数据
    public String value;

    //顶点的边的集合(其相邻的顶点和权重)
    public Set<Edge> neighbors = new HashSet<>();

    /**
     * 空构造方法
     */
    public Vertex(){

    }

    /**
     * 构造方法,输入顶点值
     * @param value 顶点值
     */
    public Vertex(String value) {
        this.value = value;
    }

    /**
     * 为该顶点加入边
     * @param neighbor 边的另一顶点
     * @param weight 边的权重
     */
    public void addEdge(Vertex neighbor, int weight){
        neighbors.add(new Edge(neighbor, weight));
    }

    /**
     * 重新toString()方法,打印顶点的值
     * @return
     */
    @Override
    public String toString() {
        return value;
    }

}

2、图的边类,包含目的顶点,以及权重(距离):

/**
 * 图的边类,带权
 * Edge是属于某个顶点的,所以出发点是该顶点
 * Edge属性只有目的点,以及权重
 * @Author: LiYang
 * @Date: 2019/9/8 21:32
 */
public class Edge {

    //边的终点(邻居)
    public Vertex neighbor;

    //权重(距离)
    public int weight;

    /**
     * 无参构造
     */
    public Edge(){

    }

    /**
     * 全参数构造
     * @param neighbor 邻接点
     * @param weight 权重
     */
    public Edge(Vertex neighbor, int weight) {
        this.neighbor = neighbor;
        this.weight = weight;
    }

}

3、普利姆(Prim)算法表类,用于动态统计距离和顶点关系的表

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 普利姆(Prim)算法的表
 * @Author: LiYang
 * @Date: 2019/9/8 22:01
 */
public class PrimTable{

    //顶点
    public Vertex vertex;

    //是否已访问
    public boolean known;

    //距离
    public int distance;

    //上一顶点
    public Vertex previous;

    /**
     * 初始化普利姆(Prim)算法表
     * @param vList 顶点集合
     * @return 普利姆(Prim)算法表
     */
    public static Map<String, PrimTable> initialPrimTable(List<Vertex> vList){
        //最后返回的普利姆(Prim)算法表
        Map<String, PrimTable> primTableMap = new HashMap<>();

        //开始循环添加(默认第一个顶点为开始伸展的起点)
        for (Vertex vertex : vList){
            //当前节点对应的普利姆(Prim)算法表的行
            PrimTable primTable = new PrimTable();

            //顶点
            primTable.vertex = vertex;

            //初始化为未访问
            primTable.known = false;

            //初始化距离为无穷大
            primTable.distance = Integer.MAX_VALUE;

            //上一顶点初始化置空
            primTable.previous = null;

            //以顶点名为键,放入Map中
            primTableMap.put(vertex.value, primTable);
        }

        //返回初始化好的普利姆(Prim)算法表
        return primTableMap;
    }

    /**
     * 寻找普利姆(Prim)算法表中距离最小的value的key
     * @param primTableMap 待查找的普利姆(Prim)算法表
     * @return 普利姆(Prim)算法表最小距离的key
     */
    public static String getMinimumPrimTableKey(Map<String, PrimTable> primTableMap){
        //最小距离
        int minimunDistance = Integer.MAX_VALUE;

        //第一次遍历普利姆(Prim)算法表,找出最小值
        for (Map.Entry<String, PrimTable> item : primTableMap.entrySet()){
            //如果已经访问过,则跳过
            if (item.getValue().known){
                continue;
            }

            //得到当前的距离
            int currentDistance = item.getValue().distance;

            //更新最小值
            if (minimunDistance > currentDistance){
                minimunDistance = currentDistance;
            }
        }

        //根据找到的最小值,找到最小值的key
        for (Map.Entry<String, PrimTable> item : primTableMap.entrySet()){
            //如果已经访问过,则跳过
            if (item.getValue().known){
                continue;
            }

            //得到当前的距离
            int currentDistance = item.getValue().distance;

            //如果当前距离就是最小距离,则返回其key
            if (currentDistance == minimunDistance){
                return item.getKey();
            }
        }

        //如果找不到,则表示普利姆(Prim)算法终止
        //因为算法while循环中加入了判断,所以不会到这里
        return "PrimFinished";
    }

    /**
     * 重新toString()方法,方便调试查看数据
     * @return
     */
    @Override
    public String toString() {
        return "PrimTable{" +
                "vertex=" + vertex +
                ", known=" + known +
                ", distance=" + distance +
                ", previous=" + previous +
                '}';
    }

}

4、图类,里面包含创建上面示例图的方法,以及普利姆(Prim)算法求最小生成树的方法,还有运行的main方法:

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * 数据结构与算法之:图
 * @Author: LiYang
 * @Date: 2019/9/8 21:31
 */
public class Graph {

    /**
     * 构建普利姆(Prim)算法所需要的图(本文刚开始的示例图)
     * 注意,普利姆(Prim)算法需要连通图
     * @return 图的顶点的集合
     */
    public static List<Vertex> buildPrimGraph(){
        //创建顶点
        Vertex vertex1 = new Vertex("V1");
        Vertex vertex2 = new Vertex("V2");
        Vertex vertex3 = new Vertex("V3");
        Vertex vertex4 = new Vertex("V4");
        Vertex vertex5 = new Vertex("V5");
        Vertex vertex6 = new Vertex("V6");
        Vertex vertex7 = new Vertex("V7");

        //为顶点创建边
        vertex1.addEdge(vertex2, 2);
        vertex1.addEdge(vertex3, 4);
        vertex1.addEdge(vertex4, 1);

        vertex2.addEdge(vertex1, 2);
        vertex2.addEdge(vertex4, 3);
        vertex2.addEdge(vertex5, 10);

        vertex3.addEdge(vertex1, 4);
        vertex3.addEdge(vertex4, 2);
        vertex3.addEdge(vertex6, 5);

        vertex4.addEdge(vertex1, 1);
        vertex4.addEdge(vertex2, 3);
        vertex4.addEdge(vertex3, 2);
        vertex4.addEdge(vertex5, 7);
        vertex4.addEdge(vertex6, 8);
        vertex4.addEdge(vertex7, 4);

        vertex5.addEdge(vertex2, 10);
        vertex5.addEdge(vertex4, 7);
        vertex5.addEdge(vertex7, 6);

        vertex6.addEdge(vertex3, 5);
        vertex6.addEdge(vertex4, 8);
        vertex6.addEdge(vertex7, 1);

        vertex7.addEdge(vertex4, 4);
        vertex7.addEdge(vertex5, 6);
        vertex7.addEdge(vertex6, 1);

        //所有顶点的集合
        List<Vertex> vList = new ArrayList<>();
        vList.add(vertex1);
        vList.add(vertex2);
        vList.add(vertex3);
        vList.add(vertex4);
        vList.add(vertex5);
        vList.add(vertex6);
        vList.add(vertex7);

        //返回所有顶点
        return vList;
    }

    /**
     * 普利姆(Prim)算法,求最小生成树的边
     * @param vList 图的顶点的集合
     */
    public static void primAlgorithm(List<Vertex> vList){
        //先生成该顶点集合的普利姆(Prim)算法表
        Map<String, PrimTable> primTable = PrimTable.initialPrimTable(vList);

        //我们从第一个顶点开始遍历,则先把普利姆(Prim)算法表的第一个顶点的距离设置为0
        primTable.get(vList.get(0).value).distance = 0;

        //重要:普利姆(Prim)算法
        while (isNotAllKnown(primTable)){

            //寻找普利姆(Prim)算法表中最小距离的key(known为false中寻找最小)
            String minimumKey = PrimTable.getMinimumPrimTableKey(primTable);

            //先将该顶点标记为known
            primTable.get(minimumKey).known = true;

            //获得该顶点
            Vertex currentVertex = primTable.get(minimumKey).vertex;

            //访问该顶点的所有有边相邻的邻居
            for (Edge item : currentVertex.neighbors){
                //获得当前邻居的value
                String neighborKey = item.neighbor.value;

                //如果该邻居顶点已被访问,则跳过
                if (primTable.get(neighborKey).known){
                    continue;
                }

                //如果该邻居顶点没有被访问,则先拿出普利姆(Prim)算法表里面的距离
                int primDistance = primTable.get(neighborKey).distance;

                //拿出到邻居路径的权重
                int neighborDistance = item.weight;

                //如果当前路径,也就是邻居路径小于普利姆(Prim)算法表里面的距离
                if (neighborDistance < primDistance){

                    //需要更新邻居普利姆(Prim)算法表里面的距离
                    primTable.get(neighborKey).distance = neighborDistance;

                    //然后普利姆(Prim)算法表里面的上一顶点,更新为当前顶点
                    primTable.get(neighborKey).previous = currentVertex;
                }
            }
        }

        //上面的while循环结束后,普利姆(Prim)算法即终止,解读最终生成的普利姆(Prim)算法表
        printNeedConnectedEdgeAndDistance(primTable);
    }

    /**
     * 检查图的所有顶点是否全部已访问
     * @param primTable 待检查的普利姆(Prim)算法表
     * @return 是否不是全部已访问
     */
    private static boolean isNotAllKnown(Map<String, PrimTable> primTable){
        //将普利姆(Prim)算法表遍历
        for (Map.Entry<String, PrimTable> item : primTable.entrySet()){

            //如果找到了一个known为false的点
            if (!item.getValue().known){
                //证明不是全known,返回true
                return true;
            }
        }

        //遍历完了,还没有找到known为false的点,则证明全部known
        return false;
    }

    /**
     * 解读最后完成的普利姆(Prim)算法表,打印得到的最小生成树需要连接的点,以及总距离和其他信息
     * @param primTable 最后完成的普利姆(Prim)算法表
     */
    public static void printNeedConnectedEdgeAndDistance(Map<String, PrimTable> primTable){
        //最小生成树总距离
        long totalDistance = 0;

        //将最后完成的普利姆(Prim)算法表遍历
        for (Map.Entry<String, PrimTable> item : primTable.entrySet()){
            //如果previous是null,则跳过
            if (item.getValue().previous == null){
                continue;
            }

            //打印出需要连接的两个顶点,以及该连接的距离
            System.out.println("需要连接的两个顶点:" + item.getValue().previous + " -> " + item.getValue().vertex + ",连接距离为:" + item.getValue().distance);

            //累加到总距离
            totalDistance = totalDistance + item.getValue().distance;
        }

        //最后,打印出最小生成树的总距离
        System.out.println("该图的最小生成树的总距离:" + totalDistance);
    }


    /**
     * 运行普利姆(Prim)算法,求出示例图的最小生成树
     * @param args
     */
    public static void main(String[] args) {
        //构建示例图
        List<Vertex> vList = buildPrimGraph();

        //运行普利姆(Prim)算法,需要再控制台查看打印结果
        primAlgorithm(vList);
    }
    
}

5、运行main方法,控制台输出最小生成树的构建方法,以及总距离(城市间电线的架设方案,以及架设电线总长度):

需要连接的两个顶点:V7 -> V6,连接距离为:1
需要连接的两个顶点:V4 -> V7,连接距离为:4
需要连接的两个顶点:V1 -> V2,连接距离为:2
需要连接的两个顶点:V4 -> V3,连接距离为:2
需要连接的两个顶点:V1 -> V4,连接距离为:1
需要连接的两个顶点:V7 -> V5,连接距离为:6
该图的最小生成树的总距离:16

6、根据上面的方案,我们连接 V7 -> V6、V4 -> V7、V1 -> V2、V4 -> V3、V1 -> V4、V7 -> V5 六条边,得到下面的图,下面的图就是上面示例图的最小生成树,也就是架设电线的最优方案,总距离为16个单位(方案不一定唯一,但总距离一定是最小的,你找不到总距离比下图还小的电线架设方案了):
在这里插入图片描述
7、我们回顾之前的示例图,方便作对比查看:
在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值