2025-12-22:最小化连通分量的最大成本。用go语言,给出一个连通的无向图,节点编号为 0 到 n-1,边集用数组 edges 表示,其中每条边 edge

12 阅读7分钟

2025-12-22:最小化连通分量的最大成本。用go语言,给出一个连通的无向图,节点编号为 0 到 n-1,边集用数组 edges 表示,其中每条边 edges[i] = [u, v, w] 连接 u 和 v,权重为 w。允许删除任意若干条边,使得剩下的图被划分成最多 k 个连通块(connected components)。

对每个连通块,把其中所有边的最大权值视为该块的代价;若某个连通块没有任何边,则其代价为 0。目标是通过删除边来最小化所有连通块代价的最大值,并返回这个最小可能的最大代价。 换句话说,就是把顶点拆成不超过 k 个组(通过删边实现),每组的代价为组内边权的最大值,要求使得所有组中代价的最大值尽可能小。

1 <= n <= 50000。

0 <= edges.length <= 100000。

edges[i].length == 3。

0 <= ui, vi < n。

1 <= wi <= 1000000。

1 <= k <= n。

输入图是连通图。

输入: n = 5, edges = [[0,1,4],[1,2,3],[1,3,2],[3,4,6]], k = 2。

输出: 4。

解释:

在这里插入图片描述

移除节点 3 和节点 4 之间的边(权值为 6)。

最终的连通分量成本分别为 0 和 4,因此最大代价为 4。

题目来自力扣3613。

分步过程说明

  1. 问题理解与目标确认

    • 初始状态是一个连通的无向图,目标是通过删除一些边,将图划分成最多 k 个连通块
    • 每个连通块的代价定义为该块内所有边中最大的权重。如果一个连通块内没有边(即只有孤立节点),其代价为0。
    • 最终目标是让所有连通块代价中的最大值尽可能小
  2. 核心思路:逆向思考与Kruskal算法

    • 这个问题可以逆向思考:我们不是主动去“删除”边,而是像构建最小生成树(MST)一样,从空图开始,逐步添加边,将节点连接起来。
    • 使用 Kruskal算法 的思想。Kruskal算法通过每次选择当前最小的边来构建MST,如果加入该边不会形成环,则加入。这个过程天然地保证了在连通性变化的关键时刻,所使用的边权是当前最小的。
    • 我们的目标是得到最多 k 个连通块。在Kruskal算法中,初始时有 n 个连通块(每个节点独立)。每成功加入一条边,就会合并两个连通块,使总连通块数减少1。
    • 因此,当连通块数量恰好减少到 k 个时,我们停止添加边。此时,为了将这 k 个连通块最终连接成一棵树(即一个连通块),还需要添加的边中的最大权重,就是当前我们最后加入的那条边的权重。这个权重就是我们要找的“最小可能的最大代价”。
  3. 算法详细步骤

    • 步骤一:排序边 首先,将所有的边按照权重(w)从小到大进行排序。这是Kruskal算法的标准第一步,目的是确保我们总是先尝试权重最小的边。
    • 步骤二:初始化并查集 初始化一个并查集(Union-Find)数据结构。初始时,每个节点都是一个独立的连通块,因此连通块的数量 cc 等于节点数 n
    • 步骤三:按顺序处理边并合并 从小到大遍历排序后的边列表:
      • 对于每条边 [u, v, w],使用并查集检查节点 uv 当前是否属于同一个连通块。
      • 如果不属于同一个连通块,则合并它们。这个合并操作会将两个连通块合并为一个,使总的连通块数量 cc 减一。
      • 在每次成功合并后,立即检查当前的连通块数量 cc 是否等于目标 k
    • 步骤四:判断终止条件并返回结果
      • 一旦在合并某条边后,连通块数量 cc 等于 k,算法立即停止。
      • 此时,当前这条边的权重 w 就是答案。这是因为这条边是我们在达到 k 个连通块之前,为了减少连通块数量而加入的最后一条边。它的权重代表了为了形成这 k 个连通块,我们所必须接受的连通块内部的最大边权
      • 如果遍历完所有边后连通块数量仍大于 k(根据题目输入的连通性,这种情况不会发生),则抛出异常。

复杂度分析

总的额外空间复杂度

  • O(n + m),其中 n 是节点数,m 是边数。
    • 并查集数据结构:需要存储每个节点的父节点信息,空间为 O(n)。
    • 边的存储:对输入的边进行排序,需要 O(m) 的空间。
    • 因此,总的空间复杂度主要由并查集和边数组决定,为 O(n + m)

总的时间复杂度

  • O(m log m)
    • 边的排序:这是算法中耗时最多的部分,时间复杂度为 O(m log m)
    • 并查集操作:对于每条边,我们进行最多两次的 Find 操作和可能的 Union 操作。当并查集采用了路径压缩优化时,这些操作的平均时间复杂度接近常数 O(α(n)),其中 α(n) 是反阿克曼函数,增长极其缓慢。因此,处理 m 条边的时间复杂度约为 O(m * α(n)),这在实践中可以近似看作 O(m)。
    • 综合来看,整个算法的总时间复杂度由排序步骤主导,为 O(m log m)

Go完整代码如下:

package main

