算法17 十大之-普里姆+克鲁斯卡尔+迪杰斯特拉

1,619 阅读14分钟

6.普里姆算法

应用场景-修路问题

看一个应用场景和问题:
61

  1. 有胜利乡有7个村庄(A,B,C,D,E,F,G),现在需要修路把7个村庄连通
  2. 各个村庄的距离用边线表示(权) ,比如 A – B 距离 5 公里
  3. 问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短?
    思路: 将 10 条边,连接即可,但是总的里程数不是最小. 正确的思路,就是尽可能的选择少的路线,并且每条路线最小,保证总里程数最少.

最小生成树

修路问题本质就是就是最小生成树问题, 先介绍一下最小生成树(Minimum Cost Spanning Tree),简称 MST。 给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树

  1. N 个顶点,一定有 N-1 条边
  2. 包含全部顶点
  3. N-1 条边都在图中
  4. 举例说明(如图:)
  5. 求最小生成树的算法主要是普里姆算法和克鲁斯卡尔算法 62

介绍

普利姆(Prim)算法求最小生成树,也就是在包含 n 个顶点的连通图中,找出只有(n-1)条边包含所有 n 个顶点的 连通子图,也就是所谓的极小连通子图
普利姆的算法如下:

  1. 设G=(V,E)是连通网,T=(U,D)是最小生成树,V,U是顶点集合,E,D是边的集合
  2. 若从顶点u开始构造最小生成树,则从集合V中取出顶点u放入集合U中,标记顶点v的visited[u]=1
  3. 若集合U中顶点ui与集合V-U中的顶点vj之间存在边,则寻找这些边中权值最小的边,但不能构成回路,将顶点 vj 加入集合 U 中,将边(ui,vj)加入集合 D 中,标记 visited[vj]=1
  4. 重复步骤2,直到U与V相等,即所有顶点都被标记为访问过,此时D中有n-1条边
  5. 提示: 单独看步骤很难理解,我们通过代码来讲解,比较好理解.
  6. 图解普利姆算法

代码

  1. 有胜利乡有7个村庄(A,B,C,D,E,F,G),现在需要修路把7个村庄连通
  2. 各个村庄的距离用边线表示(权) ,比如 A – B 距离 5 公里
  3. 问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短?
/**
 * @author DSH
 * @date 2020/10/9
 * @description 普利姆算法
 */
public class PrimAlgorithm {
    public static void main(String[] args) {
        //测试图是否创建成功
        char[] data = new char[]{'A','B','C','D','E','F','G'};
        int verxs = data.length;
        //邻接矩阵的关系使用二维数组表示,10000表示两个点不连通
        int [][] weight = new int[][]{
                {10000,5,7,10000,10000,10000,2},
                {5,10000,10000,9,10000,10000,3},
                {7,10000,10000,10000,8,10000,10000},
                {10000,9,10000,10000,10000,4,10000},
                {10000,10000,8,10000,10000,5,4},
                {10000,10000,10000,4,5,10000,6},
                {2,3,10000,10000,4,6,10000}};

        //创建一个MGraph对象
        MGraph mGraph = new MGraph(verxs);
        //创建最小生成树对象
        MinTree minTree = new MinTree();
        minTree.createGraph(mGraph,verxs,data,weight);
        //输出
        minTree.showGraph(mGraph);

        //测试普里姆算法
        minTree.prim(mGraph,0);//<A,G,B,E,F,D,C>
    }
}

//创建最小生成树->村庄的图
class MinTree{
    //创建图的邻接矩阵
    /**
     *
     * @param graph 图对象
     * @param verxs 图对应的顶点个数
     * @param data  图的各个定点的值
     * @param weight 图的邻接矩阵
     */
    public void createGraph(MGraph graph,int verxs, char data[], int[][] weight){
        int i,j;
        for (i = 0; i < verxs; i++) {//顶点
            graph.data[i] = data[i];
            for (j = 0; j < verxs; j++) {
                graph.weight[i][j] = weight[i][j];
            }
        }
    }

    //显示图的邻接矩阵
    public void showGraph(MGraph graph){
        for (int[] link: graph.weight) {
            System.out.println(Arrays.toString(link));
        }
    }

