自学内容网 自学内容网

【2024年华为OD机试】(C/D卷,200分)- 5G网络建设 (JavaScript&Java & Python&C/C++)

在这里插入图片描述

一、问题描述

题目描述

现需要在某城市进行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
    • XY 表示基站的编号,0 < X ≤ N0 < Y ≤ NX ≠ Y
    • Z 表示在 XY 之间架设光纤的成本,0 < Z < 100
    • P 表示是否已存在光纤连接,0 表示未连接,1 表示已连接。

输出描述

  • 如果给定条件,可以建设成功互联互通的5G网络,则输出最小的建设成本。
  • 如果给定条件,无法建设成功互联互通的5G网络,则输出 -1

用例

用例1

输入

3
3
1 2 3 0
1 3 1 0
2 3 5 0

输出

4

说明
只需要在1,2以及1,3基站之间铺设光纤,其成本为 3 + 1 = 4


用例2

输入

3
1
1 2 5 0

输出

-1

说明
3基站无法与其他基站连接,输出 -1


用例3

输入

3
3
1 2 3 0
1 3 1 0
2 3 5 1

输出

1

说明
2,3基站已有光纤相连,只要在1,3基站之间铺设光纤,其成本为 1


题目解析

本题是经典的最小生成树问题的变种。最小生成树(Minimum Spanning Tree, MST)是指在一个无向连通图中,选择一部分边,使得所有顶点都连通,并且这些边的总权重最小。

生成树概念

在无向连通图中,生成树是指包含了全部顶点的极小连通子图。生成树包含原图全部的 n 个顶点和 n-1 条边。生成树的两个重要特性是:

  1. 包含所有顶点。
  2. 无环。

最小生成树概念

最小生成树是指生成树中 n-1 条边的权重值之和最小。求解最小生成树的常用算法有 Prim算法Kruskal算法


Prim算法

Prim算法是基于顶点的贪心算法。其步骤如下:

  1. 选择一个起始顶点。
  2. 从当前已选顶点集合出发,选择一条权重最小的边,连接到未选顶点。
  3. 将新顶点加入已选顶点集合。
  4. 重复上述步骤,直到所有顶点都被选中。

适用场景:适用于节点少、边多的情况。


Kruskal算法

Kruskal算法是基于边的贪心算法。其步骤如下:

  1. 将所有边按权重从小到大排序。
  2. 依次选择权重最小的边,如果这条边的两个顶点不在同一个连通分量中,则将其加入生成树。
  3. 重复上述步骤,直到生成树中有 n-1 条边。

适用场景:适用于节点多、边少的情况。


本题的特殊性

本题中,某些基站之间已经存在光纤连接(即某些边的权重为0)。处理这种情况的方法是将已连接的边视为权重为0的边,然后使用最小生成树算法求解。


解题思路#

  1. 输入处理:读取基站数量 N 和边数量 M,然后读取每条边的信息。
  2. 处理已连接的边:对于已连接的边(P = 1),将其权重设为0。
  3. 构建图:将所有边按权重从小到大排序。
  4. 使用Kruskal算法
    • 初始化并查集,每个基站初始为一个独立的连通分量。

    • 依次选择权重最小的边,如果这条边的两个基站不在同一个连通分量中,则将其加入生成树,并合并这两个连通分量。

    • 记录总成本。

  5. 判断连通性:如果最终生成树中的边数小于 N-1,则输出 -1;否则输出总成本。

关键点

  • 并查集:用于判断两个基站是否在同一个连通分量中,避免形成环。
  • 权重处理:已连接的边权重设为0,确保优先选择这些边。
  • 连通性判断:最终生成树的边数必须为 N-1,否则无法连通所有基站。

通过上述方法,可以高效地求解本题的最小建设成本。 #

二、JavaScript算法源码

Prim算法

const rl = require("readline").createInterface({ input: process.stdin });
var iter = rl[Symbol.asyncIterator]();
const readline = async () => (await iter.next()).value;

