【图】最小生成数—Kruskal算法

目录

1. 什么是最小生成树?

2. Kruskal算法


1. 什么是最小生成树?

最小生成树(Minimum Spanning Tree,简称 MST)是指在一个连通无向图中,找到一个包含所有顶点的树,并且边的权值之和最小。

连通图:是指图中任意两个顶点之间都存在路径的图。

连通图中的每一棵生成树,都是原图的一个极大无环子图,即:从其中删去任何一条边,生成树 就不再连通;反之,在其中引入任何一条新边,都会形成一条回路
若连通图由n个顶点组成,则其生成树必含n个顶点和n-1条边。因此构造最小生成树的准则有三 条:
  1. 只能使用图中的边来构造最小生成树
  2. 只能使用恰好n-1条边来连接图中的n个顶点
  3. 选用的n-1条边不能构成回路
构造最小生成树的方法:Kruskal算法Prim算法。这两个算法都采用了逐步求解的贪心策略

2. Kruskal算法

算法描述:

  • 任给n个顶点;
  • 首先构造一个由这n个顶点组成、不含任何边的图,其中每个顶点自成一个连通分量;
  • 以顶点的每条边的权值排序(小->大),依次遍历排序后的边,对于每条边 (u, v),如果将其加入最小生成树不会形成回路,则将其加入图/最小生成树,并将顶点 u 和 v 合并为一个连通分量;
  • 如此重复,直到该图/最小生成树包含了图中的所有顶点。

直接拿例题中的示例模拟一下过程:

1584. 连接所有点的最小费用

给你一个points 数组,表示 2D 平面上的一些点,其中 points[i] = [xi, yi] 。

连接点 [xi, yi] 和点 [xj, yj] 的费用为它们之间的 曼哈顿距离 :|xi - xj| + |yi - yj| ,其中 |val| 表示 val 的绝对值。

请你返回将所有点连接的最小总费用。只有任意两点之间 有且仅有 一条简单路径时,才认为所有点都已连接。

根据算法原理进行分析:

  • 该图有5个顶点,任意一个顶点会有4条边,对于无向图来说A-B和B-A是同一条边。
  • 每个顶点是一个连通分量,我们先不管它。
  • 要对所有边进行排序,不断选取权值最小的一条,对于这一步,我们需要将所有边保存起来,并且不能重复存边(如A-B,B-A只能存一次)。
  • 每次选取的边,如果加入后,图中不会形成环(即这条边的两个顶点不能已经是一个连通块),则加入该条边,并将两个顶点合并成一个连通块。否则往后选取。

  •  反复上述操作,直到找到n-1条边,就找到了最小生成树。

本题的代码实现:

  1. 边与顶点的关系,用一个类edge进行描述。
  2. 边的排序,可以用ArrayList,我这里用的是优先级队列。
  3. 维护每个顶点之间的连通关系,这里最好就是使用并查集。【并查集】一种简单而强大高效的数据结构
import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;

public class LeetCode1584 {
    //1584. 连接所有点的最小费用
    static class Edge {
        private int len; //权值
        private int x; //顶点
        private int y; //顶点

        public Edge(int len, int x, int y) {
            this.len = len;
            this.x = x;
            this.y = y;
        }
    }

    public int minCostConnectPoints(int[][] points) {
        int n = points.length;
        //1.将连通图的所有边加入到优先级队列(以权值建小根堆)
        PriorityQueue<Edge> pq = new PriorityQueue<>(Comparator.comparingInt(o -> o.len));
        for (int i = 0; i < n; i++) {
            //不要重复存边
            for (int j = i + 1; j < n; j++) {
                int x = Math.abs(points[j][0] - points[i][0]);
                int y = Math.abs(points[j][1] - points[i][1]);
                pq.offer(new Edge((x + y), i, j));
            }
        }
        //2.贪心策略选边,并把选到边的对应两个顶点合并成一个连通块
        int count = 0;
        UnionFindSet ufs = new UnionFindSet(n);
        int sum = 0;
        while (!pq.isEmpty()) {
            Edge edge = pq.poll();
            int len = edge.len, x = edge.x, y = edge.y;
            //如果两个顶点不构成连通块,将结果加上这条边,并合并两个顶点
            if (ufs.union(x, y)) {
                sum += len;
                //n个顶点,选n - 1条边
                if (++count == n - 1) {
                    break;
                }
            }
        }
        return sum;
    }

    public static void main(String[] args) {
        System.out.println(new LeetCode1584().minCostConnectPoints(new int[][]{
                {0, 0}, {2, 2}, {3, 10}, {5, 2}, {7, 0}
        }));
    }
}

class UnionFindSet {

    //elem的值表示:如果大于0,表示这个点的父节点下标;如果小于0,表示该点是一个集合
    private int[] elem;

    public UnionFindSet(int n) { //参数n表示顶点个数
        this.elem = new int[n];
        Arrays.fill(elem, -1);
    }

    //合并两个顶点
    public boolean union(int x, int y) {
        x = find(x);
        y = find(y);
        //两个顶点已经是在同一个集合里了
        if (x == y) return false;
        elem[x] += elem[y];
        elem[y] = x;
        return true;
    }

    private int find(int x) {
        while (elem[x] >= 0) {
            x = elem[x];
        }
        return x;
    }
}

首先创建了一个小根堆优先队列,用来存储所有的边,并按照边的权值从小到大排序。然后通过贪心策略选取边,确保选择的边不会形成环路,并且合并相应的顶点,直到选取了足够数量的边,形成了最小生成树。

同时,还实现了一个并查集(UnionFindSet)来维护顶点之间的连通性,这是 Kruskal 算法的关键之一。这里的并查集并不用实现很多功能,根据题目需求自行调整代码逻辑,这样的效率更高。比如在合并的时候,就判断两个顶点是否已在同一个连通块,针对其返回值确定是否要加入最小生成树。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值