    //编写prim算法, 得到最小生成树
    /**
     *
     * @param graph 图
     * @param v     表示从图的第几个顶点开始生成
     */
    public void prim(MGraph graph,int v){
        //1. 标记节点(顶点)是否被访问
        int[] visited = new int[graph.verxs];
        //visited[] 默认元素的值都是0,表示没有访问过
        //把当前节点标记为已访问
        visited[v] = 1;
        //h1 和 h2 记录两个顶点的下标
        int h1 = -1;
        int h2 = -1;
        int minWeight = 10000;//将minWeight初始为一个大数,遍历过程中, 会被替换
        for (int k = 1; k < graph.verxs; k++) {//因为有graph.verxs个顶点, 普里姆算法结束后,有graph.verxs-1条边

            //这个是确定每一次生成的子图和哪个此次遍历的节点的距离最近
            for (int i = 0; i < graph.verxs; i++) {//i节点表示被访问过的节点
                for (int j = 0; j < graph.verxs; j++) {// j表示还没有访问过的节点
                    if (visited[i]==1 && visited[j]==0 && graph.weight[i][j]<minWeight){
                        //替换minWeight(寻找已经访问过的节点和未访问过的节点间的权值最小的边)
                        minWeight = graph.weight[i][j];
                        h1 = i;
                        h2 = j;
                    }
                }
            }
            //退出此次for循环,找到了一条边是最小
            System.out.println("边<" + graph.data[h1] + "," + graph.data[h2] + "> 权值:"+minWeight);
            //将当前节点标记为已经访问过了
            visited[h2] = 1;
            //重置minWeight
            minWeight = 10000;
        }

    }

}

//创建邻接矩阵 图
class MGraph{
    int verxs;//表示图的节点个数
    char[] data;//保存节点数据
    int[][] weight;//存放边,就是我们的邻接矩阵

    public MGraph(int verxs) {
        this.verxs = verxs;
        data = new char[verxs];
        weight = new int[verxs][verxs];
    }

}

输出

边<A,G> 权值:2
边<G,B> 权值:3
边<G,E> 权值:4
边<E,F> 权值:5
边<F,D> 权值:4
边<A,C> 权值:7

7.克鲁斯卡尔算法

应用场景-公交站问题

看一个应用场景和问题:

  1. 某城市新增7个站点(A,B,C,D,E,F,G),现在需要修路把7个站点连通
  2. 各个站点的距离用边线表示(权) ,比如 A – B 距离 12 公里
  3. 问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短?

克鲁斯卡尔算法介绍

  1. 克鲁斯卡尔(Kruskal)算法,是用来求加权连通图的最小生成树的算法。
  2. 基本思想:按照权值从小到大的顺序选择n-1条边,并保证这n-1条边不构成回路
  3. 具体做法:首先构造一个只含n个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止

克鲁斯卡尔算法图解说明

以城市公交站问题来图解说明 克鲁斯卡尔算法的原理和步骤:

在含有 n 个顶点的连通图中选择 n-1条边,构成一棵极小连通子图,并使该连通子图中 n-1 条边上权值之和达到 最小,则称其为连通网的最小生成树。 72
例如,对于如上图 G4 所示的连通网可以有多棵权值总和不相同的生成树。

克鲁斯卡尔算法图解

以上图 G4 为例,来对克鲁斯卡尔进行演示(假设,用数组 R 保存最小生成树结果)。

  • 第 1 步:将边<E,F>加入 R 中。 边<E,F>的权值最小,因此将它加入到最小生成树结果 R 中。
  • 第 2 步:将边<C,D>加入 R 中。 上一步操作之后,边<C,D>的权值最小,因此将它加入到最小生成树结果 R 中。
  • 第 3 步:将边<D,E>加入 R 中。 上一步操作之后,边<D,E>的权值最小,因此将它加入到最小生成树结果 R 中。
  • 第 4 步:将边<B,F>加入 R 中。 上一步操作之后,边<C,E>的权值最小,但<C,E>会和已有的边构成回路;因此,跳过边<C,E>。同理,跳 过边<C,F>。将边<B,F>加入到最小生成树结果 R 中。
  • 第 5 步:将边<E,G>加入 R 中。 上一步操作之后,边<E,G>的权值最小,因此将它加入到最小生成树结果 R 中。
  • 第 6 步:将边<A,B>加入 R 中。 上一步操作之后,边<F,G>的权值最小,但<F,G>会和已有的边构成回路;因此,跳过边<F,G>。同理,跳 过边<B,C>。将边<A,B>加入到最小生成树结果 R 中。