void (async function () {
  const n = parseInt(await readline()); // 基站数量(节点数)
  const m = parseInt(await readline()); // 基站对数量(边数)

  // 邻接矩阵, 初始化默认各点之间互不联通,即i-j边权无限大
  const graph = new Array(n + 1)
    .fill(0)
    .map(() => new Array(n + 1).fill(Infinity));

  for (let i = 0; i < m; i++) {
    const [x, y, z, p] = (await readline()).split(" ").map(Number);

    if (p == 0) {
      // x-y边权为z
      graph[x][y] = z;
      graph[y][x] = z;
    } else {
      // 对应已经联通的两点,可以理解为边权为0
      graph[x][y] = 0;
      graph[y][x] = 0;
    }
  }

  function prim() {
    // 记录最小生成树的边权和
    let minWeight = 0;

    // inTree[i] 表示 节点i 是否在最小生成树中
    const inTree = new Array(n + 1).fill(false);

    // 初始时任选一个节点作为最小生成树的初始节点,这里选择节点1
    inTree[1] = true;
    // 记录最小生成树中点数量
    let inTree_count = 1;

    // dis[i]表示 节点i 到最小生成树集合 的最短距离
    // 初始时,最小生成树集合中只有节点1,因此其他节点到最小生成树的距离,其实就是到节点1的距离
    const dis = new Array(n + 1).fill(0).map((_, i) => graph[i][1]);

    // 如果最小生成树中点数量达到n个,则结束循环
    while (inTree_count < n) {
      // 现在我们需要从未纳入最小生成树的点中,找到一个距离最小生成树最近的
      let minDis = Infinity; // minDis 记录这个最近距离
      let nodeIdx = 0; // idx 记录距离最小生成树minDis个距离的节点

      for (let i = 1; i <= n; i++) {
        // 从未纳入最小生成树的点中,找到一个距离最小生成树最近的
        if (!inTree[i] && dis[i] < minDis) {
          minDis = dis[i];
          nodeIdx = i;
        }
      }

      // 如果nodeIdx == 0,则说明未纳入最小生成树的这些点到最小生成树的距离都是Integer.MAX_VALUE,即不与最小生成树存在关联
      if (nodeIdx == 0) {
        // 则说明,当前所有点无法形成最小生成树
        return -1;
      }

      inTree[nodeIdx] = true; // 最小生成树需要纳入最短距离点nodeIdx
      inTree_count++; // 最小生成树中点数量+1
      minWeight += dis[nodeIdx]; // 更新最小生成树的权重和

      // dis[i] 初始时记录的是节点i 到 节点1 的距离(初始的生成树中只有节点1)
      // 现在生成树纳入了新节点nodeIdx,则我们需要更新一下dis[i],即有可能某些点到最小生成树中的nodeIdx点距离更近
      for (let i = 1; i <= n; i++) {
        if (!inTree[i] && graph[nodeIdx][i] < dis[i]) {
          // 注意,这是一个累进过程,初始时dis[i]记录的是节点i到节点1的距离,
          // 之后,最小生成树纳入新点后,如果节点i到新点的距离更近,则dis[i]就更新为这个更短距离
          // 总之,dis[i] 记录的是 节点 i 到最小生成树的最短距离
          dis[i] = graph[nodeIdx][i];
        }
      }
    }

    return minWeight;
  }

  console.log(prim());
})();
代码讲解:
  1. 输入处理:首先读取基站数量n和基站对数量m,然后初始化一个邻接矩阵graph,表示基站之间的连接关系。如果两个基站已经联通,则边权为0,否则边权为输入的z

  2. Prim算法实现

    • 初始化:选择一个起始节点(这里选择节点1),并将其标记为已加入最小生成树。
    • 距离数组dis:记录每个节点到当前最小生成树的最短距离。
    • 主循环:每次从未加入最小生成树的节点中,选择一个距离最小生成树最近的节点加入,并更新最小生成树的权重和。
    • 更新距离数组:每当有新节点加入最小生成树时,更新其他节点到最小生成树的最短距离。
  3. 输出结果:最终输出最小生成树的权重和。

Kruskal算法

const rl = require("readline").createInterface({ input: process.stdin });
var iter = rl[Symbol.asyncIterator]();
const readline = async () => (await iter.next()).value;

void (async function () {
  const n = parseInt(await readline()); // 基站数量(节点数)
  const m = parseInt(await readline()); // 基站对数量(边数)

  const edges = [];
  for (let i = 0; i < m; i++) {
    // 边起点, 边终点,边权重,起点和终点是否已关联
    const [x, y, z, p] = (await readline()).split(" ").map(Number);

    if (p == 0) {
      // 起点和终点未关联
      edges.push([x, y, z]);
    } else {
      // 起点和终点已关联,则关联代价实际为0
      edges.push([x, y, 0]);
    }
  }

  function kruskal() {
    let minWeight = 0;

    // 按照边权升序
    edges.sort((a, b) => a[2] - b[2]);

    const ufs = new UnionFindSet(n + 1);

    // 最先遍历出来是边权最小的边
    for (const [x, y, z] of edges) {
      // 如果edge.from节点 和 edge.to节点 是同一个连通分量(即都在最小生成树中),则此时会产生环
      // 因此只有当edge.from节点 和 edge.to节点不在同一个连通分量时,才能合并(纳入最小生成树)
      if (ufs.find(x) != ufs.find(y)) {
        minWeight += z;
        ufs.union(x, y);

        // 需要注意的是,上面初始化并查集的节点数为n+1个,因此并查集底层fa数组的长度就是n+1,即索引范围是[0, n],左闭右闭,
        // 其中0索引是无用的,1~n索引对应最小生成树中各个节点,如果者n个节点可以变为最小生成树,那么1~n节点会被合并为一个连通分量
        // 而0索引虽然无用,但是也会自己形成一个连通分量,因此最终如果能形成最小生成树,则并查集中会有两个连通分量
        if (ufs.count == 2) {
          return minWeight;
        }
      }
    }

    return -1;
  }

  console.log(kruskal());
})();

// 并查集实现
class UnionFindSet {
  constructor(n) {
    this.fa = new Array(n).fill(true).map((_, idx) => idx);
    this.count = n; // 初始时各站点互不相连,互相独立,因此需要给n个站点发送广播
  }

  // 查x站点对应的顶级祖先站点
  find(x) {
    while (x !== this.fa[x]) {
      x = this.fa[x];
    }
    return x;
  }

  // 合并两个站点,其实就是合并两个站点对应的顶级祖先节点
  union(x, y) {
    let x_fa = this.find(x);
    let y_fa = this.find(y);

    if (x_fa !== y_fa) {
      // 如果两个站点祖先相同,则在一条链上,不需要合并
      this.fa[y_fa] = x_fa; // 合并站点,即让某条链的祖先指向另一条链的祖先
      this.count--; // 一旦两个站点合并,则发送广播次数减1
    }
  }
}
代码讲解:
  1. 输入处理:读取基站数量n和基站对数量m,然后读取每条边的信息并存储在edges数组中。如果两个基站已经联通,则边权为0,否则边权为输入的z

  2. Kruskal算法实现

    • 排序:将所有边按权重从小到大排序。
    • 并查集初始化:初始化一个并查集ufs,用于管理节点的连通性。
    • 主循环:遍历排序后的边,如果边的两个端点不在同一个连通分量中,则将这条边加入最小生成树,并合并这两个连通分量。
    • 终止条件:当并查集中的连通分量减少到2时,说明最小生成树已经形成,返回最小生成树的权重和。
  3. 并查集实现

    • 初始化:每个节点的父节点初始化为自身。
    • 查找操作:查找某个节点的根节点。
    • 合并操作:将两个节点的根节点合并,减少连通分量的数量。
  4. 输出结果:最终输出最小生成树的权重和。