import (
	"fmt"
	"slices"
)

// 完整的并查集模板,见我的数据结构题单
type unionFind struct {
	fa []int // 代表元
	cc int   // 连通块个数
}

func newUnionFind(n int) unionFind {
	fa := make([]int, n)
	// 一开始有 n 个集合 {0}, {1}, ..., {n-1}
	// 集合 i 的代表元是自己
	for i := range fa {
		fa[i] = i
	}
	return unionFind{fa, n}
}

// 返回 x 所在集合的代表元
// 同时做路径压缩,也就是把 x 所在集合中的所有元素的 fa 都改成代表元
func (u unionFind) find(x int) int {
	// 如果 fa[x] == x,则表示 x 是代表元
	if u.fa[x] != x {
		u.fa[x] = u.find(u.fa[x]) // fa 改成代表元
	}
	return u.fa[x]
}

// 把 from 所在集合合并到 to 所在集合中
func (u *unionFind) merge(from, to int) {
	x, y := u.find(from), u.find(to)
	if x == y { // from 和 to 在同一个集合,不做合并
		return
	}
	u.fa[x] = y // 合并集合。修改后就可以认为 from 和 to 在同一个集合了
	u.cc--      // 成功合并,连通块个数减一
}

func minCost(n int, edges [][]int, k int) int {
	if k == n {
		return 0
	}

	slices.SortFunc(edges, func(a, b []int) int { return a[2] - b[2] })
	u := newUnionFind(n)
	for _, e := range edges {
		u.merge(e[0], e[1])
		if u.cc == k {
			return e[2]
		}
	}
	panic("impossible")
}

func main() {
	n := 5
	edges := [][]int{{0, 1, 4}, {1, 2, 3}, {1, 3, 2}, {3, 4, 6}}
	k := 2
	result := minCost(n, edges, k)
	fmt.Println(result)
}

在这里插入图片描述

Python完整代码如下:

# -*-coding:utf-8-*-

from typing import List

class UnionFind:
    def __init__(self, n: int):
        self.fa = list(range(n))  # 代表元
        self.cc = n               # 连通块个数
    
    def find(self, x: int) -> int:
        """返回 x 所在集合的代表元,同时做路径压缩"""
        if self.fa[x] != x:
            self.fa[x] = self.find(self.fa[x])
        return self.fa[x]
    
    def merge(self, from_: int, to: int) -> None:
        """把 from_ 所在集合合并到 to 所在集合中"""
        x, y = self.find(from_), self.find(to)
        if x == y:  # from_ 和 to 在同一个集合,不做合并
            return
        self.fa[x] = y  # 合并集合
        self.cc -= 1    # 成功合并,连通块个数减一

def minCost(n: int, edges: List[List[int]], k: int) -> int:
    if k == n:
        return 0
    
    # 按边权从小到大排序
    edges.sort(key=lambda e: e[2])
    uf = UnionFind(n)
    
    for e in edges:
        uf.merge(e[0], e[1])
        if uf.cc == k:
            return e[2]
    
    raise ValueError("impossible")

if __name__ == "__main__":
    n = 5
    edges = [[0, 1, 4], [1, 2, 3], [1, 3, 2], [3, 4, 6]]
    k = 2
    result = minCost(n, edges, k)
    print(result)  

在这里插入图片描述

C++完整代码如下:

#include <iostream>
#include <vector>
#include <algorithm>
#include <stdexcept>

using namespace std;

class UnionFind {
private:
    vector<int> fa; // 代表元
    int cc;         // 连通块个数

public:
    // 构造函数
    UnionFind(int n) : fa(n), cc(n) {
        // 一开始有 n 个集合 {0}, {1}, ..., {n-1}
        // 集合 i 的代表元是自己
        for (int i = 0; i < n; i++) {
            fa[i] = i;
        }
    }

    // 返回 x 所在集合的代表元,同时做路径压缩
    int find(int x) {
        if (fa[x] != x) {
            fa[x] = find(fa[x]); // fa 改成代表元
        }
        return fa[x];
    }

    // 把 from 所在集合合并到 to 所在集合中
    void merge(int from, int to) {
        int x = find(from);
        int y = find(to);
        if (x == y) { // from 和 to 在同一个集合,不做合并
            return;
        }
        fa[x] = y; // 合并集合
        cc--;      // 成功合并,连通块个数减一
    }

    // 获取当前连通块个数
    int getCC() const {
        return cc;
    }
};

int minCost(int n, vector<vector<int>>& edges, int k) {
    if (k == n) {
        return 0;
    }

    // 按边权从小到大排序
    sort(edges.begin(), edges.end(),
         [](const vector<int>& a, const vector<int>& b) {
             return a[2] < b[2];
         });

    UnionFind uf(n);
    for (const auto& e : edges) {
        uf.merge(e[0], e[1]);
        if (uf.getCC() == k) {
            return e[2];
        }
    }

    throw runtime_error("impossible");
}

int main() {
    int n = 5;
    vector<vector<int>> edges = {{0, 1, 4}, {1, 2, 3}, {1, 3, 2}, {3, 4, 6}};
    int k = 2;

    int result = minCost(n, edges, k);
    cout << result << endl;

    return 0;
}

在这里插入图片描述