此时,最小生成树构造完成!它包括的边依次是:<E,F> <C,D> <D,E> <B,F> <E,G> <A,B>。

克鲁斯卡尔算法分析

根据前面介绍的克鲁斯卡尔算法的基本思想和做法,我们能够了解到,克鲁斯卡尔算法重点需要解决的以下两个问题:
问题一 对图的所有边按照权值大小进行排序。
问题二 将边添加到最小生成树中时,怎么样判断是否形成了回路。
问题一很好解决,采用排序算法进行排序即可。
问题二,处理方式是:记录顶点在"最小生成树"中的终点,顶点的终点是"在最小生成树中与它连通的最大顶点"。 然后每次需要将一条边添加到最小生存树时,判断该边的两个顶点的终点是否重合,重合的话则会构成回路。

如 何 判 断 是 否 构 成 回 路 -举 例 说 明 (如 图 )

75

在将<E,F> <C,D> <D,E>加入到最小生成树 R 中之后,这几条边的顶点就都有了终点:
(01) C 的终点是 F
(02) D 的终点是 F
(03) E 的终点是 F
(04) F 的终点是 F

关于终点的说明:

  1. 就是将所有顶点按照从小到大的顺序排列好之后;某个顶点的终点就是"与它连通的最大顶点"。
  2. 因此,接下来,虽然<C,E>是权值最小的边。但是 C 和 E 的终点都是 F,即它们的终点相同,因此,将<C,E> 加入最小生成树的话,会形成回路。这就是判断回路的方式。也就是说,我们加入的边的两个顶点不能都指向同一 个终点,否则将构成回路。【后面有代码说明】

克鲁斯卡尔最佳实践-公交站问题

看一个公交站问题:

  1. 有北京有新增7个站点(A,B,C,D,E,F,G),现在需要修路把7个站点连通
  2. 各个站点的距离用边线表示(权) ,比如 A – B 距离 12 公里
  3. 问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短?
  4. 代码实现和注解
/**
 * @author DSH
 * @date 2020/10/9
 * @description 克鲁斯卡尔算法解决公交站问题
 */
public class KruskaCase {

    private int edgeNum;//边的个数
    private char[] vertexs;//顶点数组
    private int[][] martix;//邻接矩阵
    //使用INF 表示两个顶点不能连通
    private static final int INF = Integer.MAX_VALUE;

    public static void main(String[] args) {
        char[] vertexts = {'A','B','C','D','E','F','G'};
        //克鲁斯卡尔算法的邻接矩阵
        int matrix[][] = {
                /*A*/ {0, 12, INF, INF, INF, 16, 14},
                /*B*/ {12, 0, 10, INF, INF, 7, INF},
                /*C*/ { INF, 10, 0, 3, 5, 6, INF},
                /*D*/ {INF, INF, 3, 0, 4, INF, INF},
                /*E*/ {INF, INF, 5, 4, 0, 2, 8},
                /*F*/ {16, 7, 6, INF, 2, 0, 9},
                /*G*/ {14, INF, INF, INF, 8, 9, 0}
        };
        //创建KruskaCase对象实例
        KruskaCase kruskaCase = new KruskaCase(vertexts, matrix);
        //输出构建的矩阵
        kruskaCase.print();

        //排序测试代码
//        EData[] edges = kruskaCase.getEdges();
//        System.out.println("排序前"+Arrays.toString(edges));
//        kruskaCase.sortEdges(edges);
//        System.out.println("排序后"+Arrays.toString(edges));

        kruskaCase.kruskal();

    }

    //构造器
    public KruskaCase(char[] vertexs, int[][] martix) {
        //初始化顶点数和边的个数
        int vlen = vertexs.length;
        //初始化顶点,复制拷贝的方式, 用for循环是为了不修改传入的vertexs
        this.vertexs = new char[vlen];
        for (int i = 0; i < vlen; i++) {
            this.vertexs[i] = vertexs[i];
        }
        //初始化边
        this.martix = new int[vlen][vlen];
        for (int i = 0; i < vlen; i++) {
            for (int j = 0; j < vlen; j++) {
                this.martix[i][j] = martix[i][j];
            }
        }
        //统计边
        for (int i = 0; i < vlen; i++) {
            for (int j = i+1; j < vlen; j++) {
                if (this.martix[i][j]!=INF){
                    edgeNum++;
                }
            }
        }
    }