三、Java算法源码

Prim算法

Prim算法是一种用于求解最小生成树(Minimum Spanning Tree, MST)的贪心算法。它从一个起始节点开始,逐步扩展生成树,每次选择与当前生成树距离最近的节点加入生成树,直到所有节点都被包含。

import java.util.Scanner;

public class Main {
  public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);

    int n = sc.nextInt(); // 基站数量(节点数)
    int m = sc.nextInt(); // 基站对数量(边数)

    // 邻接矩阵
    int[][] graph = new int[n + 1][n + 1];
    for (int i = 1; i <= n; i++) {
      for (int j = 1; j <= n; j++) {
        // 初始化默认各点之间互不联通,即i-j边权无限大
        graph[i][j] = Integer.MAX_VALUE;
      }
    }

    for (int i = 0; i < m; i++) {
      int x = sc.nextInt();
      int y = sc.nextInt();
      int z = sc.nextInt();
      int p = sc.nextInt();

      if (p == 0) {
        // x-y边权为z
        graph[x][y] = z;
        graph[y][x] = z;
      } else {
        // 对应已经联通的两点,可以理解为边权为0
        graph[x][y] = 0;
        graph[y][x] = 0;
      }
    }

    System.out.println(prim(graph, n));
  }

  public static int prim(int[][] graph, int n) {
    // 记录最小生成树的边权和
    int minWeight = 0;

    // inTree[i] 表示 节点i 是否在最小生成树中
    boolean[] inTree = new boolean[n + 1];

    // 初始时任选一个节点作为最小生成树的初始节点,这里选择节点1
    inTree[1] = true;
    // 记录最小生成树中点数量
    int inTree_count = 1;

    // dis[i]表示 节点i 到最小生成树集合 的最短距离
    int[] dis = new int[n + 1];
    for (int i = 1; i <= n; i++) {
      // 初始时,最小生成树集合中只有节点1,因此其他节点到最小生成树的距离,其实就是到节点1的距离
      dis[i] = graph[1][i];
    }

    // 如果最小生成树中点数量达到n个,则结束循环
    while (inTree_count < n) {
      // 现在我们需要从未纳入最小生成树的点中,找到一个距离最小生成树最近的

      // minDis 记录这个最近距离
      int minDis = Integer.MAX_VALUE;
      // idx 记录距离最小生成树minDis个距离的节点
      int nodeIdx = 0;

      for (int i = 1; i <= n; i++) {
        // 从未纳入最小生成树的点中,找到一个距离最小生成树最近的
        if (!inTree[i] && dis[i] < minDis) {
          minDis = dis[i];
          nodeIdx = i;
        }
      }

      // 如果nodeIdx == 0,则说明未纳入最小生成树的这些点到最小生成树的距离都是Integer.MAX_VALUE,即不与最小生成树存在关联
      if (nodeIdx == 0) {
        // 则说明,当前所有点无法形成最小生成树
        return -1;
      }

      inTree[nodeIdx] = true; // 最小生成树需要纳入最短距离点nodeIdx
      inTree_count++; // 最小生成树中点数量+1
      minWeight += dis[nodeIdx]; // 更新最小生成树的权重和

      // dis[i] 初始时记录的是节点i 到 节点1 的距离(初始的生成树中只有节点1)
      // 现在生成树纳入了新节点nodeIdx,则我们需要更新一下dis[i],即有可能某些点到最小生成树中的nodeIdx点距离更近
      for (int i = 1; i <= n; i++) {
        if (!inTree[i] && graph[nodeIdx][i] < dis[i]) {
          // 注意,这是一个累进过程,初始时dis[i]记录的是节点i到节点1的距离,
          // 之后,最小生成树纳入新点后,如果节点i到新点的距离更近,则dis[i]就更新为这个更短距离
          // 总之,dis[i] 记录的是 节点 i 到最小生成树的最短距离
          dis[i] = graph[nodeIdx][i];
        }
      }
    }

    return minWeight;
  }
}

Kruskal算法

Kruskal算法也是一种用于求解最小生成树的算法。它通过按边权从小到大排序,逐步选择边加入生成树,同时避免形成环。

import java.util.Arrays;
import java.util.Scanner;

public class Main {
  // 边
  static class Edge {
    int from; // 边起点
    int to; // 边终点
    int weight; // 边权重

    public Edge(int from, int to, int weight) {
      this.from = from;
      this.to = to;
      this.weight = weight;
    }
  }

