天天看点

Kruskal算法求解最小生成树问题(java代码实现)

Kruskal算法求解最小生成树问题(java代码实现)

Kruskal算法求解最小生成树问题(java代码实现)
Kruskal算法求解最小生成树问题(java代码实现)

Kruskal算法求解上面的公交问题(即求解该图的最小生成树)的思路分析

在含有n个顶点的连通图中选择n-1条边,构成一棵极小连通子图,并使该连通子图中n-1条边上权值之和达到最小,则称其为连通网的最小生成树。

Kruskal算法求解最小生成树问题(java代码实现)

例如,对于如上图所示的连通网可以有多棵权值总和不相同的生成树。

Kruskal算法求解最小生成树问题(java代码实现)

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

图解分析

Kruskal算法求解最小生成树问题(java代码实现)

文字描述:

第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>。

上面的步骤在算法实现中的两个需要解决的问题

问题一 对图的所有边按照权值大小进行排序。

问题二 将边添加到最小生成树中时,怎么样判断是否形成了回路。

问题一很好解决,采用排序算法进行排序即可。

问题二,处理方式是:记录顶点在"最小生成树"中的终点,顶点的终点是"在最小生成树中与它连通的最大顶点"。然后每次需要将一条边添加到最小生存树时,判断该边的两个顶点的终点是否重合,重合的话则会构成回路。

Java代码实现

package com.bingym.kruskal;

import java.util.Arrays;

public class KruskalCase {
    /*
    * 克鲁斯卡尔(Kruskal)算法求最小生成树:
    * 克鲁斯卡尔(Kruskal)算法,是用来求加权连通图的最小生成树的算法。
    * (1)基本思想:按照权值从小到大的顺序选择n-1条边,并保证这n-1条边不构成回路
    * (2)具体做法:首先构造一个只含n个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止
    * */
    //定义若干属性
    //1.定义边的个数
    private int edgeNum;
    //2.定义顶点的数组:用来存储从顶点'A' 到 'G'的数组元素
    private char[] vertexs;
    //3.使用INF表示两个顶点之间是无法连通的:即两个顶点的边的长度取Integer的最大值
    private static final int INF = Integer.MAX_VALUE;
    //4.定义邻接矩阵
    private int[][] matrix;

    //定义构造器
    public KruskalCase(char[] vertexArr,int[][] matrixArr) {
        int vlen = vertexArr.length;//初始化顶点数和边的个数
        //初始化顶点:利用拷贝的方法:即初始化vertexs
        this.vertexs = new char[vlen];
        for (int i = 0; i < vertexs.length; i++) {
            this.vertexs[i] = vertexArr[i];//将传入的顶点数据保存到KruskalCase的vertexs的顶点数组中
        }
        //初始化边:也使用复制拷贝的方式:即初始化matrix
        this.matrix = new int[vlen][vlen];//行和列均为vlen长度
        for (int i = 0; i < vlen; i++) {
            for (int j = 0; j < vlen; j++) {
                this.matrix[i][j] = matrixArr[i][j];
            }
        }
        //统计边的个数:即初始化edgeNum
        for (int i = 0; i < vlen; i++) {
            for (int j = i + 1; j < vlen; j++) {
                if (this.matrix[i][j] != INF) {
                    edgeNum++;
                }
            }
        }
    }

