#P2767. 第3题-小红的连通块
-
1000ms
Tried: 10
Accepted: 6
Difficulty: 7
所属公司 :
阿里
时间 :2025年3月29日-阿里淘天(开发岗)
-
算法标签>并查集
第3题-小红的连通块
解题思路
本题要求我们在一个给定的图中,找到在不同连通块数量下,最多可以删除红色边获得的权值总和。这涉及到图的连通性和最小生成树(MST)的问题。我们将通过最小生成树(MST)来最优化删除红色边的策略,以最大化我们能够获得的权值。
最小生成树的基本概念
最小生成树是一个图的子图,它包括图中所有的节点,并且使得所有的边的总权值最小。
问题转化为最小生成树
在本题中,红色边的删除会增加连通块的数量,采用逆向思维,我们可以将问题转化为通过添加红色边,使得图的连通块数逐渐减小,且添加红色边后尽可能保留剩下红色边更大的权值。
思路分析
我们可以利用 Kruskal 算法来实现最小生成树的构建,并在过程中逐步考虑删除红色边。具体步骤如下:
-
初始化并查集:
- 我们使用并查集(Union-Find)来动态维护图中的连通块数。每次合并两个节点时,图中的连通块数减少。
- 初始时,图的连通块数为 n(每个节点自己是一个连通块)。
-
按权值排序红色边:
- 将所有红色边按照权值从小到大排序。通过从小到大考虑这些边,我们尽量保留较小的红色边以减少连通块数量,且保留更多的权值。
-
逐步添加权值小的红色边:
- 对于每个 k,我们希望尽可能地减少连通块的数量到 k,并获得最大的权值。
- 通过逐步合并节点,每合并一次,就意味着连通块数减少。
- 未使用的红色边就是被删除的红色边。
-
输出结果:
- 最终,对于每个 k(从 1 到 n),我们输出删除红色边后,连通块数不超过 k 时能获得的最大权值总和。
算法步骤
- 初始化:读入图的节点数 n 和边数 m,并初始化并查集。
- 处理红色边:将红色边按权值排序,准备按顺序进行处理。
- 逐步合并:通过合并操作减少连通块数,并更新收益。
- 输出结果:根据每个可能的连通块数量,输出对应的最大权值总和。
复杂度分析
-
时间复杂度:
- 初始化并查集需要 O(n)。
- 对所有边的处理(包括红色边的排序)需要 O(mlogm),其中 m 是边的数量。
- 并查集的每次
find和union操作的时间复杂度是 O(α(n)),其中 α(n) 是阿克曼函数的反函数,接近常数时间。
因此,总时间复杂度为 O(mlogm+mα(n)),其中 α(n) 可以认为是常数,因此时间复杂度大致为 O(mlogm)。
-
空间复杂度:
- 需要存储并查集的父节点、秩数组,空间复杂度为 O(n)。
- 额外存储红色边的数组,空间复杂度为 O(m)。
Python 代码
import sys
# 并查集数据结构
class UnionFind:
def __init__(self, size):
self.parent = list(range(size + 1)) # parent[i]表示节点i的父节点
self.rank = [0] * (size + 1) # rank[i]表示节点i的秩(树的高度)
self.count = size # 初始连通块数为n
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # 路径压缩
return self.parent[x]
def union(self, x, y):
x_root = self.find(x)
y_root = self.find(y)
if x_root == y_root:
return False # x和y已经在同一个连通块,不需要合并
if self.rank[x_root] < self.rank[y_root]:
self.parent[x_root] = y_root
else:
self.parent[y_root] = x_root
if self.rank[x_root] == self.rank[y_root]:
self.rank[x_root] += 1
self.count -= 1 # 合并后连通块数减1
return True
def main():
# 读入数据
input = sys.stdin.read().split()
ptr = 0
n = int(input[ptr]) # 节点数
ptr += 1
m = int(input[ptr]) # 边数
ptr += 1
uf = UnionFind(n) # 初始化并查集
sum_red = 0 # 红色边的权值和
red_edges = [] # 存储红色边
# 读入每一条边
for _ in range(m):
u = int(input[ptr])
ptr += 1
v = int(input[ptr])
ptr += 1
w = int(input[ptr])
ptr += 1
c = int(input[ptr])
ptr += 1
if c == 0:
uf.union(u, v) # 如果不是红色边,直接合并
else:
red_edges.append((u, v, w)) # 如果是红色边,存储
sum_red += w
# 初始连通块数
C0 = uf.count
# 对红色边按权值从小到大排序
red_edges.sort(key=lambda x: x[2])
# 存储在连通块数不超过k时,最大可以得到的权值
min_retained = [0]
sum_retained = 0
for u, v, w in red_edges:
root_u = uf.find(u)
root_v = uf.find(v)
if root_u != root_v: # 如果u和v不在同一个连通块
uf.union(root_u, root_v) # 合并它们
sum_retained += w # 增加红色边的权值
min_retained.append(sum_retained)
if len(min_retained) - 1 == C0 - 1:
break # 合并到连通块数只剩下一个时停止
# 输出结果
for k in range(1, n + 1):
if k >= C0:
print(sum_red, end=" ")
else:
t_needed = C0 - k
print(sum_red - min_retained[t_needed], end=" ")
if __name__ == "__main__":
main()
Java 代码
import java.util.*;
public class Main {
static class UnionFind {
int[] parent, rank;
int count;
public UnionFind(int size) {
parent = new int[size + 1];
rank = new int[size + 1];
for (int i = 1; i <= size; i++) {
parent[i] = i;
}
count = size;
}
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
public 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 {
parent[rootY] = rootX;
if (rank[rootX] == rank[rootY]) {
rank[rootX]++;
}
}
count--;
return true;
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
UnionFind uf = new UnionFind(n);
long sumRed = 0;
List<int[]> redEdges = new ArrayList<>();
for (int i = 0; i < m; i++) {
int u = sc.nextInt();
int v = sc.nextInt();
int w = sc.nextInt();
int c = sc.nextInt();
if (c == 0) {
uf.union(u, v);
} else {
redEdges.add(new int[] {u, v, w});
sumRed += w;
}
}
int C0 = uf.count;
redEdges.sort(Comparator.comparingInt(a -> a[2]));
List<Long> minRetained = new ArrayList<>();
long sumRetained = 0;
minRetained.add(0L);
for (int[] edge : redEdges) {
int u = edge[0];
int v = edge[1];
int w = edge[2];
int rootU = uf.find(u);
int rootV = uf.find(v);
if (rootU != rootV) {
uf.union(rootU, rootV);
sumRetained += w;
minRetained.add(sumRetained);
if (minRetained.size() - 1 == C0 - 1) {
break;
}
}
}
for (int k = 1; k <= n; k++) {
if (k >= C0) {
System.out.print(sumRed + " ");
} else {
int tNeeded = C0 - k;
System.out.print(sumRed - minRetained.get(tNeeded) + " ");
}
}
}
}
C++ 代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class UnionFind {
public:
vector<int> parent, rank;
int count;
UnionFind(int size) {
parent.resize(size + 1);
rank.resize(size + 1, 0);
for (int i = 1; i <= size; i++) {
parent[i] = i;
}
count = size;
}
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
bool unionSet(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 {
parent[rootY] = rootX;
if (rank[rootX] == rank[rootY]) {
rank[rootX]++;
}
}
count--;
return true;
}
};
int main() {
int n, m;
cin >> n >> m;
UnionFind uf(n);
long long sumRed = 0;
vector<tuple<int, int, int>> redEdges;
for (int i = 0; i < m; i++) {
int u, v, w, c;
cin >> u >> v >> w >> c;
if (c == 0) {
uf.unionSet(u, v);
} else {
redEdges.push_back({u, v, w});
sumRed += w;
}
}
int C0 = uf.count;
sort(redEdges.begin(), redEdges.end(), [](const tuple<int, int, int>& a, const tuple<int, int, int>& b) {
return get<2>(a) < get<2>(b);
});
vector<long long> minRetained = {0};
long long sumRetained = 0;
for (auto& edge : redEdges) {
int u = get<0>(edge);
int v = get<1>(edge);
int w = get<2>(edge);
int rootU = uf.find(u);
int rootV = uf.find(v);
if (rootU != rootV) {
uf.unionSet(rootU, rootV);
sumRetained += w;
minRetained.push_back(sumRetained);
if (minRetained.size() - 1 == C0 - 1) {
break;
}
}
}
for (int k = 1; k <= n; k++) {
if (k >= C0) {
cout << sumRed << " ";
} else {
int tNeeded = C0 - k;
cout << sumRed - minRetained[tNeeded] << " ";
}
}
return 0;
}
题目内容
小红有一张由 n 个节点、m 条边构成的图,每一条边都有一个权值,其中部分边被染成了红色,这些边是可以被删除的。对于每条染成红色的边,如果删除它,可以获得这条边的权值。
请你求出,对于每个 k(1≤k≤n),在最终图的连通块数量不超过 k 的条件下,小红最多能够获得的权值总和是多少。请依次输出 k=1,2,...,n 时的结果。
对于图上的两个点,如果它们之间有边相连,则称他们位于同一个连通块里。
输入描述
第一行包含两个整数 n 和 m(1≤n,m≤105),分别表示图中节点和边的数量。节点编号为 1 到 n。 接下来 m 行,第 i 行输入四个整数
ui,vi,wi,ci(1≤ui,ui≤n;1≤wi<109;0≤ci≤1)代表第 i 条边连接节点 ui 和 vi,这条边的权值为 wi ,其中 ci=1 表示该边被染成红色,ci=0 表示该边未被染色。
保证图中没有自环和重边,图初始是连通的。
输出描述
在一行上输出 n 个整数,其中,第 i 个整数代表当要求最终图的连通块数量小于等于 i 时,小红最多能够获得的受益总和。
样例1
输入
4 4
1 2 5 1
2 3 3 0
3 4 4 1
1 4 2 1
输出
5 9 11 11
说明
删除第一条边,连通块数量为 1 ,权值总和为 5 。
再删除第三条边,连通块数量为 2 ,权值总和为 9 。
再删除第四条边,连通块数量为 3 ,权值总和为 11 。
第二条边没有被染红,所以不会被删除。