  public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);

    int n = sc.nextInt(); // 基站数量(节点数)
    int m = sc.nextInt(); // 基站对数量(边数)

    Edge[] edges = new Edge[m];

    for (int i = 0; i < m; i++) {
      int x = sc.nextInt();
      int y = sc.nextInt();
      int z = sc.nextInt();
      int p = sc.nextInt();

      // 如果p==1,则可以认为x-y边权为0
      edges[i] = new Edge(x, y, p == 0 ? z : 0);
    }

    System.out.println(kruskal(edges, n));
  }

  public static int kruskal(Edge[] edges, int n) {
    int minWeight = 0;

    // 按照边权升序
    Arrays.sort(edges, (a, b) -> a.weight - b.weight);

    UnionFindSet ufs = new UnionFindSet(n + 1);

    // 最先遍历出来是边权最小的边
    for (Edge edge : edges) {
      // 如果edge.from节点 和 edge.to节点 是同一个连通分量(即都在最小生成树中),则此时会产生环
      // 因此只有当edge.from节点 和 edge.to节点不在同一个连通分量时,才能合并(纳入最小生成树)
      if (ufs.find(edge.from) != ufs.find(edge.to)) {
        minWeight += edge.weight;
        ufs.union(edge.from, edge.to);

        // 需要注意的是,上面初始化并查集的节点数为n+1个,因此并查集底层fa数组的长度就是n+1,即索引范围是[0, n],左闭右闭,
        // 其中0索引是无用的,1~n索引对应最小生成树中各个节点,如果者n个节点可以变为最小生成树,那么1~n节点会被合并为一个连通分量
        // 而0索引虽然无用,但是也会自己形成一个连通分量,因此最终如果能形成最小生成树,则并查集中会有两个连通分量
        if (ufs.count == 2) {
          return minWeight;
        }
      }
    }

    return -1;
  }
}

// 并查集
class UnionFindSet {
  int[] fa;
  int count;

  public UnionFindSet(int n) {
    this.fa = new int[n];
    this.count = n;
    for (int i = 0; i < n; i++) this.fa[i] = i;
  }

  public int find(int x) {
    if (x != this.fa[x]) {
      return (this.fa[x] = this.find(this.fa[x]));
    }
    return x;
  }

  public void union(int x, int y) {
    int x_fa = this.find(x);
    int y_fa = this.find(y);

    if (x_fa != y_fa) {
      this.fa[y_fa] = x_fa;
      this.count--;
    }
  }
}

代码解释

  1. Prim算法

    • 邻接矩阵:使用二维数组graph存储图的边权信息。
    • 初始化:将所有边的权重初始化为Integer.MAX_VALUE,表示初始时各节点之间不连通。
    • 输入处理:根据输入的边信息更新邻接矩阵。如果p == 0,表示边权为z;如果p == 1,表示边权为0(即已经连通)。
    • Prim算法核心
      • 使用inTree数组记录哪些节点已经在生成树中。
      • 使用dis数组记录每个节点到生成树的最短距离。
      • 每次选择距离生成树最近的节点加入生成树,并更新其他节点到生成树的距离。
      • 如果所有节点都被加入生成树,则返回最小生成树的总权重;否则返回-1表示无法形成最小生成树。
  2. Kruskal算法

    • 边类:定义了一个Edge类,用于存储边的起点、终点和权重。
    • 输入处理:根据输入的边信息创建Edge对象数组。
    • Kruskal算法核心
      • 将边按权重升序排序。
      • 使用并查集(UnionFindSet)来管理节点的连通性。
      • 遍历所有边,如果边的两个端点不在同一个连通分量中,则将边加入生成树,并合并两个连通分量。
      • 如果最终并查集中只剩下两个连通分量(一个包含所有节点,另一个是未使用的0索引),则返回最小生成树的总权重;否则返回-1表示无法形成最小生成树。
  3. 并查集

    • 初始化:每个节点的父节点初始化为自己。
    • 查找:使用路径压缩优化查找操作。
    • 合并:将两个节点的连通分量合并,并减少连通分量的数量。

总结

  • Prim算法:适用于稠密图,时间复杂度为O(n^2)。
  • Kruskal算法:适用于稀疏图,时间复杂度为O(m log m),其中m是边的数量。

这两种算法都可以有效地求解最小生成树问题,选择哪种算法取决于图的结构和具体需求。

四、Python算法源码

以下是关于 Prim 和 Kruskal 算法的详细注释和讲解。这两个算法用于在图中找到最小生成树(Minimum Spanning Tree,MST)。

Prim 算法

代码解析
import sys

# 输入获取
n = int(input())  # 基站数量(节点数)
m = int(input())  # 基站对数量(边数)

# 邻接矩阵, 初始化默认各点之间互不联通,即i-j边权无限大
graph = [[sys.maxsize for _ in range(n + 1)] for _ in range(n + 1)]

for _ in range(m):
    x, y, z, p = map(int, input().split())
    if p == 0:
        # x-y边权为z
        graph[x][y] = z
        graph[y][x] = z
    else:
        # 对应已经联通的两点,可以理解为边权为0
        graph[x][y] = 0
        graph[y][x] = 0
  1. 输入部分
    • 读取节点数量 n 和边数量 m
    • 使用邻接矩阵 graph 存储图中每对节点之间的边权。初始时,所有边权设为 sys.maxsize(无穷大),表示未联通。
    • 根据输入信息,如果 p == 0,则边权为 z;如果 p == 1,则表示已联通,边权为 0
