求最小生成树算法

章节目录:

    • 一、基本概念
      • 1.1 生成树
      • 1.2 最小生成树
    • 二、普利姆算法
      • 2.1 算法介绍
      • 2.2 算法步骤
      • 2.3 应用场景
    • 三、克鲁斯卡尔算法
      • 3.1 算法介绍
      • 3.2 算法步骤
      • 3.3 应用场景
    • 四、两种算法对比
    • 五、结束语

一、基本概念

1.1 生成树

一个连通图的生成树是一个极小的连通子图,它包含图中全部的 n 个顶点,但只有构成一棵树的 n-1 条边。

  • 一个连通图可以有多棵生成树;

  • 一个连通图的所有生成树都包含相同的顶点个数边数

  • 生成树当中不存在环

  • 移除生成树中的任意一条边都会导致图的不连通, 生成树的边最少特性;

  • 在生成树中添加一条边会构成环。

  • 对于包含 n 个顶点的连通图,生成树包含 n 个顶点和 n-1 条边;

  • 对于包含 n 个顶点的无向完全图最多包含 n ⁿ﹣² 棵生成树。

  • 示意图

求最小生成树算法_第1张图片

  • 上图中包含3个顶点的无向完全图,生成树的棵数为:3 ³﹣² = 3

1.2 最小生成树

所谓一个带权图的最小生成树,就是原图中边的权值最小的生成树,所谓最小是指边的权值之和小于或者等于其它生成树的边的权值之和。

  • 示意图

求最小生成树算法_第2张图片

  • 最小生成树可以用普里姆算法Prim算法 )或者克鲁斯卡尔算法kruskal算法 )求出。

二、普利姆算法

2.1 算法介绍

普里姆算法Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树Minimum Spanning Tree)。由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点,且其所有边的权值之和亦为最小

2.2 算法步骤

  1. 选取权值最小边的其中一个顶点作为起始点
  2. 找到离当前顶点权值最小的边,并记录该顶点为已选择;
  3. 重复第二步,直到找到所有顶点,就找到了图的最小生成树。

2.3 应用场景

假设有 7 个村庄(A, B, C, D, E, F, G),现在需要修路把 7 个村庄连通。各个村庄的距离用边线表示(权),比如 A<–>B 距离 5 公里,问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短?(通过普利姆算法求最小生成树)

  • 普利姆算法-求最小生成树-示意图

求最小生成树算法_第3张图片

  • 代码示例
public class PrimAlgorithmDemo {

    public static void main(String[] args) {
        // 各顶点的数据。
        char[] data = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G'};
        // 顶点个数。
        int vertexes = data.length;
        // 邻接矩阵的关系使用二维数组表示,这里用 Integer.MAX_VALUE 这个大数,表示两个点不联通。
        int x = Integer.MAX_VALUE;
        int[][] matrix = {
                //     A  B  C  D  E  F  G
                /*A*/ {x, 5, 7, x, x, x, 2},
                /*B*/ {5, x, x, 9, x, x, 3},
                /*C*/ {7, x, x, x, 8, x, x},
                /*D*/ {x, 9, x, x, x, 4, x},
                /*E*/ {x, x, 8, x, x, 5, 4},
                /*F*/ {x, x, x, 4, 5, x, 6},
                /*G*/ {2, 3, x, x, 4, 6, x},
        };

        Graph graph = new Graph(vertexes);
        MinTree minTree = new MinTree();
        minTree.createGraph(graph, vertexes, data, matrix);
        // 图创建是否成功。
        minTree.showGraph(graph);
        // [2147483647, 5, 7, 2147483647, 2147483647, 2147483647, 2]
        // [5, 2147483647, 2147483647, 9, 2147483647, 2147483647, 3]
        // [7, 2147483647, 2147483647, 2147483647, 8, 2147483647, 2147483647]
        // [2147483647, 9, 2147483647, 2147483647, 2147483647, 4, 2147483647]
        // [2147483647, 2147483647, 8, 2147483647, 2147483647, 5, 4]
        // [2147483647, 2147483647, 2147483647, 4, 5, 2147483647, 6]
        // [2, 3, 2147483647, 2147483647, 4, 6, 2147483647]

        // 测试普利姆算法。
        System.out.println();
        // 从顶点 A 开始。
        minTree.prim(graph, 0);
        // 边A<->G 权值:2
        // 边G<->B 权值:3
        // 边G<->E 权值:4
        // 边E<->F 权值:5
        // 边F<->D 权值:4
        // 边A<->C 权值:7
    }
}