    //对边进行排序,冒泡排序
    private void sortEdges(EData[] edges){
        for (int i = 0; i < edges.length-1; i++) {
            for (int j = 0; j < edges.length-1-i; j++) {
                if (edges[j].weight>edges[j+1].weight){
                    EData temp = edges[j];
                    edges[j] = edges[j+1];
                    edges[j+1] = temp;
                }
            }
        }
    }

    /**
     *
     * @param ch 传入的顶点的值 比如'A', 'B'等
     * @return 返回ch顶点对应的下标, 如果找不到,返回-1
     */
    private int getPosition(char ch){
        for (int i = 0; i < vertexs.length; i++) {
            if (vertexs[i]==ch){
                return i;
            }
        }
        return -1;
    }

    /**
     * 获取图中的边放到EData[] 数组中, 后面需要遍历该数组
     * 是通过matrix邻接矩阵来获取
     * EData[] 形式 [{'A','B','12'}...]
     * @return
     */
    private EData[] getEdges(){
        int index = 0;
        EData[] edges = new EData[edgeNum];
        for (int i = 0; i < vertexs.length; i++) {
            for (int j = i+1; j < vertexs.length; j++) {
                if (martix[i][j]!=INF){
                    edges[index++] = new EData(vertexs[i],vertexs[j],martix[i][j]);
                }
            }
        }
        return edges;
    }

    /**
     * 获取下标为i的顶点的终点,用于后面判断两个顶点的终点是否相同
     * @param ends 数组记录了各个顶点对应的终点是哪个, ends 数组是在遍历过程中,逐步形成
     * @param i 表示传入的顶点对应的下标
     * @return 返回的就是下标为i的这个顶点对应的终点的下标
     */
    private int getEnd(int[] ends,int i){
        while (ends[i]!=0){
            i = ends[i];
        }
        return i;
    }

    //
    public void kruskal(){
        int index = 0;//表示最后结果数组的索引
        int ends[] = new int[edgeNum];//用于保存"已有最小生成树"中的每个顶点在最小生成树中的终点
        //创建结果数组,保存最后的最小生成树
        EData[] rets = new EData[edgeNum];

        //获取图中所有的边的集合, 一共有12条边
        EData[] edges = getEdges();

        //按照边的权值大小进行排序(从小到大)
        sortEdges(edges);
        //遍历edges数组,将边添加到最小生成树中,判断是准备加入的边是否形成了回路,如果没有,就加入到rets,否则不能加入
        for (int i = 0; i < edgeNum; i++) {
            //获取到第i条边的第一个顶点(起点)
            int p1 = getPosition(edges[i].start);// E : 4
            //获取到第i条边的第二个顶点(终点)
            int p2 = getPosition(edges[i].end);// F : 5
            //获取p1这个顶点在已有的最小生成树中的终点是哪一个
            int m = getEnd(ends,p1);
            //获取p2这个顶点在已有的最小生成树中的终点是哪一个
            int n = getEnd(ends,p2);
            //判断是否构成回路
            if (m != n){//没有构成回路
                ends[m] = n;//设置m在已有最小生成树中的终点 <E,F> [0,0,0,0,0,0,0,0,0,0,0,0] => [0,0,0,0,5,0,0,0,0,0,0,0] (E的终点是F)
                rets[index++] = edges[i];//有一条边加入到rets数组
            }
        }

        //统计并打印"最小生成树",输出rets
        System.out.println("最小生成树为");
        for (int i = 0; i < index; i++) {
            System.out.println(rets[i]);
        }
//        System.out.println("最小生成树为 = "+Arrays.toString(rets));

    }

    //打印邻接矩阵
    public void print(){
        System.out.println("邻接矩阵为:");
        for (int i = 0; i < vertexs.length; i++) {
            for (int j = 0; j < vertexs.length; j++) {
                System.out.printf("%15d",martix[i][j]);
            }
            System.out.println();
        }
    }

}