# Prim算法
def prim():
    minWeight = 0  # 记录最小生成树的总权重
    inTree = [False] * (n + 1)  # 表示每个节点是否在生成树中
    inTree[1] = True  # 初始选择节点1加入生成树
    inTree_count = 1  # 生成树中的节点数

    dis = [0] * (n + 1)
    for i in range(1, n + 1):
        dis[i] = graph[1][i]  # 初始距离为节点1到各节点的距离

    while inTree_count < n:
        minDis = sys.maxsize  # 最近距离初始化为无穷大
        nodeIdx = 0  # 记录距离最短的节点

        for i in range(1, n+1):
            if not inTree[i] and dis[i] < minDis:
                minDis = dis[i]
                nodeIdx = i

        if nodeIdx == 0:
            return -1  # 无法形成生成树

        inTree[nodeIdx] = True
        inTree_count += 1
        minWeight += dis[nodeIdx]

        for i in range(1, n+1):
            if not inTree[i] and graph[nodeIdx][i] < dis[i]:
                dis[i] = graph[nodeIdx][i]

    return minWeight
  1. Prim 算法逻辑
    • 初始化:最小生成树的权重 minWeight0,从节点 1 开始构建生成树。
    • 使用数组 inTree 跟踪哪些节点已在生成树中。
    • dis 数组保存每个节点到生成树的最短距离。
    • 主循环:在生成树节点数小于 n 时,继续进行。
      • 从未加入生成树的节点中,选择距离生成树最近的节点 nodeIdx
      • 如果所有未加入的节点距离生成树无穷大,返回 -1,表示无法构成生成树。
      • 纳入该节点并更新生成树的总权重。
      • 更新 dis 数组,记录新加入的节点可能带来的更短距离。
# 算法调用
print(prim())
  • 调用 prim() 函数并打印结果。

Kruskal 算法

代码解析
# 并查集实现
class UnionFindSet:
    def __init__(self, n):
        self.fa = [i for i in range(n)]
        self.count = n

    def find(self, x):
        if x != self.fa[x]:
            self.fa[x] = self.find(self.fa[x])
        return self.fa[x]

    def union(self, x, y):
        x_fa = self.find(x)
        y_fa = self.find(y)
        if x_fa != y_fa:
            self.fa[y_fa] = x_fa
            self.count -= 1
  1. 并查集(Union-Find)
    • 用于高效判断节点是否属于同一连通分量。
    • find 方法:路径压缩,找到节点的根,并将路径上的节点直接连接到根。
    • union 方法:连接两个节点,合并它们所属的连通分量。
# 输入获取
n = int(input())  # 基站数量(节点数)
m = int(input())  # 基站对数量(边数)

edges = []
for _ in range(m):
    x, y, z, p = map(int, input().split())
    if p == 0:
        edges.append([x, y, z])  # 未关联
    else:
        edges.append([x, y, 0])  # 已关联,权重为0
  1. 输入部分
    • 读取节点和边的数量。
    • 使用 edges 列表存储所有边的信息。如果已关联,则权重设为 0
# kruskal算法
def kruskal():
    minWeight = 0
    edges.sort(key=lambda x: x[2])  # 按边权升序排序
    ufs = UnionFindSet(n+1)

    for x, y, z in edges:
        if ufs.find(x) != ufs.find(y):
            minWeight += z
            ufs.union(x, y)

            if ufs.count == 2:
                return minWeight

    return -1
  1. Kruskal 算法逻辑
    • 按边权重升序对边排序。
    • 遍历所有边,使用并查集判断边的两个节点是否属于不同连通分量。
    • 如果是,则合并两个节点并将该边纳入生成树,增加总权重。
    • 当并查集中的连通分量数量为 2 时,表示所有节点已形成一个连通分量,返回当前权重。
# 算法入口
print(kruskal())
  • 调用 kruskal() 函数并打印结果。

结论

  • Prim 算法:适合稠密图,利用邻接矩阵和顶点集来构造最小生成树,每次选择最近的节点加入生成树。

  • Kruskal 算法:适合稀疏图,使用边集和并查集处理,按边权重排序后逐一尝试加入边构建最小生成树。

  • 这两种算法都有效解决了最小生成树问题,选用哪种取决于图的性质(稠密或稀疏)和具体需求。

五、C/C++算法源码:

C 版本

以下是 Prim 算法Kruskal 算法的 C 语言版本


Prim 算法

代码实现
#include <stdio.h>
#include <limits.h>

int n, m; // 节点数量 n 和边数量 m
int graph[21][21]; // 邻接矩阵表示图,最多支持 20 个节点

// Prim 最小生成树算法
int prim() {
    int minWeight = 0; // 最小生成树的总权重
    int inTree[21] = {0}; // 标记节点是否在最小生成树中
    int dis[21]; // 存储节点到最小生成树的最短距离

    // 初始化:从节点 1 开始构建最小生成树
    inTree[1] = 1; // 节点 1 加入生成树
    int inTree_count = 1; // 生成树节点数量

    // 初始化 dis 数组,初始时其他节点到生成树的距离为到节点 1 的距离
    for (int i = 1; i <= n; i++) {
        dis[i] = graph[1][i];
    }

    // 循环直到所有节点都被加入到生成树中
    while (inTree_count < n) {
        int minDis = INT_MAX; // 当前最短距离
        int nodeIdx = 0;      // 最近的节点编号

        // 找到距离最小生成树最近的未加入节点
        for (int i = 1; i <= n; i++) {
            if (!inTree[i] && dis[i] < minDis) {
                minDis = dis[i];
                nodeIdx = i;
            }
        }

        // 如果无法找到这样的节点,说明图不连通,返回 -1
        if (nodeIdx == 0) {
            return -1;
        }

        // 将新节点加入最小生成树
        inTree[nodeIdx] = 1;
        inTree_count++;
        minWeight += minDis; // 累加边权到总权重

        // 更新其他节点到最小生成树的最短距离
        for (int i = 1; i <= n; i++) {
            if (!inTree[i] && graph[nodeIdx][i] < dis[i]) {
                dis[i] = graph[nodeIdx][i];
            }
        }
    }

    return minWeight; // 返回最小生成树的总权重
}

