Medium

题目描述

给定一个有 n 个节点的无向连通图,节点标号为 0 到 n-1,以及一个二维整数数组 edges,其中 edges[i] = [ui, vi, wi] 表示节点 ui 和 vi 之间有一条权重为 wi 的无向边,还给定一个整数 k。

你可以从图中删除任意数量的边,使得结果图最多有 k 个连通分量。

一个连通分量的成本定义为该分量中的最大边权重。如果一个分量没有边,则其成本为 0。

返回删除边后所有分量中最大成本的最小可能值。

示例 1:

输入:n = 5, edges = [[0,1,4],[1,2,3],[1,3,2],[3,4,6]], k = 2
输出:4
解释:
删除节点 3 和 4 之间的边(权重 6)。
得到的分量成本为 0 和 4,所以总体最大成本为 4。

示例 2:

输入:n = 4, edges = [[0,1,5],[1,2,5],[2,3,5]], k = 1
输出:5
解释:
不能删除任何边,因为只允许一个分量(k = 1)需要图保持完全连通。
单个分量的成本等于其最大边权重,即 5。

约束条件:

  • 1 <= n <= 5 * 10^4
  • 0 <= edges.length <= 10^5
  • edges[i].length == 3
  • 0 <= ui, vi < n
  • 1 <= wi <= 10^6
  • 1 <= k <= n
  • 输入图是连通的

解题思路

这是一个结合二分搜索和并查集的经典问题。核心思想是二分答案:

解题思路:

  1. 二分搜索框架:对最大成本进行二分搜索。左边界为0,右边界为所有边权重的最大值。

  2. 可行性检查:对于每个候选答案mid,我们需要判断是否能通过删除一些边,使得:

    • 图最多有k个连通分量
    • 每个分量的最大边权重不超过mid
  3. 并查集实现:只保留权重不超过mid的边,用并查集统计连通分量数量。如果分量数量不超过k,说明当前mid是可行的。

  4. 优化策略

    • 首先按边权重排序
    • 在二分搜索中,只考虑权重不超过mid的边
    • 使用并查集高效统计连通分量

算法流程:

  • 提取所有边权重并排序去重,作为二分搜索的候选值
  • 二分搜索最小的可行答案
  • 对每个候选值,构建只包含不超过该权重的边的图,统计连通分量数量

这种方法的关键洞察是:如果某个权重W可行,那么所有大于W的权重也都可行,具有单调性。

代码实现