/**
 * 最小生成树。
 */
class MinTree {

    /**
     * 创建图
     *
     * @param graph    图
     * @param vertexes 顶点个数
     * @param data     各顶点的值
     * @param matrix   邻接矩阵
     */
    public void createGraph(Graph graph, int vertexes, char[] data, int[][] matrix) {
        int i;
        int j;
        // 顶点。
        for (i = 0; i < vertexes; i++) {
            graph.setData(i, data[i]);
            for (j = 0; j < vertexes; j++) {
                graph.setEdges(i, j, matrix[i][j]);
            }
        }
    }

    /**
     * 显示图信息。
     *
     * @param graph 图
     */
    public void showGraph(Graph graph) {
        for (int[] link : graph.getEdges()) {
            System.out.println(Arrays.toString(link));
        }
    }

    /**
     * 普利姆算法。
     * 目的:通过该算法,得到最小生成树。
     *
     * @param graph 图对象
     * @param v     表示从图的第几个顶点开始生成('A'->0 'B'->1...)。
     */
    public void prim(Graph graph, int v) {
        int vertexes = graph.getVertexes();
        int[] isVisited = new int[vertexes];
        isVisited[v] = 1;
        // x 和 y 记录两个顶点的下标。
        int x = -1;
        int y = -1;
        int minWeight = Integer.MAX_VALUE;

        for (int k = 1; k < vertexes; k++) {
            // 这个是确定每一次生成的子图 ,和哪个节点的距离最近。
            // i节点表示被访问过的节点。
            for (int i = 0; i < vertexes; i++) {
                // j节点表示还没有访问过的节点。
                for (int j = 0; j < vertexes; j++) {

                    if (1 == isVisited[i]
                            && 0 == isVisited[j]
                            && graph.getEdgeByCoords(i, j) < minWeight
                    ) {
                        // 替换最小权值(寻找已经访问过的节点和未访问过的节点间的权值最小的边)。
                        minWeight = graph.getEdgeByCoords(i, j);
                        x = i;
                        y = j;
                    }
                }
            }

            // 找到一条边是最小。
            System.out.println(
                    "边" + graph.getData(x) + "<->" + graph.getData(y)
                            + " 权值:" + minWeight
            );

            // 将当前这个节点标记为已经访问。
            isVisited[y] = 1;
            // minWeight 重新设置为最大值 Integer.MAX_VALUE。
            minWeight = Integer.MAX_VALUE;
        }
    }
}


/**
 * 图。
 */
class Graph {
    /**
     * 图顶点的个数。
     */
    private final int vertexes;

    /**
     * 图顶点的数据。
     */
    private final char[] data;

    /**
     * 图的边(邻接矩阵)。
     */
    private final int[][] edges;

    /**
     * 初始化构造。
     *
     * @param vertexes 顶点个数
     */
    public Graph(int vertexes) {
        this.vertexes = vertexes;
        this.data = new char[vertexes];
        this.edges = new int[vertexes][vertexes];
    }

    public int getVertexes() {
        return vertexes;
    }

    public char getData(int n) {
        return this.data[n];
    }

    public int[][] getEdges() {
        return edges;
    }

    public int getEdgeByCoords(int i, int j) {
        return this.edges[i][j];
    }

    public void setData(int i, char data) {
        this.data[i] = data;
    }

    public void setEdges(int i, int j, int weight) {
        this.edges[i][j] = weight;
    }
}

三、克鲁斯卡尔算法

3.1 算法介绍

克鲁斯卡尔算法是求连通网的最小生成树的另一种方法。与普里姆算法不同,它的时间复杂度为 O(eloge)(e为网中的边数),所以,适合于求边稀疏的网的最小生成树。

3.2 算法步骤

  • 克鲁斯卡尔算法查找最小生成树的方法是:将连通网中所有的边按照权值大小做升序排序,从权值最小的边开始选择,只要此边和已选择的边一起构成回路就可以选择它组成最小生成树。
  • 实现克鲁斯卡尔算法的难点在于“如何判断一个新边是否会和已选择的边构成回路”。(处理方式是:记录顶点在“最小生成树”中的终点,顶点的终点是“在最小生成树中与它连通的最大顶点”。 然后每次需要将一条边添加到最小生存树时,判断该边的两个顶点的终点是否重合,重合的话则会构成回路。)
  • 回路判断-示意图
  • 我们待加入的边的两个顶点不能都指向同一个终点,否则将构成回路