int main() {
    scanf("%d %d", &n, &m);

    // 初始化邻接矩阵,所有节点间距离初始化为无穷大
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            graph[i][j] = INT_MAX;
        }
    }

    // 读取边的信息
    for (int i = 0; i < m; i++) {
        int x, y, z, p;
        scanf("%d %d %d %d", &x, &y, &z, &p);

        if (p == 0) {
            // 如果节点未联通,取权重为 z
            graph[x][y] = z;
            graph[y][x] = z;
        } else {
            // 如果节点已联通,边权重为 0
            graph[x][y] = 0;
            graph[y][x] = 0;
        }
    }

    // 输出 Prim 算法计算的最小生成树权重
    printf("%d\n", prim());

    return 0;
}

Prim 算法讲解

  1. 邻接矩阵表示图

    • 使用二维数组 graph 表示图,graph[i][j] 存储节点 i 到节点 j 的边权值。
    • 如果两个节点不直接连接,边权值初始化为无穷大 (INT_MAX)。
  2. 逻辑流程

    • 从任意一个节点(本例中为节点 1)开始构建最小生成树。
    • 在每一步中,找到距离当前生成树最近的未加入节点,将其加入生成树,并更新其他节点到生成树的最短距离。
    • 重复上述过程,直到所有节点都加入生成树。
  3. 复杂度

    • 时间复杂度是 (O(n^2)),因为每次需要遍历所有节点找到最近的节点。

Kruskal 算法

代码实现
#include <stdio.h>
#include <stdlib.h>

// 并查集结构
typedef struct {
    int *fa;   // 父节点数组
    int count; // 连通分量数量
} UFS;

// 初始化并查集
UFS *new_UFS(int n) {
    UFS *ufs = (UFS *)malloc(sizeof(UFS));
    ufs->fa = (int *)malloc(sizeof(int) * n);
    for (int i = 0; i < n; i++) {
        ufs->fa[i] = i; // 每个节点的初始父节点是自身
    }
    ufs->count = n;
    return ufs;
}

// 并查集查找操作
int find_UFS(UFS *ufs, int x) {
    if (x != ufs->fa[x]) {
        ufs->fa[x] = find_UFS(ufs, ufs->fa[x]); // 路径压缩
    }
    return ufs->fa[x];
}

// 并查集合并操作
void union_UFS(UFS *ufs, int x, int y) {
    int rootX = find_UFS(ufs, x);
    int rootY = find_UFS(ufs, y);
    if (rootX != rootY) {
        ufs->fa[rootY] = rootX; // 合并两个连通分量
        ufs->count--;
    }
}

// 边结构
typedef struct Edge {
    int from;    // 起点
    int to;      // 终点
    int weight;  // 权重
} Edge;

int n, m; // 节点数量和边数量
Edge *edges; // 边数组

// 边权排序规则
int cmp(const void *a, const void *b) {
    return ((Edge *)a)->weight - ((Edge *)b)->weight;
}

// Kruskal 最小生成树算法
int kruskal() {
    int minWeight = 0; // 最小生成树总权重

    // 按边的权重升序排序
    qsort(edges, m, sizeof(Edge), cmp);

    UFS *ufs = new_UFS(n + 1); // 初始化并查集,节点编号从 1 开始

    // 遍历所有边
    for (int i = 0; i < m; i++) {
        int x = edges[i].from;
        int y = edges[i].to;

        // 如果两个节点不在同一连通分量中,则将此边加入最小生成树
        if (find_UFS(ufs, x) != find_UFS(ufs, y)) {
            minWeight += edges[i].weight; // 累加权重
            union_UFS(ufs, x, y);         // 合并连通分量

            // 如果剩下的连通分量只有 2 个,说明所有节点已连通
            if (ufs->count == 2) {
                return minWeight;
            }
        }
    }

    return -1; // 如果图不连通,返回 -1
}

int main() {
    scanf("%d %d", &n, &m);

    edges = (Edge *)malloc(sizeof(Edge) * m); // 动态分配边数组

    // 读取边信息
    for (int i = 0; i < m; i++) {
        int x, y, z, p;
        scanf("%d %d %d %d", &x, &y, &z, &p);

        edges[i].from = x;
        edges[i].to = y;
        edges[i].weight = (p == 0) ? z : 0; // 如果已联通,边权值为 0
    }

    // 输出 Kruskal 算法计算的最小生成树权重
    printf("%d\n", kruskal());

    return 0;
}

Kruskal 算法讲解

  1. 边排序

    • 按边的权重升序排序,优先处理权重较小的边。
  2. 并查集

    • 使用并查集判断两个节点是否属于同一连通分量;如果不属于,则将它们合并。
  3. 逻辑流程

    • 遍历所有边,尝试将其加入生成树。
    • 当所有节点形成一个连通分量时,停止算法并返回生成树总权重。
  4. 复杂度

    • 排序复杂度为 (O(m \log m)),查找和合并复杂度为 (O(\alpha(n))),总复杂度接近 (O(m \log m))。

总结

  • Prim 算法适用于稠密图,使用邻接矩阵存储图。
  • Kruskal 算法适用于稀疏图,使用边集和并查集处理。
  • 两种算法都可以高效解决最小生成树问题,根据具体的图结构选择合适的算法即可。

C++ 版本

以下是 Prim 和 Kruskal 算法:


Prim 算法
#include <iostream>
#include <climits>
#include <vector>

using namespace std;

const int MAX_N = 21; // 假设节点最大数量为 21
int graph[MAX_N][MAX_N]; // 邻接矩阵,用于存储边权值
int n, m; // n 表示节点数量,m 表示边数量

