Bootstrap

代码随想录算法训练营第五十九天|prim算法、kruskal算法

53. 寻宝

在这里插入图片描述
在这里插入图片描述

Prim算法

Prim 算法用于寻找加权连通图的最小生成树(Minimum Spanning Tree,MST)。最小生成树是指连接图中所有顶点的最小权重的树。该算法从一个起始节点开始,不断选择与已加入生成树的节点集最近的节点,并将其加入生成树,直到所有节点都包含在生成树中。

实现思路:
1.初始化:

  • 创建一个邻接矩阵 grid 来存储图中每条边的权重,初始值为一个大数(如 10001),表示两点间没有边。
  • 创建一个数组 minDist,用于存储每个顶点到生成树的最小距离,初始值也为一个大数。
  • 创建一个布尔数组 isInTree,用于标记顶点是否已在生成树中。

2.读取输入数据:

  • 读取每条边的信息,并更新邻接矩阵 grid。

3.Prim 算法主循环:

  • 循环 v-1 次,每次将一个顶点加入生成树。
  • 在每次循环中,找到当前未在生成树中的最小距离顶点 cur。
  • 将 cur 顶点标记为已在生成树中。
  • 更新所有与 cur 相连的未在生成树中的顶点的最小距离。

prim三部曲

第一步:选距离生成树最近的非生成树节点

  • 选取最小生成树节点的条件:
    • (1)不在最小生成树里
    • (2)距离最小生成树最近的节点

第二步:最近节点(cur)加入生成树

第三步:更新非生成树节点到生成树的距离(即更新minDist数组)

  • cur节点加入之后, 最小生成树加入了新的节点,那么所有节点到 最小生成树的距离(即minDist数组)需要更新一下。由于cur节点是新加入到最小生成树,那么只需要关心与 cur 相连的 非生成树节点 的距离 是否比 原来 非生成树节点到生成树节点的距离更小了呢。
  • 更新的条件:
    • 节点是 非生成树里的节点
    • 与cur相连的某节点的权值 比 该某节点距离最小生成树的距离小

代码实现:

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int v = sc.nextInt();
        int e = sc.nextInt();
        int x, y, k;

        // grid 是一个邻接矩阵,用于存储图中每条边的权重,权重默认最大值,题目描述val最大为10000
        int[][] grid = new int[v + 1][v + 1];
        for (int i = 0; i <= v; i++) {
            Arrays.fill(grid[i], 10001);
        }

        while (e-- > 0) {
            x = sc.nextInt();
            y = sc.nextInt();
            k = sc.nextInt();
            // 因为是双向图,所以两个方向都要填上
            grid[x][y] = k;
            grid[y][x] = k;
        }

        // 所有节点到最小生成树的最小距离
        int[] minDist = new int[v + 1];
        Arrays.fill(minDist, 10001);

        // 用来判断是否是生成树节点
        boolean[] isInTree = new boolean[v + 1];

        // 我们只需要循环 n-1次,建立 n - 1条边,就可以把n个节点的图连在一起
        for (int i = 1; i < v; i++) {

            // 1、prim三部曲,第一步:选距离生成树最近的非生成树节点,
            int cur = -1; // 选中哪个节点 加入最小生成树
            int minVal = Integer.MAX_VALUE;
            for (int j = 1; j <= v; j++) { // 1 - v,顶点编号,这里下标从1开始
                //  选取最小生成树节点的条件:
                //  (1)不在最小生成树里
                //  (2)距离最小生成树最近的节点
                if (!isInTree[j] && minDist[j] < minVal) {
                    minVal = minDist[j];
                    cur = j;
                }
            }

            // 2、prim三部曲,第二步:最近节点(cur)加入生成树
            isInTree[cur] = true;

            // 3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组)
            // cur节点加入之后, 最小生成树加入了新的节点,那么所有节点到 最小生成树的距离(即minDist数组)需要更新一下
            // 由于cur节点是新加入到最小生成树,那么只需要关心与 cur 相连的 非生成树节点 的距离 是否比 原来 非生成树节点到生成树节点的距离更小了呢
            for (int j = 1; j <= v; j++) {
                // 更新的条件:
                // (1)节点是 非生成树里的节点
                // (2)与cur相连的某节点的权值 比 该某节点距离最小生成树的距离小
                // 很多录友看到自己 就想不明白什么意思,其实就是 cur 是新加入 最小生成树的节点,那么 所有非生成树的节点距离生成树节点的最近距离 由于 cur的新加入,需要更新一下数据了
                if (!isInTree[j] && grid[cur][j] < minDist[j]) {
                    minDist[j] = grid[cur][j];
                }
            }
        }

        // 统计结果
        int result = 0;
        for (int i = 2; i <= v; i++) { // 不计第一个顶点,因为统计的是边的权值,v个节点有 v-1条边
            result += minDist[i];
        }
        System.out.println(result);
    }

拓展:如果让打印出来 最小生成树的每条边呢?

此时有两个问题:

  1. 用什么结构来记录
  2. 如何记录

用什么结构来记录?

我们使用一维数组就可以记录。 parent[节点编号] = 节点编号, 这样就把一条边记录下来了。(当然如果节点编号非常大,可以考虑使用map)