class Solution {
public:
    int minCost(int n, vector<vector<int>>& edges, int k) {
        // 提取并排序所有不同的权重
        set<int> weightSet;
        for (auto& edge : edges) {
            weightSet.insert(edge[2]);
        }
        vector<int> weights(weightSet.begin(), weightSet.end());
        weights.insert(weights.begin(), 0); // 添加0作为最小值
        
        // 二分搜索
        int left = 0, right = weights.size() - 1;
        int result = weights[right];
        
        while (left <= right) {
            int mid = left + (right - left) / 2;
            int maxWeight = weights[mid];
            
            if (canAchieve(n, edges, k, maxWeight)) {
                result = maxWeight;
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        
        return result;
    }
    
private:
    bool canAchieve(int n, vector<vector<int>>& edges, int k, int maxWeight) {
        // 并查集
        vector<int> parent(n);
        iota(parent.begin(), parent.end(), 0);
        
        function<int(int)> find = [&](int x) {
            return parent[x] == x ? x : parent[x] = find(parent[x]);
        };
        
        // 只连接权重不超过maxWeight的边
        for (auto& edge : edges) {
            if (edge[2] <= maxWeight) {
                int u = find(edge[0]);
                int v = find(edge[1]);
                if (u != v) {
                    parent[u] = v;
                }
            }
        }
        
        // 统计连通分量数量
        set<int> components;
        for (int i = 0; i < n; i++) {
            components.insert(find(i));
        }
        
        return components.size() <= k;
    }
};
class Solution:
    def minCost(self, n: int, edges: List[List[int]], k: int) -> int:
        # 提取并排序所有不同的权重
        weights = sorted(set(edge[2] for edge in edges))
        weights = [0] + weights  # 添加0作为最小值
        
        # 二分搜索
        left, right = 0, len(weights) - 1
        result = weights[right]
        
        while left <= right:
            mid = (left + right) // 2
            max_weight = weights[mid]
            
            if self.can_achieve(n, edges, k, max_weight):
                result = max_weight
                right = mid - 1
            else:
                left = mid + 1
        
        return result
    
    def can_achieve(self, n: int, edges: List[List[int]], k: int, max_weight: int) -> bool:
        # 并查集
        parent = list(range(n))
        
        def find(x):
            if parent[x] != x:
                parent[x] = find(parent[x])
            return parent[x]
        
        # 只连接权重不超过max_weight的边
        for u, v, w in edges:
            if w <= max_weight:
                root_u, root_v = find(u), find(v)
                if root_u != root_v:
                    parent[root_u] = root_v
        
        # 统计连通分量数量
        components = len(set(find(i) for i in range(n)))
        
        return components <= k
public class Solution {
    public int MinCost(int n, int[][] edges, int k) {
        // 提取并排序所有不同的权重
        var weightSet = new HashSet<int>();
        foreach (var edge in edges) {
            weightSet.Add(edge[2]);
        }
        var weights = weightSet.OrderBy(x => x).ToList();
        weights.Insert(0, 0); // 添加0作为最小值
        
        // 二分搜索
        int left = 0, right = weights.Count - 1;
        int result = weights[right];
        
        while (left <= right) {
            int mid = left + (right - left) / 2;
            int maxWeight = weights[mid];
            
            if (CanAchieve(n, edges, k, maxWeight)) {
                result = maxWeight;
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        
        return result;
    }
    
    private bool CanAchieve(int n, int[][] edges, int k, int maxWeight) {
        // 并查集
        int[] parent = new int[n];
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }
        
        int Find(int x) {
            if (parent[x] != x) {
                parent[x] = Find(parent[x]);
            }
            return parent[x];
        }
        
        // 只连接权重不超过maxWeight的边
        foreach (var edge in edges) {
            if (edge[2] <= maxWeight) {
                int rootU = Find(edge[0]);
                int rootV = Find(edge[1]);
                if (rootU != rootV) {
                    parent[rootU] = rootV;
                }
            }
        }
        
        // 统计连通分量数量
        var components = new HashSet<int>();
        for (int i = 0; i < n; i++) {
            components.Add(Find(i));
        }
        
        return components.Count <= k;
    }
}
var minCost = function(n, edges, k) {
    // 提取并排序所有不同的权重
    const weightSet = new Set();
    for (const edge of edges) {
        weightSet.add(edge[2]);
    }
    const weights = [0, ...Array.from(weightSet).sort((a, b) => a - b)];
    
    // 二分搜索
    let left = 0, right = weights.length - 1;
    let result = weights[right];
    
    while (left <= right) {
        const mid = Math.floor((left + right) / 2);
        const maxWeight = weights[mid];
        
        if (canAchieve(n, edges, k, maxWeight)) {
            result = maxWeight;
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    
    return result;
};

function canAchieve(n, edges, k, maxWeight) {
    // 并查集
    const parent = Array.from({length: n}, (_, i) => i);
    
    function find(x) {
        if (parent[x] !== x) {
            parent[x] = find(parent[x]);
        }
        return parent[x];
    }
    
    // 只连接权重不超过maxWeight的边
    for (const [u, v, w] of edges) {
        if (w <= maxWeight) {
            const rootU = find(u);
            const rootV = find(v);
            if (rootU !== rootV) {
                parent[rootU] = rootV;
            }
        }
    }
    
    // 统计连通分量数量
    const components = new Set();
    for (let i = 0; i < n; i++) {
        components.add(find(i));
    }
    
    return components.size <= k;
}

复杂度分析

复杂度类型
时间复杂度O(E log W · (E + N))
空间复杂度O(N + W)

其中:

  • E 是边的数量
  • N 是节点数量
  • W 是不同权重值的数量
  • 二分搜索需要 O(log W) 次
  • 每次检查需要 O(E + N) 时间进行并查集操作