题目描述

✅ 1135. 最低成本连通所有城市

题意分析

城市编号为 1 到 n,每条可用道路是带成本的无向边。要让任意两个城市之间都有路径,并使选中道路的总成本最小;无法全部连通时返回 -1。

道路成本非负,连通方案中若出现环,删去环上的一条边仍然连通且不会增加成本,所以总能用一棵生成树达到最优。Kruskal 算法按成本从小到大选边,每次连接两个原本分离的连通块。

解法:Kruskal + 并查集

核心思路

[!blue]

并查集记录已选道路形成的连通块。find(x) 找到城市所在块的代表,路径压缩让沿途节点直接指向代表;union 只在两端代表不同时合并,把 rank 较小的根挂到较大的根下,同秩合并时才把新根的 rank 加一。两端已经同块时,再选这条边会成环,因此跳过。

贪心选择可以用交换说明。设当前边 e 连接两个不同块,将其中一个块与其他城市分开;比 e 更便宜的边已经处理过,不可能还跨过这条分界,否则两端早已合并。若一棵包含已选边的最小生成树没有 e,加入 e 后产生的环中必有另一条跨界边。用 e 替换那条边不会增加成本,也不会删掉块内已经选定的边,因此仍有一棵最小生成树包含这次选择。

answer 累计成功选入的边权,edgeCount 统计成功合并次数。初始有 n 个城市连通块,每次成功合并恰好减少一个块,又始终不成环,所以 edgeCount == n-1 就意味着全部城市已经连通,可以返回总成本。

如果所有边处理完仍不足 n-1 次合并,剩余连通块之间没有可用道路,返回 -1。最后再次检查边数,也让只有一个城市、没有道路时返回零。数组开到 n+1 是为了直接使用城市编号,下标零不参与城市的连通判断。

解题步骤

  • 边按成本排序,并查集按城市编号初始化。
  • 两端同根跳过,否则合并并累计。
  • 选够 n−1 条返回,否则返回负一。

代码实现

class Solution {
    public int minimumCost(int n, int[][] connections) {
        Arrays.sort(connections, (x, y) -> Integer.compare(x[2], y[2]));

        UnionFind uf = new UnionFind(n + 1);
        int answer = 0;
        int edgeCount = 0;

        for (int[] edge : connections) {
            int cityA = edge[0];
            int cityB = edge[1];
            int cost = edge[2];

            // 已经连通的端点再加边会成环,不计成本和边数。
            if (!uf.union(cityA, cityB)) {
                continue;
            }

            answer += cost;
            edgeCount++;

            // 无环森林已有足够边数,此时所有城市已连通。
            if (edgeCount == n - 1) {
                return answer;
            }
        }

        // 单城市无边也已连通,最终按森林边数判断。
        return edgeCount == n - 1 ? answer : -1;
    }

    private static class UnionFind {
        private final int[] parent;
        private final int[] rank;

        UnionFind(int size) {
            parent = new int[size];
            rank = new int[size];

            for (int idx = 0; idx < size; idx++) {
                parent[idx] = idx;
            }
        }

        int find(int x) {
            if (parent[x] != x) {
                parent[x] = find(parent[x]);
            }

            return parent[x];
        }

        boolean union(int x, int y) {
            int rootX = find(x);
            int rootY = find(y);

            if (rootX == rootY) {
                return false;
            }

            if (rank[rootX] < rank[rootY]) {
                parent[rootX] = rootY;
            } else if (rank[rootX] > rank[rootY]) {
                parent[rootY] = rootX;
            } else {
                parent[rootY] = rootX;
                rank[rootX]++;
            }

            return true;
        }
    }
}
import "sort"

func minimumCost(n int, connections [][]int) int {
    sort.Slice(connections, func(i int, j int) bool {
        return connections[i][2] < connections[j][2]
    })

    uf := newUnionFind(n + 1)
    answer := 0
    edgeCount := 0

    for _, edge := range connections {
        cityA, cityB, cost := edge[0], edge[1], edge[2]
        // 已经连通的端点再加边会成环,不计成本和边数。
        if !uf.union(cityA, cityB) {
            continue
        }

        answer += cost
        edgeCount++
        // 无环森林已有足够边数,此时所有城市已连通。
        if edgeCount == n-1 {
            return answer
        }
    }

    // 无环森林已有足够边数,此时所有城市已连通。
    if edgeCount == n-1 {
        return answer
    }
    return -1
}

type unionFind struct {
    parent []int
    rank   []int
}

func newUnionFind(size int) *unionFind {
    parent := make([]int, size)
    rank := make([]int, size)
    for idx := 0; idx < size; idx++ {
        parent[idx] = idx
    }
    return &unionFind{parent: parent, rank: rank}
}

func (uf *unionFind) find(x int) int {
    if uf.parent[x] != x {
        uf.parent[x] = uf.find(uf.parent[x])
    }
    return uf.parent[x]
}

func (uf *unionFind) union(x int, y int) bool {
    rootX := uf.find(x)
    rootY := uf.find(y)
    if rootX == rootY {
        return false
    }
    if uf.rank[rootX] < uf.rank[rootY] {
        uf.parent[rootX] = rootY
    } else if uf.rank[rootX] > uf.rank[rootY] {
        uf.parent[rootY] = rootX
    } else {
        uf.parent[rootY] = rootX
        uf.rank[rootX]++
    }

    return true
}

复杂度分析

  • 时间复杂度:$O(n+E\log(E+1)+E\alpha(n))$。$E$ 为道路数,三部分分别对应并查集初始化、边排序,以及带路径压缩和按秩合并的并查集操作;$\alpha$ 是反阿克曼函数,增长极慢。
  • 空间复杂度:并查集 $O(n)$;Java 对象数组排序另需 $O(E)$,Go 排序栈 $O(\log(E+1))$。

关键点总结

[!green]

  • 选边数达到 n−1 的依据是已选边始终构成森林。
  • rank 是高度上界,路径压缩后不必等于实际高度。
  • 循环结束后仍按已选边数检查连通性,覆盖没有任何合并发生的单城市情形。

易错点总结

[!yellow]

  • 未排序就选边,只能得到某棵生成树而非最小。
  • 同根边仍计入数量,会在图未连通时提前结束。
  • 只看候选边数够不够,无法判断城市是否真的连通。

相似题目

题目 难度 关联与区别
1584. 连接所有点的最小费用 中等 同样求最小生成树,原题完整图的边权由曼哈顿距离生成,本题直接给出可用城市道路。
1168. 水资源分配优化 困难 把每个村庄建井的选择连到虚拟节点后,原题可转为包含虚拟点的最小生成树。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2024/74255171
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!