【数据结构】图的最小生成树算法

假设你是电信的实施工程师,需要为一个镇的九个村庄架设通信网络做设计,村
庄位置大致如图,其中 Vo~V8是村庄,之间连线的数字表示村与村间的可通达
的直线距离,比如Vo至V1就是10公里(个别如Vo与V6,V6与V8,V5与V7未测算距
离是因为有高山或湖泊,不予考虑)。你们领导要求你必须用最小的成本完成这次任
务。你说怎么办?

image-20220127202810611

这个问题实质上找连通网的最小生成树。

最小生成树定义:一个连通图的生成树是一个极小的连通子图,它含有图中全部的顶点,但只有足以构成一棵树的n-1条边。我们把构造连通网的最小代价生成树。称为最小生成树

经典的有两种算法,普里姆算法和克鲁斯卡尔算法

一、普里姆(Prim算法)

1.1、核心思想

用一个待定的最小权值的数组来保存每一个将来有可能跟我们相连的邻接点的最小权值,通过将顶点连通的方式,不断的连接,直至找到最后的最小权值。

1.2、步骤模拟

1,就上图而言,首先顶点v0+顶点v1看做一个整体,保存最小权值10。

那么就有

v0: 0 10 # # # 11 # # #

v1: 10 0 18 # # # 16 # 12

sum: 0 0 18 # # 11 16 # 12

连通的记为0,都有数字的取最小值,都是不可连通#相加仍不可连通

2,然后在连通的边18、11、16、12中,11最小,加上顶点v5

