现需要在某城市进行 5G 网络建设,已经选取 N
个地点设置 5G 基站,编号固定为 1
到 N
,接下来需要各个基站之间使用光纤进行连接以确保基站能互联互通,不同基站之间架设光纤的成本各不相同,且有些节点之间已经存在光纤相连,请你设计算法,计算出能联通这些基站的最小成本是多少。
注意,基站的联通具有传递性,即基站 A
与基站 B
架设了光纤,基站 B
与基站 C
也架设了光纤,则基站 A
与基站 C
视为可以互相联通
第一行输入表示基站的个数 N
,其中 0 < N <= 20
第二行输入表示具备光纤直连条件的基站对的数目 M
,其中 0 < M < N * (N - 1) / 2
第三行开始连续输入 M
行数据,格式为 X Y Z P
,其中 X Y
表示基站的编号,0 < X <= N
, 0 < Y <= N
且 X
不等于 Y
, Z
表示在 X Y
之间架设光纤的成本,其中 0 < Z < 100
,P
表示是否已存在光纤连接,0
表示未连接, 1
表示已连接。
如果给定条件,可以建设成功互联互通的 5G 网络,则输出最小的建设成本,
如果给定条件,无法建设成功互联互通的 5G 网络,则输出-1
3
3
1 2 3 0
1 3 1 0
2 3 5 0
4
只需要在 1,2
以及 2,3
基站之间铺设光纤,其成本为 3+1=4
3
1
1 2 5 0
-1
3` 基站无法与其他基站连接,输出`-1
3
3
1 2 3 0
1 3 1 0
2 3 5 1
1
2,3` 基站已有光纤相连,只有要再 `1,3` 站点 `2` 向铺设,其成本为 `1
题目要求生成连接所有节点的最小成本,其中部分节点已经连接。显然这是一个最小生成树的问题,经典的解法有Kruskal算法和Prim算法,属于比较难的算法,找个时间给大家补一下这部分相关知识。
对于包含n
个节点的树,必然存在n-1
条边。这个结论是最小生成树算法的一个基础出发点:我们需要在所有可能的边中选出n-1
条能够把所有节点连接的边,并使得这些边的权值的和尽可能地小。
不管是Kruskal算法还是Prim算法,都是基于排序和贪心的算法。
Kruskal算法是基于所有边权值排序的算法。传统的Kruskal算法包含以下步骤
edges
中,并按照权重进行从小到大排序Z
,所连接的节点分别为X
和Y
。若
X
和Y
连接后不会形成环,即X
和Y
原本属于两个不同的连通块。则
union(X, Y)
Z
的边应该被选择,ans += Z
1
,即edge_num += 1
X
和Y
连接后会形成环,即X
和Y
原本就属于同一个连通块。则‘
edge_num
等于n-1
。那么对于本题而言,已经存在了若干点之间已经存在边,应该对上述过程做出相应的调整。考虑边的时候,若
P = 0
,则和原方法一样先储存在数组edges
中P = 1
,则需要判断此时的两个节点X
和Y
是否已经属于同一个连通块。若
X
和Y
已经属于同一个连通块,即在之前的已经存在的边已经连通了,find(X) == find(Y)
,那么直接跳过这条边。X
和Y
不属于同一个连通块,则需要令它们合并,即union(X, Y)
,同时边数加1,即edge_num += 1
。Prim算法是基于当前已连通集合的外延边权值排序的算法。传统的Prim算法包含以下步骤
neighbor_dic
,邻接表的key
为节点编号,value
为该节点所有邻接节点nxt_node
以及构成的边的权值Z
所构成的数组,以(Z, nxt_node)
二元组的方式进行存储。cur_node
,一般选择cur_node = 0
或cur_node = 1
。heap
,用于储存若干待连接的边。在小根堆中,边权值Z
更小的(Z, nxt_node)
二元组会被储存在堆顶。node_used
,用于储存若干已经连通的节点。cur_node
出发,把起始点作为已连通集合的出发点。已连通集合不断往外扩散构建边,由于需要构建n-1
条边,该过程直接在一个循环n-1
次的for
循环中进行。其具体过程如下
cur_node
在邻接表eighbor_dic[cur_node]
中的所有近邻点nxt_node
,其构成的边的权值为Z
。若
nxt_node
已经出现在集合node_used
中,说明该近邻点已经位于已连通集合中,直接跳过。nxt_node
尚未出现在集合node_used
中,说明该近邻点尚未位于已连通集合中,将这条边以(Z, nxt_node)
二元组的形式加入小根堆中。cur_node
的所有近邻点以及构成的边都加入小根堆中之后,使用while
循环反复考虑堆顶元素。若
while
循环。heap[0][1]
已经位于已连通集合中,则弹出堆顶元素,考虑下一个堆顶元素。heap[0][1]
尚未位于已连通集合中,则这个节点将成为当前已连通集合的下一个的外延节点,退出while
循环。heap[0]
即为当前已连通集合的新的外延节点。需要
heap[0][1]
对应的边权值heap[0][0]
计入总权值ans
中heap[0][1]
加入集合node_used
,表示成为已连通集合的一部分。heap[0][1]
设置为新的cur_node
,因为新加入的这个节点会带来更多的近邻点和边。那么对于本题而言,已经存在了若干点之间已经存在边,应该对上述过程做出相应的调整。需要做出如下调整
key
,集合中每一个节点的非本集合邻接节点nxt_node
以及构成的边的权值Z
所构成的数组作为value
,构建出对应的邻接表set_neighbor_dic
set_neighbor_dic
进行建堆过程。注意剩余需要构建的边数为N-1-edge_num
。# 题目:2023B-建设5G网络
# 分值:200
# 作者:闭着眼睛学数理化
# 算法:最小生成树Kruskal算法
# 代码看不懂的地方,请直接在群上提问
# 并查集的查:寻找节点x的根节点
def find(x):
if parents[x] != x:
parents[x] = find(parents[x])
return parents[x]
# 并查集的并:将节点x和节点y合并在同一个集合中
# 即设置x的根节点的父节点为y的根节点
def union(x, y):
parents[find(x)] = find(y)
N = int(input())
M = int(input())
# 父节点数组,parents[i]储存i的父节点,初始化为他们自身
# 注意编号是从1开始的,所以数组长度设置为n+1
parents = [i for i in range(N+1)]
# 初始化树的边数为0
edge_num = 0
# 初始化记录所有尚未连接的边的情况
edges = list()
# 遍历M条边的关系
for _ in range(M):
X, Y, Z, P = map(int, input().split())
# 如果节点X和Y已经连接,
if P == 1:
# 如果X和Y已经属于同一个集合,则直接跳过
if find(X) == find(Y):
continue
# 如果X和Y不属于一个集合,则令他们合并,同时边数+1
else:
union(X, Y)
edge_num += 1
# 如果节点X和Y还没有连接,则记录该条边的情况
else:
# 储存边的权重Z,两个节点X和Y
edges.append((Z, X, Y))
# 令所有尚未连接的边从小到大排列
edges.sort()
# 答案变量ans
ans = 0
# 遍历所有边
for Z, X, Y in edges:
# 如果节点X和Y不属于同一个集合
# 则将他们合并在同一个集合中
if find(X) != find(Y):
union(X, Y)
# 答案增加Z,即需要Z的成本来构建最小生成树
ans += Z
# 最小生成树的边数+1
edge_num += 1
# 如果发现边数已经等于N-1,说明最小生成树已经构建完成
# 退出循环
if edge_num == N-1:
break
print(ans if edge_num == N-1 else -1)
import java.util.*;
public class Main {
static int[] parents;
public static int find(int x) {
if (parents[x] != x) {
parents[x] = find(parents[x]);
}
return parents[x];
}
public static void union(int x, int y) {
parents[find(x)] = find(y);
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N = scanner.nextInt();
int M = scanner.nextInt();
parents = new int[N + 1];
for (int i = 1; i <= N; i++) {
parents[i] = i;
}
int edgeNum = 0;
List<int[]> edges = new ArrayList<>();
for (int i = 0; i < M; i++) {
int X = scanner.nextInt();
int Y = scanner.nextInt();
int Z = scanner.nextInt();
int P = scanner.nextInt();
if (P == 1) {
if (find(X) != find(Y)) {
union(X, Y);
edgeNum++;
}
} else {
edges.add(new int[]{Z, X, Y});
}
}
edges.sort(Comparator.comparingInt(o -> o[0]));
int ans = 0;
for (int[] edge : edges) {
int Z = edge[0];
int X = edge[1];
int Y = edge[2];
if (find(X) != find(Y)) {
union(X, Y);
ans += Z;
edgeNum++;
if (edgeNum == N - 1) {
break;
}
}
}
System.out.println(edgeNum == N - 1 ? ans : -1);
}
}
#include
#include
#include
using namespace std;
vector<int> parents;
int find(int x) {
if (parents[x] != x) {
parents[x] = find(parents[x]);
}
return parents[x];
}
void unionSets(int x, int y) {
parents[find(x)] = find(y);
}
int main() {
int N, M;
cin >> N >> M;
parents.resize(N + 1);
for (int i = 1; i <= N; ++i) {
parents[i] = i;
}
int edge_num = 0;
vector<pair<int, pair<int, int>>> edges;
for (int i = 0; i < M; ++i) {
int X, Y, Z, P;
cin >> X >> Y >> Z >> P;
if (P == 1) {
if (find(X) == find(Y)) {
continue;
} else {
unionSets(X, Y);
edge_num++;
}
} else {
edges.push_back({Z, {X, Y}});
}
}
sort(edges.begin(), edges.end());
int ans = 0;
for (const auto& edge : edges) {
int Z = edge.first;
int X = edge.second.first;
int Y = edge.second.second;
if (find(X) != find(Y)) {
unionSets(X, Y);
ans += Z;
edge_num++;
if (edge_num == N - 1) {
break;
}
}
}
cout << (edge_num == N - 1 ? ans : -1) << endl;
return 0;
}
# 题目:2023C-建设5G网络
# 分值:200
# 作者:闭着眼睛学数理化
# 算法:最小生成树-Prim算法
# 代码看不懂的地方,请直接在群上提问
from collections import defaultdict
from heapq import heappop, heappush
# 并查集的查:寻找节点x的根节点
def find(x):
if parents[x] != x:
parents[x] = find(parents[x])
return parents[x]
# 并查集的并:将节点x和节点y合并在同一个集合中
# 即设置x的根节点的父节点为y的根节点
def union(x, y):
parents[find(x)] = find(y)
N = int(input())
M = int(input())
# 父节点数组,parents[i]储存i的父节点,初始化为他们自身
# 注意编号是从1开始的,所以数组长度设置为n+1
parents = [i for i in range(N+1)]
# 初始化树的边数为0
edge_num = 0
# 初始化小节点的邻接表
neighbor_dic = defaultdict(list)
# 初始化大节点/已连通集合的邻接表
set_neighbor_dic = defaultdict(list)
# 遍历M条边的关系
for _ in range(M):
X, Y, Z, P = map(int, input().split())
# 如果节点X和Y已经连接,
if P == 1:
# 如果X和Y已经属于同一个集合,则直接跳过
if find(X) == find(Y):
continue
# 如果X和Y不属于一个集合,则令他们合并,同时边数+1
else:
union(X, Y)
edge_num += 1
# 如果节点X和Y还没有连接,则记录该条边的情况
else:
# 在邻接表中储存边
neighbor_dic[X].append((Z, Y))
neighbor_dic[Y].append((Z, X))
# 遍历所有节点
for i in range(1, N+1):
# 获得节点i的根节点root_i,作为这个集的编号
root_i = find(i)
# 遍历节点i的所有近邻节点j
for Z, j in neighbor_dic[i]:
# 如果i和j已经属于同一个集合,则直接跳过
if find(i) == find(j):
continue
# 否则,寻找j对应的集的根节点的编号root_j
else:
root_j = find(j)
# 两个大节点的编号分别为root_i和root_j
# 用他们的编号构建大节点之间的邻接表set_neighbor_dic
set_neighbor_dic[root_i].append((Z, root_j))
# 注意此处无需重复储存
# set_neighbor_dic[root_j].append((Z, root_i))
# 因为在for循环后续遇到root_j的时候,也会储存root_i
# 如果set_neighbor_dic为空,说明所有节点在初始时就已经是连通状态
# 直接输出0
if len(set_neighbor_dic) == 0:
print(0)
else:
# 剩余内容和传统Prim算法类似
# 随机在set_neighbor中选择一个根节点作为起始节点
cur_node = list(set_neighbor_dic.keys())[0]
# 表示根节点已经使用了的集合
node_used = {cur_node}
heap_edges = list()
ans = 0
# 循环N-1-edge_num次,构建N-1-edge_num条边
# 其中edge_num为已经连通的边数
for _ in range(N-1-edge_num):
# 遍历cur_node的所有邻接点
for nxt_Z, nxt_node in set_neighbor_dic[cur_node]:
# 如果邻接点没有位于已使用集合中,将该条边的信息加入heap中
if nxt_node not in node_used:
heappush(heap_edges, (nxt_Z, nxt_node))
# 弹出堆顶元素,但堆顶元素对应的边的节点已经使用过,那么应该排除掉
while len(heap_edges) > 0 and heap_edges[0][1] in node_used:
heappop(heap_edges)
# 若此时堆中没有元素,但构建的边数还不够n-1,说明无法构建所有点,返回-1
if len(heap_edges) == 0:
ans = -1
break
# 获得堆顶元素,此时cur_node是连通块中新加入的节点
# 将边的权重cur_Z更新给ans,将新加入的节点cur_node加入node_used中
cur_Z, cur_node = heappop(heap_edges)
node_used.add(cur_node)
ans += cur_Z
print(ans)
import java.util.*;
public class Main {
static int[] parents;
public static int find(int x) {
if (parents[x] != x) {
parents[x] = find(parents[x]);
}
return parents[x];
}
public static void union(int x, int y) {
parents[find(x)] = find(y);
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N = scanner.nextInt();
int M = scanner.nextInt();
parents = new int[N + 1];
for (int i = 1; i <= N; i++) {
parents[i] = i;
}
int edgeNum = 0;
Map<Integer, List<int[]>> neighborDic = new HashMap<>();
Map<Integer, List<int[]>> setNeighborDic = new HashMap<>();
for (int i = 0; i < M; i++) {
int X = scanner.nextInt();
int Y = scanner.nextInt();
int Z = scanner.nextInt();
int P = scanner.nextInt();
if (P == 1) {
if (find(X) != find(Y)) {
union(X, Y);
edgeNum++;
}
} else {
neighborDic.computeIfAbsent(X, k -> new ArrayList<>()).add(new int[]{Z, Y});
neighborDic.computeIfAbsent(Y, k -> new ArrayList<>()).add(new int[]{Z, X});
}
}
for (int i = 1; i <= N; i++) {
int rootI = find(i);
for (int[] edge : neighborDic.getOrDefault(i, new ArrayList<>())) {
int Z = edge[0];
int j = edge[1];
if (find(i) != find(j)) {
int rootJ = find(j);
setNeighborDic.computeIfAbsent(rootI, k -> new ArrayList<>()).add(new int[]{Z, rootJ});
}
}
}
if (setNeighborDic.isEmpty()) {
System.out.println(0);
} else {
int curNode = setNeighborDic.keySet().iterator().next();
Set<Integer> nodeUsed = new HashSet<>();
nodeUsed.add(curNode);
PriorityQueue<int[]> heapEdges = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
int ans = 0;
for (int i = 0; i < N - 1 - edgeNum; i++) {
for (int[] edge : setNeighborDic.get(curNode)) {
int nxtZ = edge[0];
int nxtNode = edge[1];
if (!nodeUsed.contains(nxtNode)) {
heapEdges.offer(new int[]{nxtZ, nxtNode});
}
}
while (!heapEdges.isEmpty() && nodeUsed.contains(heapEdges.peek()[1])) {
heapEdges.poll();
}
if (heapEdges.isEmpty()) {
ans = -1;
break;
}
int[] topEdge = heapEdges.poll();
int curZ = topEdge[0];
curNode = topEdge[1];
nodeUsed.add(curNode);
ans += curZ;
}
System.out.println(ans);
}
}
}
#include
#include
#include
#include
#include
using namespace std;
vector<int> parents;
int find(int x) {
if (parents[x] != x) {
parents[x] = find(parents[x]);
}
return parents[x];
}
void unionSets(int x, int y) {
parents[find(x)] = find(y);
}
int main() {
int N, M;
cin >> N >> M;
parents.resize(N + 1);
for (int i = 1; i <= N; i++) {
parents[i] = i;
}
int edgeNum = 0;
unordered_map<int, vector<vector<int>>> neighborDic;
unordered_map<int, vector<vector<int>>> setNeighborDic;
for (int i = 0; i < M; i++) {
int X, Y, Z, P;
cin >> X >> Y >> Z >> P;
if (P == 1) {
if (find(X) != find(Y)) {
unionSets(X, Y);
edgeNum++;
}
} else {
neighborDic[X].push_back({Z, Y});
neighborDic[Y].push_back({Z, X});
}
}
for (int i = 1; i <= N; i++) {
int rootI = find(i);
for (auto& edge : neighborDic[i]) {
int Z = edge[0];
int j = edge[1];
if (find(i) != find(j)) {
int rootJ = find(j);
setNeighborDic[rootI].push_back({Z, rootJ});
}
}
}
if (setNeighborDic.empty()) {
cout << 0 << endl;
} else {
int curNode = setNeighborDic.begin()->first;
unordered_set<int> nodeUsed;
nodeUsed.insert(curNode);
priority_queue<vector<int>, vector<vector<int>>, greater<vector<int>>> heapEdges;
int ans = 0;
for (int i = 0; i < N - 1 - edgeNum; i++) {
for (auto& edge : setNeighborDic[curNode]) {
int nxtZ = edge[0];
int nxtNode = edge[1];
if (nodeUsed.find(nxtNode) == nodeUsed.end()) {
heapEdges.push({nxtZ, nxtNode});
}
}
while (!heapEdges.empty() && nodeUsed.find(heapEdges.top()[1]) != nodeUsed.end()) {
heapEdges.pop();
}
if (heapEdges.empty()) {
ans = -1;
break;
}
vector<int> topEdge = heapEdges.top();
int curZ = topEdge[0];
curNode = topEdge[1];
nodeUsed.insert(curNode);
ans += curZ;
heapEdges.pop();
}
cout << ans << endl;
}
return 0;
}
时间复杂度:O(MlogM)
。无论是Kruskal算法还是Prim算法,都涉及到对所有边进行排序的过程。
空间复杂度:O(NM)
。
华为OD算法/大厂面试高频题算法冲刺训练目前开始常态化报名!目前已服务100+同学成功上岸!
课程讲师为全网50w+粉丝编程博主@吴师兄学算法 以及小红书头部编程博主@闭着眼睛学数理化
每期人数维持在20人内,保证能够最大限度地满足到每一个同学的需求,达到和1v1同样的学习效果!
60+天陪伴式学习,40+直播课时,300+动画图解视频,300+LeetCode经典题,200+华为OD真题/大厂真题,还有简历修改、模拟面试、专属HR对接将为你解锁
可上全网独家的欧弟OJ系统练习华子OD、大厂真题
可查看链接 大厂真题汇总 & OD真题汇总(持续更新)
绿色聊天软件戳 od1336
了解更多