int prim() {
    int minWeight = 0; // 最小生成树的总权重
    vector<bool> inTree(n + 1, false); // inTree[i] 表示节点 i 是否已经纳入到最小生成树
    vector<int> dis(n + 1, INT_MAX);   // dis[i] 表示每个节点到生成树的最小距离

    inTree[1] = true; // 从节点 1 开始构建最小生成树
    for (int i = 1; i <= n; i++) {
        dis[i] = graph[1][i]; // 初始化距离:从节点 1 到其他所有节点的距离
    }

    int inTree_count = 1; // 当前生成树中的节点数量

    while (inTree_count < n) {
        int minDis = INT_MAX; // 当前最短距离
        int nodeIdx = 0;      // 记录距离生成树最近的节点

        // 遍历所有未加入生成树的节点,找到距离生成树最近的节点
        for (int i = 1; i <= n; i++) {
            if (!inTree[i] && dis[i] < minDis) {
                minDis = dis[i];
                nodeIdx = i;
            }
        }

        if (nodeIdx == 0) { 
            // 如果无法找到合适的节点,则说明图不连通,无法形成生成树
            return -1;
        }

        // 将该节点加入到生成树中
        inTree[nodeIdx] = true;
        inTree_count++;
        minWeight += dis[nodeIdx]; // 更新总权重

        // 更新 dis 数组:检查是否通过新加入的节点可以缩短其他节点到生成树的距离
        for (int i = 1; i <= n; i++) {
            if (!inTree[i] && graph[nodeIdx][i] < dis[i]) {
                dis[i] = graph[nodeIdx][i];
            }
        }
    }

    return minWeight;
}

int main() {
    cin >> n >> m;

    // 初始化邻接矩阵,所有边权值初始为无穷大(表示节点间不联通)
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            graph[i][j] = INT_MAX;
        }
    }

    // 输入边信息
    for (int i = 0; i < m; i++) {
        int x, y, z, p;
        cin >> x >> y >> z >> p;

        if (p == 0) {
            // 如果 p == 0,边权值为 z
            graph[x][y] = z;
            graph[y][x] = z;
        } else {
            // 如果 p == 1,表示两点已经联通,边权值设为 0
            graph[x][y] = 0;
            graph[y][x] = 0;
        }
    }

    cout << prim() << endl;

    return 0;
}

C++ Prim 算法讲解
  1. 数据结构

    • 使用邻接矩阵 graph 表示图,其中 graph[i][j] 表示节点 i 和节点 j 之间的边权值。
    • 使用数组 inTree 表示节点是否已经加入到生成树中。
    • 使用数组 dis 记录每个节点到生成树的最短距离。
  2. 初始化

    • 将所有边权值初始化为 INT_MAX,表示两点之间最初不直接联通。
    • 从节点 1 开始构建生成树,初始化 dis 为从节点 1 到其他节点的距离。
  3. 主循环

    • 每次找到距离生成树最近的未加入节点,将其加入生成树。
    • 更新 dis 数组,检查是否通过新加入节点可以更短连接其他未加入节点。
  4. 返回结果

    • 如果找到了一棵包含所有节点的生成树,返回其总权重;否则返回 -1 表示图不连通。

Kruskal 算法
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

// 边的结构体
struct Edge {
    int from;   // 边的起点
    int to;     // 边的终点
    int weight; // 边的权重
};

// 并查集实现
class UnionFindSet {
public:
    vector<int> fa; // 并查集的父节点数组
    int count;      // 当前连通分量数量

    UnionFindSet(int n) {
        fa.resize(n);
        for (int i = 0; i < n; i++) {
            fa[i] = i; // 每个节点初始化时是独立的连通分量
        }
        count = n;
    }

    int find(int x) {
        if (x != fa[x]) {
            fa[x] = find(fa[x]); // 路径压缩
        }
        return fa[x];
    }

    void unionSet(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);

        if (rootX != rootY) {
            fa[rootY] = rootX; // 合并两个连通分量
            count--;
        }
    }
};

// Kruskal 算法
int kruskal(int n, vector<Edge>& edges) {
    int minWeight = 0;

    // 按边权值升序排序
    sort(edges.begin(), edges.end(), [](const Edge& a, const Edge& b) {
        return a.weight < b.weight;
    });

    UnionFindSet ufs(n + 1); // 初始化并查集,包含 n+1 个节点

    // 遍历每条边
    for (const auto& edge : edges) {
        int x = edge.from;
        int y = edge.to;

        // 如果两个节点不在同一个连通分量中,则合并它们
        if (ufs.find(x) != ufs.find(y)) {
            minWeight += edge.weight; // 累加边权
            ufs.unionSet(x, y);       // 合并连通分量

            // 如果所有节点已连通,返回结果
            if (ufs.count == 2) {
                return minWeight;
            }
        }
    }

    return -1; // 如果图不连通,返回 -1
}

int main() {
    int n, m;
    cin >> n >> m;

    vector<Edge> edges(m);

    for (int i = 0; i < m; i++) {
        int x, y, z, p;
        cin >> x >> y >> z >> p;

        edges[i].from = x;
        edges[i].to = y;
        edges[i].weight = (p == 0 ? z : 0); // 如果已联通,权值为 0
    }

    cout << kruskal(n, edges) << endl;

    return 0;
}

C++ Kruskal 算法讲解
  1. 数据结构

    • 使用结构体 Edge 表示边,包括起点、终点和权重。
    • 使用类 UnionFindSet 实现并查集,用于判断节点是否属于同一连通分量。
  2. 排序

    • 按权重升序排序所有边,以便从权重最小的边开始处理。
  3. 算法流程

    • 遍历排序后的边集,如果两个节点不在同一连通分量,则将其加入生成树,并合并两个连通分量。
    • 直到所有节点连通时,返回生成树的总权重。
  4. 返回结果

    • 如果能形成生成树,返回其总权重;否则返回 -1 表示图不连通。

