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。
分步過程説明
- 問題理解與目標確認
- 初始狀態是一個連通的無向圖,目標是通過刪除一些邊,將圖劃分成最多
k個連通塊。 - 每個連通塊的代價定義為該塊內所有邊中最大的權重。如果一個連通塊內沒有邊(即只有孤立節點),其代價為0。
- 最終目標是讓所有連通塊代價中的最大值儘可能小。
- 核心思路:逆向思考與Kruskal算法
- 這個問題可以逆向思考:我們不是主動去“刪除”邊,而是像構建最小生成樹(MST)一樣,從空圖開始,逐步添加邊,將節點連接起來。
- 使用 Kruskal算法 的思想。Kruskal算法通過每次選擇當前最小的邊來構建MST,如果加入該邊不會形成環,則加入。這個過程天然地保證了在連通性變化的關鍵時刻,所使用的邊權是當前最小的。
- 我們的目標是得到最多
k個連通塊。在Kruskal算法中,初始時有n個連通塊(每個節點獨立)。每成功加入一條邊,就會合並兩個連通塊,使總連通塊數減少1。 - 因此,當連通塊數量恰好減少到
k個時,我們停止添加邊。此時,為了將這k個連通塊最終連接成一棵樹(即一個連通塊),還需要添加的邊中的最大權重,就是當前我們最後加入的那條邊的權重。這個權重就是我們要找的“最小可能的最大代價”。
- 算法詳細步驟
- 步驟一:排序邊
首先,將所有的邊按照權重(w)從小到大進行排序。這是Kruskal算法的標準第一步,目的是確保我們總是先嚐試權重最小的邊。 - 步驟二:初始化並查集
初始化一個並查集(Union-Find)數據結構。初始時,每個節點都是一個獨立的連通塊,因此連通塊的數量cc等於節點數n。 - 步驟三:按順序處理邊併合並
從小到大遍歷排序後的邊列表:
- 對於每條邊
[u, v, w],使用並查集檢查節點u和v當前是否屬於同一個連通塊。 - 如果不屬於同一個連通塊,則合併它們。這個合併操作會將兩個連通塊合併為一個,使總的連通塊數量
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;
}