KruskalAlgorithm(克鲁斯卡尔算法)

KruskalAlgorithm介绍

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

最小生成树

(Minimum Cost Spanning Tree),简称 MST。给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树

克鲁斯卡尔算法应用场景-公交站问题

KruskalAlgorithm(克鲁斯卡尔算法)_第1张图片

  1. 某城市新增 7 个站点(A, B, C, D, E, F, G) ,现在需要修路把 7 个站点连通

  2. 各个站点的距离用边线表示(权) ,比如 A – B 距离 12 公里

  3. 问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短?

克鲁斯卡尔算法图解说明

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

KruskalAlgorithm(克鲁斯卡尔算法)_第2张图片 KruskalAlgorithm(克鲁斯卡尔算法)_第3张图片

KruskalAlgorithm(克鲁斯卡尔算法)_第4张图片KruskalAlgorithm(克鲁斯卡尔算法)_第5张图片

KruskalAlgorithm(克鲁斯卡尔算法)_第6张图片 KruskalAlgorithm(克鲁斯卡尔算法)_第7张图片

第1步:将边加入 R 中。

的权值最小,因此将它加入到最小生成树结果 R 中。

第2步:将边加入 R 中。

上一步操作之后,边的权值最小,因此将它加入到最小生成树结果 R 中。

第3步:将边加入 R 中。

上一步操作之后,边的权值最小,因此将它加入到最小生成树结果 R 中。

第4步:将边加入 R 中。

上一步操作之后,边的权值最小,但会和已有的边构成回路;因此,跳过边。同理,跳过边。将边加入到最小生成树结果 R 中。

第5步:将边加入 R 中。

上一步操作之后,边的权值最小,因此将它加入到最小生成树结果 R 中。

第6步:将边加入 R 中。
上一步操作之后,边的权值最小,但会和已有的边构成回路;因此,跳过边。同理,跳过边。将边加入到最小生成树结果 R 中。

此时,最小生成树构造完成!它包括的边依次是:*** ***。

克鲁斯卡尔算法分析

根据前面介绍的克鲁斯卡尔算法的基本思想和做法,我们能够了解到,克鲁斯卡尔算法重点需要解决的以下两个问题:

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

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

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

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

如何判断是否构成回路

KruskalAlgorithm(克鲁斯卡尔算法)_第8张图片

在将 加入到最小生成树 R 中之后,这几条边的顶点就都有了终点:

  • C 的终点是 F。

  • D 的终点是 F。

  • E 的终点是 F。

  • F 的终点是 F。

关于终点的说明:

  1. 就是将所有顶点按照从小到大的顺序排列好之后;某个顶点的终点就是"与它连通的最大顶点"。

  2. 因此,接下来,虽然是权值最小的边。但是 C 和 E 的终点都是 F,即它们的终点相同,因此,将加入最小生成树的话,会形成回路。这就是判断回路的方式。也就是说,我们加入的边的两个顶点不都指向同一个终点,否则将构成回路

代码实现

public class KruskalAlgorithm {
    private int edgeNum; // 边的个数
    private char[] vertexs; // 顶点数组
    private int[][] matrix; // 邻接矩阵
    // 用 INF 表示两个顶点之间不连通
    private static final int INF = Integer.MAX_VALUE;

    public static void main(String[] args) {
        char[] vertexs = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
        int[][] matrix = {
               /*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}
        };

        KruskalAlgorithm kruskal = new KruskalAlgorithm(vertexs, matrix);
        kruskal.print();
        kruskal.kruskal();

    }
    // 构造器
    public KruskalAlgorithm(char[] vertexs, int[][] matrix) {
        this.vertexs = vertexs;
        this.matrix = matrix;
        
        // 统计边
        for (int i = 0; i < vertexs.length; i++) {
            for (int j = i+1; j < vertexs.length; j++){
                if (this.matrix[i][j] != INF) {
                    this.edgeNum++;
                }
            }
        }
    }
    // Kruskal核心算法
    public void kruskal() {
        int index = 0; //
        EData[] results = new EData[edgeNum]; // 用来保存最后的最小生成树
        int[] ends = new int[edgeNum]; // 用来保存每个顶点在最小生成树中的终点下标
        // 获取图中的所有边
        EData[] edges = getEdges();
        // 将边按照权值大小进行排序,因为Kruskal算法是从权值最小的开始挑
        sortEdges(edges);
        System.out.println("图的边集合");
        System.out.println(Arrays.toString(edges));
        /*遍历 edges 数组,判断要加入的边是否形成回路,
        如没有则加入最小生成树 results 中,否则不加入*/
        for (int i = 0; i < this.edgeNum; i++) {
            // 用来表示第 i 条边的一个顶点(起点)
            int p1 = getPosition(edges[i].start);
            // 用来表示第 i 条边的另一个顶点(终点)
            int p2 = getPosition(edges[i].end);

            // 获取p1这个顶点在当前最小生成树中的终点
            int m = getEnd(ends, p1);
            // 获取p2这个顶点在当前最小生成树中的终点
            int n = getEnd(ends, p2);
            // 判断两个终点是否相同,即是否构成回路
            if (m != n) { // 没有构成回路
                ends[m] = n; // 例如 ->  : [0,0,0,0,5,0,0,...]
                results[index++] = edges[i]; //加入到最小生成树中
            }
        }

        // 打印"最小生成树"
        System.out.println("最小生成树为");
        for (int i = 0; i < index; i++) {
            System.out.println(results[i]);
        }
    }
    
    // 打印邻接矩阵
    public void print() {
        System.out.println("===邻接矩阵===");
        for (int[] link : this.matrix){
            System.out.println(Arrays.toString(link));
        }
    }

    // 对边进行排序处理
    public void sortEdges(EData[] edges) {
        EData temp;
        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) {
                    temp = edges[j];
                    edges[j] = edges[j+1];
                    edges[j+1] = temp;
                }
            }
        }
    }

    /**
     * @param ch 表示顶点的值 'A', 'B' ...
     * @return 返回顶点在顶点数组中对应的下标,如找不到,则返回-1
     */
    public int getPosition(char ch){
        for (int i = 0; i < vertexs.length; i++) {
            if (vertexs[i] == ch) {
                return i;
            }
        }
        return -1;
    }

    /**
     * 从邻近矩阵中找到连通的边
     * @return 形如{['A', 'B', 12], ['B', 'F', 7], ...}
     */
    private EData[] getEdges() {
        int index = 0;
        EData[] edges = new EData[this.edgeNum];
        for (int i = 0; i < this.vertexs.length; i++) {
            for (int j = i+1; j < this.vertexs.length; j++) {
                if (this.matrix[i][j] != INF) {
                    edges[index++] = new EData(this.vertexs[i], this.vertexs[j], this.matrix[i][j]);
                }
            }
        }
        return edges;
    }

    /**
     * 获取下标为 i 的顶点的终点,用于判断两个顶点的终点是否相同
     * @param ends 用来记录各个顶点对应的终点下标
     * @param i 表示传入顶点对应的下标
     * @return 返回下标为 i 的这个顶点对应的终点下标
     */
    private int getEnd(int[] ends, int i){
        while (ends[i] != 0) { // 不断的往下找,直到找到这个顶点的终点
            i = ends[i];
        }
        return i;
    }
}

// 创建一个类-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 "<" + start + "," + end + ">" + ":" + weight;
    }
}

运行结果

KruskalAlgorithm(克鲁斯卡尔算法)_第9张图片

:以上大部分内容来源于韩顺平老师的数据结构和算法笔记

你可能感兴趣的:(算法,java,算法)