//创建一个类EData, 它的对象实例表示一条边
class EData{
    char start;//边的起点(一个点)
    char end;//边的终点(另外一个点)
    int weight;//边的权值

    public EData(char start, char end, int weight) {
        this.start = start;
        this.end = end;
        this.weight = weight;
    }

    @Override
    public String toString() {
        return "EData{" +
                "<" + start +
                ", " + end +
                ">=" + weight +
                '}';
    }
}

8. 迪杰斯特拉算法

应用场景-最短路径问题

看一个应用场景和问题:
81

  1. 战争时期,胜利乡有7个村庄(A,B,C,D,E,F,G),现在有六个邮差,从G点出发,需要分别把邮件分别送到 A,B,C,D,E,F 六个村庄
  2. 各个村庄的距离用边线表示(权) ,比如 A – B 距离 5 公里
  3. 问:如何计算出G村庄到其它各个村庄的最短距离?
  4. 如果从其它点出发到各个点的最短距离又是多少?

迪杰斯特拉(Dijkstra)算法介绍

迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个结点到其他结点的最短路径。它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止。

迪杰斯特拉(Dijkstra)算法过程

  1. 设置出发顶点为v,顶点集合V{v1,v2,vi...},v到V中各顶点的距离构成距离集合Dis,Dis{d1,d2,di...},Dis 集合记录着 v 到图中各顶点的距离(到自身可以看作 0,v 到 vi 距离对应为 di)
  2. 从Dis中选择值最小的di并移出Dis集合,同时移出V集合中对应的顶点vi,此时的v到vi即为最短路径
  3. 更新Dis集合,更新规则为:比较v到V集合中顶点的距离值,与v通过vi到V集合中顶点的距离值,保留值较小的一个(同时也应该更新顶点的前驱节点为 vi,表明是通过 vi 到达的)
  4. 重复执行两步骤,直到最短路径顶点为目标顶点即可结束

迪杰斯特拉(Dijkstra)算法最佳应用-最短路径

  1. 战争时期,胜利乡有7个村庄(A,B,C,D,E,F,G),现在有六个邮差,从G点出发,需要分别把邮件分别送到 A,B,C,D,E,F 六个村庄
  2. 各个村庄的距离用边线表示(权) ,比如 A – B 距离 5 公里
  3. 问:如何计算出G村庄到其它各个村庄的最短距离?
  4. 如果从其它点出发到各个点的最短距离又是多少?
  5. 使用图解的方式分析了迪杰斯特拉(Dijkstra)算法思路

代码实现

/**
 * @author DSH
 * @date 2020/10/10
 * @description 迪杰斯特拉算法 解决最短路径问题
 */
public class DijkstraAlogrithm {
    public static void main(String[] args) {
        char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
        //邻接矩阵
        int[][] matrix = new int[vertex.length][vertex.length];
        final int N = 65535;//表示不可连接
        matrix[0] = new int[]{N, 5, 7, N, N, N, 2};
        matrix[1] = new int[]{5, N, N, 9, N, N, 3};
        matrix[2] = new int[]{7, N, N, N, 8, N, N};
        matrix[3] = new int[]{N, 9, N, N, N, 4, N};
        matrix[4] = new int[]{N, N, 8, N, N, 5, 4};
        matrix[5] = new int[]{N, N, N, 4, 5, N, 6};
        matrix[6] = new int[]{2, 3, N, N, 4, 6, N};
        //创建graph对象
        Graph graph = new Graph(vertex, matrix);
        //测试邻接矩阵显示
        graph.showGraph();
        graph.dijkstra(6);//出发顶点是G
        graph.showDijistra();
    }


}

//创建邻接矩阵 图
class Graph {
    char[] vertex;//顶点数组
    int[][] matrix;//邻接矩阵
    VisitedVertex visitedVertex;

    public Graph(char[] vertex, int[][] matrix) {
        this.vertex = vertex;
        this.matrix = matrix;
    }

    //显示图的邻接矩阵
    public void showGraph() {
        for (int[] link : matrix) {
            System.out.println(Arrays.toString(link));
        }
    }

