【Py/Java/C++三种语言OD2023C卷真题】20天拿下华为OD笔试之【最小生成树】2023C-5G网络建设【欧弟算法】全网注释最详细分类最全的华为OD真题题解

文章目录

  • 题目描述与示例
    • 题目描述
    • **输入描述**
    • **输出描述**
    • **示例一**
      • **输入**
      • **输出**
      • **说明**
    • **示例二**
      • **输入**
      • **输出**
      • **说明**
    • **示例三**
      • **输入**
      • **输出**
      • **说明**
  • 解题思路
    • Kruskal算法
    • Prim算法
  • 代码
    • 解法一:Kruskal算法
      • python
      • java
      • cpp
    • 解法二:Prim算法
      • python
      • java
      • cpp
    • 时空复杂度
  • 华为OD算法/大厂面试高频题算法练习冲刺训练

题目描述与示例

题目描述

现需要在某城市进行 5G 网络建设,已经选取 N 个地点设置 5G 基站,编号固定为 1N,接下来需要各个基站之间使用光纤进行连接以确保基站能互联互通,不同基站之间架设光纤的成本各不相同,且有些节点之间已经存在光纤相连,请你设计算法,计算出能联通这些基站的最小成本是多少。

注意,基站的联通具有传递性,即基站 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 <= N0 < Y <= NX 不等于 YZ 表示在 X Y 之间架设光纤的成本,其中 0 < Z < 100P 表示是否已存在光纤连接,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算法是基于所有边权值排序的算法。传统的Kruskal算法包含以下步骤

  1. 将所有边储存在数组edges中,并按照权重进行从小到大排序
  2. 考虑排序后的每一条边,其权重为Z,所连接的节点分别为XY。若
    1. XY连接后不会形成环,即XY原本属于两个不同的连通块。则
      • 使用并查集将其并为同一个连通块,union(X, Y)
      • 这条权重为Z的边应该被选择,ans += Z
      • 此时树中的边数加1,即edge_num += 1
    2. XY连接后会形成环,即XY原本就属于同一个连通块。则‘
      • 跳过这条边,无需做其他计算。
  3. 上述循环持续进行,直到边数edge_num等于n-1

那么对于本题而言,已经存在了若干点之间已经存在边,应该对上述过程做出相应的调整。考虑边的时候,若

  • 这条边尚未存在,即P = 0,则和原方法一样先储存在数组edges
  • 这条边已经存在,即P = 1,则需要判断此时的两个节点XY是否已经属于同一个连通块。若
    • 两个节点XY已经属于同一个连通块,即在之前的已经存在的边已经连通了,find(X) == find(Y),那么直接跳过这条边。
    • 两个节点XY不属于同一个连通块,则需要令它们合并,即union(X, Y),同时边数加1,即edge_num += 1

Prim算法

Prim算法是基于当前已连通集合的外延边权值排序的算法。传统的Prim算法包含以下步骤

  1. 根据边的数据,构建图的邻接表neighbor_dic,邻接表的key为节点编号,value为该节点所有邻接节点nxt_node以及构成的边的权值Z所构成的数组,以(Z, nxt_node)二元组的方式进行存储。
  2. 选择任意一个点作为初始点cur_node,一般选择cur_node = 0cur_node = 1
  3. 构建一个小根堆heap,用于储存若干待连接的边。在小根堆中,边权值Z更小的(Z, nxt_node)二元组会被储存在堆顶。
  4. 构建一个集合node_used,用于储存若干已经连通的节点。
  5. 从初始点cur_node出发,把起始点作为已连通集合的出发点。已连通集合不断往外扩散构建边,由于需要构建n-1条边,该过程直接在一个循环n-1次的for循环中进行。其具体过程如下
    1. 选择cur_node在邻接表eighbor_dic[cur_node]中的所有近邻点nxt_node,其构成的边的权值为Z。若
      1. 近邻点nxt_node已经出现在集合node_used中,说明该近邻点已经位于已连通集合中,直接跳过。
      2. 近邻点nxt_node尚未出现在集合node_used中,说明该近邻点尚未位于已连通集合中,将这条边以(Z, nxt_node)二元组的形式加入小根堆中。
    2. cur_node的所有近邻点以及构成的边都加入小根堆中之后,使用while循环反复考虑堆顶元素。若
      1. 堆为空,则堆顶元素不存在。退出while循环。
      2. 堆顶元素对应的外延节点heap[0][1]已经位于已连通集合中,则弹出堆顶元素,考虑下一个堆顶元素。
      3. 堆顶元素对应的外延节点heap[0][1]尚未位于已连通集合中,则这个节点将成为当前已连通集合的下一个的外延节点,退出while循环。
    3. 判断此时堆是否为空,如果堆为空,则无法继续外延,无法构建树。
    4. 此时堆顶元素heap[0]即为当前已连通集合的新的外延节点。需要
      1. 将该节点heap[0][1]对应的边权值heap[0][0]计入总权值ans
      2. 将该节点heap[0][1]加入集合node_used,表示成为已连通集合的一部分
      3. 将该节点heap[0][1]设置为新的cur_node,因为新加入的这个节点会带来更多的近邻点和边。

那么对于本题而言,已经存在了若干点之间已经存在边,应该对上述过程做出相应的调整。需要做出如下调整

  1. 对于已经连通的节点所构成的连通块,可以看作是一个大节点。这个大节点和其他大节点之间存在的边,由所有大节点内的小节点和其他不在这个大节点的其他小节点之间的边构成。换句话说,把问题转化为考虑如何用尽量小的边权和,来连接若干大节点
  2. 基于上述思路,要先预处理已连通的节点。可以考虑用并查集来完成,以每一个集合的根节点作为key,集合中每一个节点的非本集合邻接节点nxt_node以及构成的边的权值Z所构成的数组作为value,构建出对应的邻接表set_neighbor_dic
  3. 剩余过程则和传统的Prim算法一致,对set_neighbor_dic进行建堆过程。注意剩余需要构建的边数为N-1-edge_num

代码

解法一:Kruskal算法

python

# 题目: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)

java

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);
    }
}

cpp

#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;
}

解法二:Prim算法

python

# 题目: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)

java

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);
        }
    }
}

cpp

#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算法/大厂面试高频题算法练习冲刺训练

  • 华为OD算法/大厂面试高频题算法冲刺训练目前开始常态化报名!目前已服务100+同学成功上岸!

  • 课程讲师为全网50w+粉丝编程博主@吴师兄学算法 以及小红书头部编程博主@闭着眼睛学数理化

  • 每期人数维持在20人内,保证能够最大限度地满足到每一个同学的需求,达到和1v1同样的学习效果!

  • 60+天陪伴式学习,40+直播课时,300+动画图解视频,300+LeetCode经典题,200+华为OD真题/大厂真题,还有简历修改、模拟面试、专属HR对接将为你解锁

  • 可上全网独家的欧弟OJ系统练习华子OD、大厂真题

  • 可查看链接 大厂真题汇总 & OD真题汇总(持续更新)

  • 绿色聊天软件戳 od1336了解更多

你可能感兴趣的:(最新华为OD真题,java,c++,华为od)