求最小生成树算法_第4张图片

3.3 应用场景

假设有 7 个村庄(A, B, C, D, E, F, G),现在需要修路把 7 个村庄连通。各个村庄的距离用边线表示(权),比如 A<–>B 距离 12 公里,问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短?(通过克鲁斯卡尔算法求最小生成树)

  • 克鲁斯卡尔算法-求最小生成树-示意图

求最小生成树算法_第5张图片

  • 代码示例
public class KruskalAlgorithmDemo {

    public static void main(String[] args) {
        // 各顶点的数据。
        char[] vertexes = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
        // 邻接矩阵的关系使用二维数组表示,这里用 Integer.MAX_VALUE 这个大数,表示两个点不联通。
        int x = Integer.MAX_VALUE;
        int[][] matrix = {
                //     A  B   C  D  E  F  G
                /*A*/ {0, 12, x, x, x, 16, 14},
                /*B*/ {12, 0, 10, x, x, 7, x},
                /*C*/ {x, 10, 0, 3, 5, 6, x},
                /*D*/ {x, x, 3, 0, 4, x, x},
                /*E*/ {x, x, 5, 4, 0, 2, 8},
                /*F*/ {16, 7, 6, x, 2, 0, 9},
                /*G*/ {14, x, x, x, 8, 9, 0}
        };

        Graph graph = new Graph(vertexes, matrix);
        graph.showMatrix();
        // 邻接矩阵为:
        //
        //           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

        System.out.println();
        EdgeData[] results = new MinTree().kruskal(graph);
        System.out.println("图的边共计:" + results.length + "条。");
        // 图的边共计:12条。

        System.out.println("最小生成树如下:");
        for (EdgeData result : results) {
            if (null != result) {
                System.out.println(result);
            }
        }
        // 最小生成树如下:
        // EdgeData: {= 2}
        // EdgeData: {= 3}
        // EdgeData: {= 4}
        // EdgeData: {= 7}
        // EdgeData: {= 8}
        // EdgeData: {= 12}
    }
}


/**
 * 最小生成树。
 */
class MinTree {

    /**
     * 克鲁斯卡尔算法。
     * 目的:通过该算法,得到最小生成树。
     *
     * @param graph 图
     * @return {@link EdgeData[]} 边数据信息
     */
    public EdgeData[] kruskal(Graph graph) {
        // 表示最后结果数组的索引。
        int index = 0;
        int edgeNum = graph.getEdgeNum();
        // 用于保存"已有最小生成树" 中的每个顶点在最小生成树中的终点。
        int[] ends = new int[edgeNum];
        // 创建结果数组, 保存最后的最小生成树。
        EdgeData[] results = new EdgeData[edgeNum];

        // 图中12条边构成的集合。
        EdgeData[] edges = graph.getEdges();

        // 按照边的权值大小进行排序(从小到大)。
        graph.sortEdges(edges);

        // 将没有形成回路的边添加到最小生成树中时,否则不添加。
        for (int i = 0; i < edgeNum; i++) {
            // 获取到第i条边的第一个顶点(边的起点:p1=4)。
            int p1 = graph.getPosition(edges[i].start);
            // 获取到第i条边的第二个顶点(边的终点:p2=5)
            int p2 = graph.getPosition(edges[i].end);
            // 获取p1这个顶点在已有最小生成树中的终点(m=4)。
            int m = graph.getEnd(p1, ends);
            // 获取p2这个顶点在已有最小生成树中的终点(n=5)。
            int n = graph.getEnd(p2, ends);
            // 如果没有构成回路。
            if (m != n) {
                // 设置 m 在"已有最小生成树"中的终点  [0,0,0,0,5,0,0,0,0,0,0,0]
                ends[m] = n;
                // 有一条边加入到rets数组。
                results[index++] = edges[i];
            }
        }

        return results;
    }
}


/**
 * 图。
 */
class Graph {
    /**
     * 边的条数。
     */
    private int edgeNum;

    /**
     * 顶点数据。
     */
    private final char[] vertexesData;

    /**
     * 邻接矩阵。
     */
    private final int[][] matrix;

    /**
     * 表示边不联通。
     */
    private static final int DISCONNECTED = Integer.MAX_VALUE;