    //迪杰斯特拉算法
    /**
     * @param index 表示出发顶点对应的下标
     */
    public void dijkstra(int index){
        visitedVertex = new VisitedVertex(vertex.length, index);
        update(index);//更新index顶点到周围顶点的距离和前驱顶点
        for (int j = 0; j < vertex.length; j++) {
            index = visitedVertex.updateArr();//选择并返回新的访问顶点
            update(index);//更新index顶点到周围顶点的距离和前驱顶点
        }
    }

    //更新index下标顶点到周围顶点的距离和周围顶点的前驱顶点
    private void update(int index){
        int len = 0;
        //根据遍历我们的邻接矩阵的 matrix[index] 行
        for (int j = 0; j < matrix[index].length; j++) {
            len = visitedVertex.getDis(index)+matrix[index][j];
            //如果j顶点没有被访问过,并且len小于出发顶点到j顶点的距离,就需要更新
            if (!visitedVertex.in(j) && len < visitedVertex.getDis(j)){
                visitedVertex.updatePre(j,index);//更新j顶点的前驱为index的顶点
                visitedVertex.updateDis(j,len);//更新出发顶点到j顶点的距离
            }
        }
    }

    public void showDijistra(){
        visitedVertex.show();
    }
}

//已访问顶点的集合
class VisitedVertex{
    // 记录各个顶点是否访问过 1 表示访问过,0 未访问,会动态更新
    public int[] already_arr;
    // 每个下标对应的值为前一个顶点下标, 会动态更新
    public int[] pre_visited;
    // 记录出发顶点到其他所有顶点的距离,比如 G 为出发顶点,就会记录 G 到其它顶点的距离,会动态更新,求的最短距离就会存放到 dis
    public int[] dis;

    /**
     *
     * @param length 表示顶点的个数
     * @param index 出发顶点对应的下标,比如G顶点,下标就是6
     */
    public VisitedVertex(int length,int index) {
        this.already_arr = new int[length];
        this.pre_visited = new int[length];
        this.dis = new int[length];
        //初始化dis数组
        Arrays.fill(dis,65535);
        this.already_arr[index] = 1;//设置出发顶点被访问过
        this.dis[index] = 0;//设置出发顶点的访问距离为0
    }

    /**
     * 功能: 判断index顶点是否被访问过
     * @param index
     * @return 如果访问过 返回true,否则false
     */
    public boolean in(int index){
        return already_arr[index] == 1;
    }

    /**
     * 更新出发顶点到index顶点的距离
     * @param index 哪一个顶点
     * @param len 距离多少
     */
    public void updateDis(int index,int len){
        dis[index] = len;
    }

    /**
     * 功能: 更新pre这个顶点的前驱为index的节点
     * @param pre
     * @param index
     */
    public void updatePre(int pre, int index){
        pre_visited[pre] = index;
    }

    /**
     * 功能: 返回出发顶点到index顶点的距离
     * @param index
     */
    public int getDis(int index){
        return dis[index];
    }

    /**
     * 继续选择并返回新的访问顶点, 比如这里的G之后, 就是A点作为新的访问顶点(注意不是出发顶点)
     * @return
     */
    public int updateArr(){
        int min = 65535, index = 0;
        for (int i = 0; i < already_arr.length; i++) {
            if (already_arr[i] == 0 && dis[i] < min){
                min = dis[i];
                index = i;
            }
        }
        //更新index顶点被访问过
        already_arr[index] = 1;
        return index;
    }

    /**
     * 显示最后的结果
     * 即将三个数组的情况输出
     */
    public void show(){
        System.out.println("=================");
        //already_arr
        for (int i = 0; i < already_arr.length; i++) {
            System.out.print(already_arr[i]+" ");
        }
        System.out.println();
        //pre_visited
        for (int pre: pre_visited) {
            System.out.print(pre+" ");
        }
        System.out.println();
        //dis
        for (int d: dis) {
            System.out.print(d+" ");
        }
        System.out.println();
        //输出整理
        char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
        int count = 0;
        for (int d: dis) {
            if (d!=65535){
                System.out.print(vertex[count]+"("+d+") ");
            }else {
                System.out.print("N");
            }
            count++;
        }
        //输出结果 A(2) B(3) C(9) D(10) E(4) F(6) G(0)
        System.out.println();
    }

}