3,依次重复。最小权值的数组变化过程如下图所示(其中#表示不可连通),

4,最后得到最小生成树10+11+12+8+16+19+7+16=99,也就是解决了上述施工问题。

v0 v1 v2 v3 v4 v5 v6 v7 v8		顶点变化
0  10  #  #  # 11  #  # #   	 v0
0  0  18  #  # 11  16 # 12		 +v1:10
0  0  18  #  26 0  16 # 12		 +v5:11
0  0  8  21  26 0  16 # 0		 	 +v8:12
0  0  8  21  26 0  16 # 0      +v2:8
0  0  0  21  26 0  16 # 0      +v6:16
0  0  0  21  26 0  0 19 0      +v7:19
0  0  0  16  7  0  0  0 0      +v4:7
0  0  0  16  0  0  0  0 0      +v3:16
0  0  0  0   0  0  0  0 0 		 
1.3、实现如下
public class Graph {
    private int vertexSize;//顶点数量
    private int[] vertexs;//顶点数组
    private int[][] matrix;
    private static final int MAX_WEIGHT = 1000;

    public Graph(int vertextSize) {
        this.vertexSize = vertextSize;
        matrix = new int[vertextSize][vertextSize];
        vertexs = new int[vertextSize];
        for (int i = 0; i < vertextSize; i++) {
            vertexs[i] = i;
        }
    }

    /**
     * prim 普里姆算法
     */
    public void prim() {
        //最小代价顶点权值的数组,为0表示已经获取最小权值
        int[] lowcost = new int[vertexSize];

        //放顶点权值
        int[] adjvex = new int[vertexSize];

        int min, minIndex, sum = 0;
        // 拷贝v0到lowcost数组
        for (int i = 1; i < vertexSize; i++) {
            lowcost[i] = matrix[0][i];
        }
        // 从v1开始遍历
        for (int i = 1; i < vertexSize; i++) {
            min = MAX_WEIGHT;
            minIndex = 0;
            //找最小值、最小值index
            for (int j = 1; j < vertexSize; j++) {
                if (lowcost[j] < min && lowcost[j] > 0) {
                    min = lowcost[j];
                    minIndex = j;
                }
            }
            System.out.println("顶点:" + adjvex[minIndex] + ",权值:" + min);
            //加上每个阶段的最小生成树
            sum += min;
            //找到置0表示已经连通
            lowcost[minIndex] = 0;
            for (int j = 1; j < vertexSize; j++) {
                if (lowcost[j] != 0 && matrix[minIndex][j] < lowcost[j]) {
                    // 顶点相加:lowcost加上matrix[minIndex]
                    lowcost[j] = matrix[minIndex][j];
                    // 记录是哪个顶点
                    adjvex[j] = minIndex;
                }
            }
        }
        System.out.println("最小生成树权值和:" + sum);
    }

    public static void main(String[] args) {
        Graph graph = new Graph(9);

        int[] a1 = new int[]{0, 10, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, 11, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT};
        int[] a2 = new int[]{10, 0, 18, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, 16, MAX_WEIGHT, 12};
        int[] a3 = new int[]{MAX_WEIGHT, MAX_WEIGHT, 0, 22, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, 8};
        int[] a4 = new int[]{MAX_WEIGHT, MAX_WEIGHT, 22, 0, 20, MAX_WEIGHT, MAX_WEIGHT, 16, 21};
        int[] a5 = new int[]{MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, 20, 0, 26, MAX_WEIGHT, 7, MAX_WEIGHT};
        int[] a6 = new int[]{11, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, 26, 0, 17, MAX_WEIGHT, MAX_WEIGHT};
        int[] a7 = new int[]{MAX_WEIGHT, 16, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, 17, 0, 19, MAX_WEIGHT};
        int[] a8 = new int[]{MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, 16, 7, MAX_WEIGHT, 19, 0, MAX_WEIGHT};
        int[] a9 = new int[]{MAX_WEIGHT, 12, 8, 21, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, 0};

        graph.matrix[0] = a1;
        graph.matrix[1] = a2;
        graph.matrix[2] = a3;
        graph.matrix[3] = a4;
        graph.matrix[4] = a5;
        graph.matrix[5] = a6;
        graph.matrix[6] = a7;
        graph.matrix[7] = a8;
        graph.matrix[8] = a9;

        graph.prim();
    }
}

运行结果

顶点:0,权值:10
顶点:0,权值:11
顶点:1,权值:12
顶点:8,权值:8
顶点:1,权值:16
顶点:6,权值:19
顶点:7,权值:7
顶点:7,权值:16
最小生成树权值和:99

二、克鲁斯卡尔(Kruskal算法)

2.1、核心思想

以边为数据结构来构造图,按照边的权重从小到大排列,然后依次相加,判断如果回环则跳过这条边,直至找到最后的最小权值。

2.2、步骤模拟
image-20220127202810611

1,首先我们以边来构造图得到右图表格,数据存储begin、end、weight

2,然后按顺序加起来,这里前7条边相加都不构成回环,到第8条(v5,v6),判断v5 -> v0 -> v1 -> v6已构成回环,所以跳过这条边。

3,依次重复。最小权值的边变化过程如下图所示

(v4,v7) +7
(v2,v8) +8
(v0,v1) +10
(v0,v5) +11
(v1,v8) +12
(v3,v7) +16
(v1,v6) +16
(v5,v6) 回环: v5 -> v0 -> v1 -> v6
(v1,v2) 回环: v1 -> v8 -> v2
(v6,v7) +19
(v3,v4) 回环: v3 -> v7 -> v4
(v3,v8) 回环: v3 -> v7 -> v6 -> v1 -> v8
(v2,v3) 回环: v2 -> v8 -> v3
(v3,v6) 回环: v3 -> v4 -> v7 -> v6
(v4,v5) 回环: v4 -> v3 -> v6 -> v5

4,最后得到最小生成树17+8+10+11+12+16+16+19=99,也就是解决了上述施工问题。

2.3、回环判断逻辑

1,构造长度为edgeSize的数组;

2,每次加入一条边就以begin为下标,end为数值存入数组;

如:edges(4,7)这条边 => [0,0,0,7,0] ,即 a[4]=7

3,同时通过判断新加入边的begin、end在数组中最终指向同一个值来判断回环

2.4、实现如下
// 克鲁斯卡尔算法
public class GraphKruskal {
    private Edge[] edges;
    private int edgeSize;

    public GraphKruskal(int edgeSize) {
        this.edgeSize = edgeSize;
        edges = new Edge[edgeSize];
    }

    public void miniSpanTreeKruskal() {
        int m, n, sum = 0;
        int[] parent = new int[edgeSize];//神奇的数组,下标为起点,值为终点
        for (int i = 0; i < edgeSize; i++) {
            parent[i] = 0;
        }
        for (int i = 0; i < edgeSize; i++) {
            n = find(parent, edges[i].begin);
            m = find(parent, edges[i].end);
            if (n != m) {
                parent[n] = m;
                System.out.println("边添加成功 -> (" + edges[i].begin + "," + edges[i].end + "): +" + edges[i].weight);
                sum += edges[i].weight;
            } else {
                System.out.println("第" + i + "条边回环了");
            }
        }
        System.out.println("sum:" + sum);
    }

    /*
     * 将神奇数组进行查询获取非回环的值
     */
    public int find(int[] parent, int f) {
        while (parent[f] > 0) {
            int temp = f;
            f = parent[f];
            System.out.println("找到路径:(" + temp + "," + f + ")");
        }
        return f;
    }

    public void createEdgeArray() {
        edges[0] = new Edge(4, 7, 7);
        edges[1] = new Edge(2, 8, 8);
        edges[2] = new Edge(0, 1, 10);
        edges[3] = new Edge(0, 5, 11);
        edges[4] = new Edge(1, 8, 12);
        edges[5] = new Edge(3, 7, 16);
        edges[6] = new Edge(1, 6, 16);
        edges[7] = new Edge(5, 6, 17);
        edges[8] = new Edge(1, 2, 18);
        edges[9] = new Edge(6, 7, 19);
        edges[10] = new Edge(3, 4, 20);
        edges[11] = new Edge(3, 8, 21);
        edges[12] = new Edge(2, 3, 22);
        edges[13] = new Edge(3, 6, 24);
        edges[14] = new Edge(4, 5, 26);
    }

    class Edge {
        private int begin;
        private int end;
        private int weight;

        public Edge(int begin, int end, int weight) {
            super();
            this.begin = begin;
            this.end = end;
            this.weight = weight;
        }
    }


    public static void main(String[] args) {
        GraphKruskal graphKruskal = new GraphKruskal(15);
        graphKruskal.createEdgeArray();
        graphKruskal.miniSpanTreeKruskal();
    }
}

运行结果:

边添加成功 -> (4,7): +7
边添加成功 -> (2,8): +8
边添加成功 -> (0,1): +10
找到路径:(0,1)
边添加成功 -> (0,5): +11
找到路径:(1,5)
边添加成功 -> (1,8): +12
边添加成功 -> (3,7): +16
找到路径:(1,5)
找到路径:(5,8)
边添加成功 -> (1,6): +16
找到路径:(5,8)
找到路径:(8,6)
第7条边回环了
找到路径:(1,5)
找到路径:(5,8)
找到路径:(8,6)
找到路径:(2,8)
找到路径:(8,6)
第8条边回环了
边添加成功 -> (6,7): +19
找到路径:(3,7)
找到路径:(4,7)
第10条边回环了
找到路径:(3,7)
找到路径:(8,6)
找到路径:(6,7)
第11条边回环了
找到路径:(2,8)
找到路径:(8,6)
找到路径:(6,7)
找到路径:(3,7)
第12条边回环了
找到路径:(3,7)
找到路径:(6,7)
第13条边回环了
找到路径:(4,7)
找到路径:(5,8)
找到路径:(8,6)
找到路径:(6,7)
第14条边回环了
sum:99

  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流星雨在线

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值