    public static void main(String[] args) {
        char[] vertexsArr = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
        //克鲁斯卡尔算法的邻接矩阵
        int[][] matrixArr = {
                /*A*//*B*//*C*//*D*//*E*//*F*//*G*/
                /*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}};
        //创建KruskalCase实例
        KruskalCase kruskalCase = new KruskalCase(vertexsArr,matrixArr);
        //进行最小生成树的构建
        kruskalCase.print();//图的邻接矩阵的输出打印
        kruskalCase.kruskal();//通过Kruskal算法进行最小生成树的构建

    }

    //Kruskal算法方法
    public void kruskal() {
        //创建索引:表示最后数组的缩影
        int index = 0;
        int[] ends = new int[edgeNum];//用于保存"已有最小生成树" 中的每个顶点在最小生成树中的终点
        //创建结果数组,保存最后得到的最小生成树结果
        EdgeData[] minTree = new EdgeData[edgeNum];
        //1.首先获取图中所有边的对象的集合
        EdgeData[] edgeArr = getEdgeArr();
        System.out.println("图的边的集合=" + Arrays.toString(edgeArr) + " 共有边(条数):"+ edgeArr.length);
        //2.对这些边进行排序处理:按从小到大排序
        sortEdge(edgeArr);
        //输出排序后的边的集合:
        System.out.println("排序以后图的边的集合=" + Arrays.toString(edgeArr) + " 共有边(条数):"+ edgeArr.length);
        //3.遍历edgeArr数组:将边添加到最小生成树中
        //因为已经进行了排序:所以我们依次添加即可
        //只要注意每次添加时判断是否形成回路,若形成回路,则放弃该条边
        for (int i = 0; i < edgeNum; i++) {
            //首先获取到边的第一个顶点
            int start = getPosition(edgeArr[i].start);
            //再获取到边的第二个顶点
            int end = getPosition(edgeArr[i].end);

            //分别获取两个顶点的终点
            int dest1 = getEnd(ends,start);
            int dest2 = getEnd(ends,end);
            //判断是否构成回路:也就是这两个顶点的终点是否相同
            if (dest1 != dest2) {   //此时没有构成回路
                //第一次E和F,通过getEnd方法:dest1 = 4;dest2 = 5,则将ends[4] = 5:即将E的终点设置为F
                ends[dest1] = dest2;//将此时dest1在"已有的最小生成树"中的终点设置为dest2
                //注意:我们无需对dest2的终点进行设置
                //并将此时有效的边加入到minTree数组中
                minTree[index++] = edgeArr[i];
            }
        }
        //最终通过for循环的遍历添加处理得到最小生成树:
        System.out.println("得到的最小生成树为:");
        for (int i = 0; i < index; i++) {//将打印的上界设为index:即最终最小生成树的边的个数
            System.out.println(minTree[i]);
        }
    }

    //定义方法1:打印邻接矩阵
    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("%12d",matrix[i][j]);//将邻接矩阵数据输出打印:格式化
            }
            System.out.println();//换行处理
        }
    }

    //定义方法2:对边进行排序处理:这里采用较为简单的冒泡排序法
    private void sortEdge(EdgeData[] edgeArr) {//edgeArr存储每个有效边的对象的集合
        for (int i = 0; i < edgeArr.length - 1; i++) {//冒泡排序排序次数:edgeArr.length - 1次
            for (int j = 0; j < edgeArr.length - 1 - i; j++) {
                if (edgeArr[j].weight > edgeArr[j + 1].weight) {
                    //进行交换,则按照从小到大排序
                    EdgeData temp = edgeArr[j];
                    edgeArr[j] = edgeArr[j + 1];
                    edgeArr[j + 1] = temp;
                }
            }
        }
    }

    //定义方法3:根据指定的顶点的值返回其对应的下标
    private int getPosition(char vertex) {
        for (int i = 0; i < vertexs.length; i++) {
            if (vertexs[i] == vertex) {
                return i;
            }
        }
        //找不到则返回-1
        return -1;
    }

    //定义方法4:获取图中的边:将其放到EdgeData[] 数组中,后面需要进行遍历
    //通过邻接矩阵martrix邻接矩阵得到
    //得到的结果EdgeData[] 形式 [['A','B', 12], ['B','F',7], .....]
    private EdgeData[] getEdgeArr() {
        int index = 0;
        EdgeData[] edgeArr = new EdgeData[edgeNum];//数组大小为当前给出的图的所有的边的个数
        for (int i = 0; i < vertexs.length; i++) {
            for (int j = i+1; j < vertexs.length; j++) {
                if (matrix[i][j] != INF) {
                    //说明两个顶点存在边
                    edgeArr[index++] = new EdgeData(vertexs[i],vertexs[j],matrix[i][j]);
                }
            }
        }
        return edgeArr;//将得到的所有的边的对象的数组返回
    }

    //定义方法5:获取下标为i的顶点的终点:用来后面判断两个顶点的终点是否相同
    private int getEnd(int[] ends,int i) {
        //ends:该数组用来记录各个顶点对应的终点是哪一个:刚开始均为0
        //i:即表示传入的查询终点的顶点的下标
        //很巧妙的计算终点
        while (ends[i] != 0) {
            i = ends[i];//若当前i的终点不为0,则将其赋值给i
        }
        return i;//若当前i的终点为0,即i的顶点刚加入,则自己即为其顶点
    }

}

//定义一个类EdgeData:表示一条边:里面保存一条边的两个顶点以及这条边的长度(即权值)
class EdgeData {
    //定义边的属性
    public char start;//边的一个顶点
    public char end;///边的另一个顶点
    public int weight;//边的权值

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

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

输出结果:
图的邻接矩阵为:
           0          12  2147483647  2147483647  2147483647          16          14
          12           0          10  2147483647  2147483647           7  2147483647
  2147483647          10           0           3           5           6  2147483647
  2147483647  2147483647           3           0           4  2147483647  2147483647
  2147483647  2147483647           5           4           0           2           8
          16           7           6  2147483647           2           0           9
          14  2147483647  2147483647  2147483647           8           9           0
图的边的集合=[EdgeData [<A,B> weight=12], EdgeData [<A,F> weight=16], EdgeData [<A,G> weight=14], EdgeData [<B,C> weight=10], EdgeData [<B,F> weight=7], EdgeData [<C,D> weight=3], EdgeData [<C,E> weight=5], EdgeData [<C,F> weight=6], EdgeData [<D,E> weight=4], EdgeData [<E,F> weight=2], EdgeData [<E,G> weight=8], EdgeData [<F,G> weight=9]] 共有边(条数):12
排序以后图的边的集合=[EdgeData [<E,F> weight=2], EdgeData [<C,D> weight=3], EdgeData [<D,E> weight=4], EdgeData [<C,E> weight=5], EdgeData [<C,F> weight=6], EdgeData [<B,F> weight=7], EdgeData [<E,G> weight=8], EdgeData [<F,G> weight=9], EdgeData [<B,C> weight=10], EdgeData [<A,B> weight=12], EdgeData [<A,G> weight=14], EdgeData [<A,F> weight=16]] 共有边(条数):12
得到的最小生成树为:
EdgeData [<E,F> weight=2]
EdgeData [<C,D> weight=3]
EdgeData [<D,E> weight=4]
EdgeData [<B,F> weight=7]
EdgeData [<E,G> weight=8]
EdgeData [<A,B> weight=12]