如何记录?
根据 minDist数组,选组距离 生成树 最近的节点 加入生成树,那么 minDist数组里记录的其实也是 最小生成树的边的权值。所以应该在更新 minDist数组 的时候,去更新parent数组来记录一下对应的边

kruskal算法

Kruskal算法是用于寻找加权无向连通图的最小生成树(Minimum Spanning Tree, MST)的贪心算法。其核心思想是按边的权重从小到大进行排序,然后依次选择权重最小且不形成环的边,直至构建出最小生成树。

实现思路:
1.初始化:

  • 创建一个列表 edges 来存储所有边的信息。
  • 初始化并查集 UnionFind,用于判断顶点是否在同一个集合中并进行集合的合并操作。

2.读取输入数据:

  • 读取所有边的信息,并存储到 edges 列表中。

3.边排序:

  • 按边的权值从小到大排序。

4.构建最小生成树:

  • 遍历排序后的边列表,对于每条边,判断其两个顶点是否在同一个集合中。
  • 如果两个顶点不在同一个集合中,则将这条边加入最小生成树,并进行集合的合并操作。
  • 累加加入边的权值到 resultVal 中。

代码实现

public class Main {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // 读取节点数和边数
        int v = scanner.nextInt();
        int e = scanner.nextInt();
        List<Edge> edges = new ArrayList<>();
        int resultVal = 0;

        // 读取所有边的信息
        for (int i = 0; i < e; i++) {
            int x = scanner.nextInt();
            int y = scanner.nextInt();
            int val = scanner.nextInt();
            edges.add(new Edge(x, y, val));
        }

        // 按边的权值从小到大排序
        edges.sort(Comparator.comparingInt(edge -> edge.val));

        // 初始化并查集
        UnionFind uf = new UnionFind(v + 1);

        // 遍历所有边,使用Kruskal算法构建最小生成树
        for (Edge edge : edges) {
            int x = uf.find(edge.l);
            int y = uf.find(edge.r);
            // 如果两个节点不在同一个集合中,加入这条边
            if (x != y) {
                resultVal += edge.val;
                uf.join(x, y);
            }
        }

        // 输出最小生成树的总权值
        System.out.println(resultVal);
        scanner.close();
    }

    // 并查集类
    static class UnionFind {
        private int n;
        private int[] father;

        public UnionFind(int n) {
            this.n = n;
            father = new int[n];
            init();
        }

        // 初始化并查集,每个节点的父节点初始化为自己
        private void init() {
            for (int i = 0; i < n; i++) {
                father[i] = i;
            }
        }

        // 判断两个节点是否在同一个集合中
        public boolean isSame(int u, int v) {
            return find(u) == find(v);
        }

        // 合并两个节点所在的集合
        public void join(int u, int v) {
            u = find(u);
            v = find(v);
            if (u != v) {
                father[v] = u;
            }
        }

        // 查找节点u的根节点,并进行路径压缩
        public int find(int u) {
            return father[u] == u ? u : (father[u] = find(father[u]));
        }
    }

    // 边类
    static class Edge {
        int l, r, val;

        Edge(int l, int r, int val) {
            this.l = l;
            this.r = r;
            this.val = val;
        }
    }

}

拓展:如果题目要求将最小生成树的边输出的话,应该怎么办呢?

Kruskal 算法 输出边的话,相对prim 要容易很多,因为 Kruskal 本来就是直接操作边,边的结构自然清晰,不用像 prim一样 需要再节点练成线输出边 (因为prim是对节点操作,而 Kruskal是对边操作,这是本质区别)

当判断两个节点不在同一个集合的时候,这两个节点的边就加入到最小生成树, 所以添加边的操作在这里:

        // 遍历所有边,使用Kruskal算法构建最小生成树
        for (Edge edge : edges) {
            int x = uf.find(edge.l);
            int y = uf.find(edge.r);
            // 如果两个节点不在同一个集合中,加入这条边
            if (x != y) {
                result.put(edge);// 记录最小生成树的边
                resultVal += edge.val;
                uf.join(x, y);
            }
        }

Kruskal 和 Prim 对比

什么情况用哪个算法更合适呢。

Kruskal 与 prim 的关键区别在于,prim维护的是节点的集合,而 Kruskal 维护的是边的集合。 如果 一个图中,节点多,但边相对较少,那么使用Kruskal 更优。

为什么边少的话,使用 Kruskal 更优呢?

因为 Kruskal 是对边进行排序的后 进行操作是否加入到最小生成树。
边如果少,那么遍历操作的次数就少。
在节点数量固定的情况下,图中的边越少,Kruskal 需要遍历的边也就越少。
而 prim 算法是对节点进行操作的,节点数量越少,prim算法效率就越优。
所以在 稀疏图中,用Kruskal更优。 在稠密图中,用prim算法更优。
边数量较少为稀疏图,接近或等于完全图(所有节点皆相连)为稠密图

Prim 算法 时间复杂度为 O(n^2),其中 n 为节点数量,它的运行效率和图中边树无关,适用稠密图。
Kruskal算法 时间复杂度 为 nlogn,其中n 为边的数量,适用稀疏图。

;