总结

  • Prim 算法:适用于稠密图,直接从任意节点开始构建生成树。
  • Kruskal 算法:适用于稀疏图,先将边排序,逐步构建生成树。
  • 这两种算法都能够高效解决最小生成树问题,适用场景不同。

六、尾言

什么是华为OD?

华为OD(Outsourcing Developer,外包开发工程师)是华为针对软件开发工程师岗位的一种招聘形式,主要包括笔试、技术面试以及综合面试等环节。尤其在笔试部分,算法题的机试至关重要。

为什么刷题很重要?

  1. 机试是进入技术面的第一关:
    华为OD机试(常被称为机考)主要考察算法和编程能力。只有通过机试,才能进入后续的技术面试环节。

  2. 技术面试需要手撕代码:
    技术一面和二面通常会涉及现场编写代码或算法题。面试官会注重考察候选人的思路清晰度、代码规范性以及解决问题的能力。因此提前刷题、多练习是通过面试的重要保障。

  3. 入职后的可信考试:
    入职华为后,还需要通过“可信考试”。可信考试分为三个等级:

    • 入门级:主要考察基础算法与编程能力。
    • 工作级:更贴近实际业务需求,可能涉及复杂的算法或与工作内容相关的场景题目。
    • 专业级:最高等级,考察深层次的算法以及优化能力,与薪资直接挂钩。

刷题策略与说明:

2024年8月14日之后,华为OD机试的题库转为 E卷,由往年题库(D卷、A卷、B卷、C卷)和全新题目组成。刷题时可以参考以下策略:

  1. 关注历年真题:

    • 题库中的旧题占比较大,建议优先刷历年的A卷、B卷、C卷、D卷题目。
    • 对于每道题目,建议深度理解其解题思路、代码实现,以及相关算法的适用场景。
  2. 适应新题目:

    • E卷中包含全新题目,需要掌握全面的算法知识和一定的灵活应对能力。
    • 建议关注新的刷题平台或交流群,获取最新题目的解析和动态。
  3. 掌握常见算法:
    华为OD考试通常涉及以下算法和数据结构:

    • 排序算法(快速排序、归并排序等)
    • 动态规划(背包问题、最长公共子序列等)
    • 贪心算法
    • 栈、队列、链表的操作
    • 图论(最短路径、最小生成树等)
    • 滑动窗口、双指针算法
  4. 保持编程规范:

    • 注重代码的可读性和注释的清晰度。
    • 熟练使用常见编程语言,如C++、Java、Python等。

如何获取资源?

  1. 官方参考:

    • 华为招聘官网或相关的招聘平台会有一些参考信息。
    • 华为OD的相关公众号可能也会发布相关的刷题资料或学习资源。
  2. 加入刷题社区:

    • 找到可信的刷题交流群,与其他备考的小伙伴交流经验。
    • 关注知名的刷题网站,如LeetCode、牛客网等,这些平台上有许多华为OD的历年真题和解析。
  3. 寻找系统性的教程:

    • 学习一本经典的算法书籍,例如《算法导论》《剑指Offer》《编程之美》等。
    • 完成系统的学习课程,例如数据结构与算法的在线课程。

积极心态与持续努力:

刷题的过程可能会比较枯燥,但它能够显著提升编程能力和算法思维。无论是为了通过华为OD的招聘考试,还是为了未来的职业发展,这些积累都会成为重要的财富。

考试注意细节

  1. 本地编写代码

    • 在本地 IDE(如 VS Code、PyCharm 等)上编写、保存和调试代码,确保逻辑正确后再复制粘贴到考试页面。这样可以减少语法错误,提高代码准确性。
  2. 调整心态,保持冷静

    • 遇到提示不足或实现不确定的问题时,不必慌张,可以采用更简单或更有把握的方法替代,确保思路清晰。
  3. 输入输出完整性

    • 注意训练和考试时都需要编写完整的输入输出代码,尤其是和题目示例保持一致。完成代码后务必及时调试,确保功能符合要求。
  4. 快捷键使用

    • 删除行可用 Ctrl+D,复制、粘贴和撤销分别为 Ctrl+CCtrl+VCtrl+Z,这些可以正常使用。
    • 避免使用 Ctrl+S,以免触发浏览器的保存功能。
  5. 浏览器要求

    • 使用最新版的 Google Chrome 浏览器完成考试,确保摄像头开启并正常工作。考试期间不要切换到其他网站,以免影响考试成绩。
  6. 交卷相关

    • 答题前,务必仔细查看题目示例,避免遗漏要求。
    • 每完成一道题后,点击【保存并调试】按钮,多次保存和调试是允许的,系统会记录得分最高的一次结果。完成所有题目后,点击【提交本题型】按钮。
    • 确保在考试结束前提交试卷,避免因未保存或调试失误而丢分。
  7. 时间和分数安排

    • 总时间:150 分钟;总分:400 分。
    • 试卷结构:2 道一星难度题(每题 100 分),1 道二星难度题(200 分)。及格分为 150 分。合理分配时间,优先完成自己擅长的题目。
  8. 考试环境准备

    • 考试前请备好草稿纸和笔。考试中尽量避免离开座位,确保监控画面正常。
    • 如需上厕所,请提前规划好时间以减少中途离开监控的可能性。
  9. 技术问题处理

    • 如果考试中遇到断电、断网、死机等技术问题,可以关闭浏览器并重新打开试卷链接继续作答。
    • 出现其他问题,请第一时间联系 HR 或监考人员进行反馈。

祝你考试顺利,取得理想成绩!


原文地址:https://blog.csdn.net/m0_63168877/article/details/145280396

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!