    public int getEdgeNum() {
        return edgeNum;
    }

    /**
     * 构造图。
     *
     * @param vertexesData 顶点数据
     * @param matrix       矩阵
     */
    public Graph(char[] vertexesData, int[][] matrix) {
        // 初始化顶点数和边的个数。
        int vLen = vertexesData.length;

        // 通过复制拷贝的方式,初始化顶点。
        this.vertexesData = new char[vLen];
        System.arraycopy(vertexesData, 0, this.vertexesData, 0, vLen);

        // 通过复制拷贝的方式,初始化边。
        this.matrix = new int[vLen][vLen];
        for (int i = 0; i < vLen; i++) {
            System.arraycopy(matrix[i], 0, this.matrix[i], 0, vLen);
        }

        // 统计边的条数。
        for (int i = 0; i < vLen; i++) {
            for (int j = i + 1; j < vLen; j++) {
                if (DISCONNECTED != this.matrix[i][j]) {
                    // 联通的边进行计数。
                    edgeNum++;
                }
            }
        }
    }

    /**
     * 显示矩阵信息。
     */
    public void showMatrix() {
        System.out.println("邻接矩阵为: \n");
        int length = this.vertexesData.length;
        for (int i = 0; i < length; i++) {
            for (int j = 0; j < length; j++) {
                System.out.printf("%12d", matrix[i][j]);
            }
            System.out.println();
        }
    }

    /**
     * 通过冒泡方式对边排序。
     *
     * @param edges 边的集合
     */
    public void sortEdges(EdgeData[] edges) {
        int len = edges.length - 1;
        EdgeData tmp = null;
        for (int i = 0; i < len; i++) {
            for (int j = 0; j < (len - i); j++) {
                // 前一个的权值大于后一个,则互换位置。
                if (edges[j].weight > edges[j + 1].weight) {
                    tmp = edges[j];
                    edges[j] = edges[j + 1];
                    edges[j + 1] = tmp;
                }
            }
        }
    }

    /**
     * 找到对应顶点的下标。
     *
     * @param vertex 顶点的值
     * @return int   返回顶点下标,找不到时返回-1
     */
    public int getPosition(char vertex) {
        for (int i = 0; i < this.vertexesData.length; i++) {
            if (vertex == this.vertexesData[i]) {
                return i;
            }
        }
        return -1;
    }

    /**
     * 通过邻接矩阵获取图的边信息,并将其保存至数组。
     *
     * @return {@link EdgeData[]}
     */
    public EdgeData[] getEdges() {
        int index = 0;
        EdgeData[] edges = new EdgeData[this.edgeNum];
        int length = this.vertexesData.length;
        for (int i = 0; i < length; i++) {
            for (int j = (i + 1); j < length; j++) {
                if (DISCONNECTED != this.matrix[i][j]) {
                    // 示例:[['A','B', 12], ['B','F',7], .....]
                    edges[index++] = new EdgeData(vertexesData[i], vertexesData[j], matrix[i][j]);
                }
            }
        }
        return edges;
    }

    /**
     * 获取当前下标顶点的终点。
     * 目的:用于后面判断两个顶点的终点是否相同。
     *
     * @param i    对应顶点的下标
     * @param ends 各个顶点所对应的终点
     * @return int 当前顶点对应的终点下标
     */
    public int getEnd(int i, int[] ends) {
        while (0 != ends[i]) {
            i = ends[i];
        }
        return i;
    }
}


/**
 * 边数据。
 */
class EdgeData {

    /**
     * 边的起始点。
     */
    char start;

    /**
     * 边的终点。
     */
    char end;

    /**
     * 边的权值。
     */
    int weight;

    /**
     * 构造器
     *
     * @param start  边的起始点
     * @param end    边的终点
     * @param 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 + "}";
    }
}

四、两种算法对比

  • 克鲁斯卡尔算法的时间复杂度为 O(eloge),其中 e 为边数,克鲁斯卡尔算法主要针对边展开,边数少时效率会很高,所以对于稀疏图有优势。
  • 普里姆算法的时间复杂度为 O(nⁿ),其中 n 为顶点数,适用于稠密图

五、结束语


“-------怕什么真理无穷,进一寸有一寸的欢喜。”

微信公众号搜索:饺子泡牛奶

你可能感兴趣的:(数据结构与算法,算法,图论,数据结构)