现需要在某城市进行5G网络建设,已经选取N个地点设置5G基站,编号固定为1到N。接下来需要各个基站之间使用光纤进行连接以确保基站能互联互通。不同基站之间假设光纤的成本各不相同,且有些节点之间已经存在光纤相连。
请你设计算法,计算出能联通这些基站的最小成本是多少。
注意:基站的联通具有传递性,比如基站A与基站B架设了光纤,基站B与基站C也架设了光纤,则基站A与基站C视为可以互相联通。
0 < N ≤ 20
。0 < M < N * (N - 1) / 2
。X Y Z P
。
X
,Y
表示基站的编号,0 < X ≤ N
,0 < Y ≤ N
,X ≠ Y
。Z
表示在 X
、Y
之间架设光纤的成本,0 < Z < 100
。P
表示是否已存在光纤连接,0
表示未连接,1
表示已连接。-1
。输入
3
3
1 2 3 0
1 3 1 0
2 3 5 0
输出
4
说明
只需要在1,2以及1,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基站之间铺设光纤,其成本为 1
。
本题是经典的最小生成树问题的变种。最小生成树(Minimum Spanning Tree, MST)是指在一个无向连通图中,选择一部分边,使得所有顶点都连通,并且这些边的总权重最小。
在无向连通图中,生成树是指包含了全部顶点的极小连通子图。生成树包含原图全部的 n
个顶点和 n-1
条边。生成树的两个重要特性是:
最小生成树是指生成树中 n-1
条边的权重值之和最小。求解最小生成树的常用算法有 Prim算法 和 Kruskal算法。
Prim算法是基于顶点的贪心算法。其步骤如下:
适用场景:适用于节点少、边多的情况。
Kruskal算法是基于边的贪心算法。其步骤如下:
n-1
条边。适用场景:适用于节点多、边少的情况。
本题中,某些基站之间已经存在光纤连接(即某些边的权重为0)。处理这种情况的方法是将已连接的边视为权重为0的边,然后使用最小生成树算法求解。
N
和边数量 M
,然后读取每条边的信息。P = 1
),将其权重设为0。初始化并查集,每个基站初始为一个独立的连通分量。
依次选择权重最小的边,如果这条边的两个基站不在同一个连通分量中,则将其加入生成树,并合并这两个连通分量。
记录总成本。
N-1
,则输出 -1
;否则输出总成本。N-1
,否则无法连通所有基站。通过上述方法,可以高效地求解本题的最小建设成本。 #
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());
})();
输入处理:首先读取基站数量n
和基站对数量m
,然后初始化一个邻接矩阵graph
,表示基站之间的连接关系。如果两个基站已经联通,则边权为0,否则边权为输入的z
。
Prim算法实现:
dis
:记录每个节点到当前最小生成树的最短距离。输出结果:最终输出最小生成树的权重和。
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
}
}
}
输入处理:读取基站数量n
和基站对数量m
,然后读取每条边的信息并存储在edges
数组中。如果两个基站已经联通,则边权为0,否则边权为输入的z
。
Kruskal算法实现:
ufs
,用于管理节点的连通性。并查集实现:
输出结果:最终输出最小生成树的权重和。
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算法也是一种用于求解最小生成树的算法。它通过按边权从小到大排序,逐步选择边加入生成树,同时避免形成环。
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--;
}
}
}
Prim算法:
graph
存储图的边权信息。Integer.MAX_VALUE
,表示初始时各节点之间不连通。p == 0
,表示边权为z
;如果p == 1
,表示边权为0
(即已经连通)。inTree
数组记录哪些节点已经在生成树中。dis
数组记录每个节点到生成树的最短距离。-1
表示无法形成最小生成树。Kruskal算法:
Edge
类,用于存储边的起点、终点和权重。Edge
对象数组。UnionFindSet
)来管理节点的连通性。-1
表示无法形成最小生成树。并查集:
这两种算法都可以有效地求解最小生成树问题,选择哪种算法取决于图的结构和具体需求。
以下是关于 Prim 和 Kruskal 算法的详细注释和讲解。这两个算法用于在图中找到最小生成树(Minimum Spanning Tree,MST)。
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
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
minWeight
为 0
,从节点 1
开始构建生成树。inTree
跟踪哪些节点已在生成树中。dis
数组保存每个节点到生成树的最短距离。n
时,继续进行。
nodeIdx
。-1
,表示无法构成生成树。dis
数组,记录新加入的节点可能带来的更短距离。# 算法调用
print(prim())
prim()
函数并打印结果。# 并查集实现
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
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
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
2
时,表示所有节点已形成一个连通分量,返回当前权重。# 算法入口
print(kruskal())
kruskal()
函数并打印结果。Prim 算法:适合稠密图,利用邻接矩阵和顶点集来构造最小生成树,每次选择最近的节点加入生成树。
Kruskal 算法:适合稀疏图,使用边集和并查集处理,按边权重排序后逐一尝试加入边构建最小生成树。
这两种算法都有效解决了最小生成树问题,选用哪种取决于图的性质(稠密或稀疏)和具体需求。
以下是 Prim 算法和 Kruskal 算法的 C 语言版本
#include
#include
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;
}
邻接矩阵表示图:
graph
表示图,graph[i][j]
存储节点 i
到节点 j
的边权值。INT_MAX
)。逻辑流程:
1
)开始构建最小生成树。复杂度:
#include
#include
// 并查集结构
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;
}
边排序:
并查集:
逻辑流程:
复杂度:
以下是 Prim 和 Kruskal 算法:
#include
#include
#include
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;
}
数据结构:
graph
表示图,其中 graph[i][j]
表示节点 i
和节点 j
之间的边权值。inTree
表示节点是否已经加入到生成树中。dis
记录每个节点到生成树的最短距离。初始化:
INT_MAX
,表示两点之间最初不直接联通。1
开始构建生成树,初始化 dis
为从节点 1
到其他节点的距离。主循环:
dis
数组,检查是否通过新加入节点可以更短连接其他未加入节点。返回结果:
-1
表示图不连通。#include
#include
#include
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;
}
数据结构:
Edge
表示边,包括起点、终点和权重。UnionFindSet
实现并查集,用于判断节点是否属于同一连通分量。排序:
算法流程:
返回结果:
-1
表示图不连通。华为OD(Outsourcing Developer,外包开发工程师)是华为针对软件开发工程师岗位的一种招聘形式,主要包括笔试、技术面试以及综合面试等环节。尤其在笔试部分,算法题的机试至关重要。
机试是进入技术面的第一关:
华为OD机试(常被称为机考)主要考察算法和编程能力。只有通过机试,才能进入后续的技术面试环节。
技术面试需要手撕代码:
技术一面和二面通常会涉及现场编写代码或算法题。面试官会注重考察候选人的思路清晰度、代码规范性以及解决问题的能力。因此提前刷题、多练习是通过面试的重要保障。
入职后的可信考试:
入职华为后,还需要通过“可信考试”。可信考试分为三个等级:
2024年8月14日之后,华为OD机试的题库转为 E卷,由往年题库(D卷、A卷、B卷、C卷)和全新题目组成。刷题时可以参考以下策略:
关注历年真题:
适应新题目:
掌握常见算法:
华为OD考试通常涉及以下算法和数据结构:
保持编程规范:
官方参考:
加入刷题社区:
寻找系统性的教程:
刷题的过程可能会比较枯燥,但它能够显著提升编程能力和算法思维。无论是为了通过华为OD的招聘考试,还是为了未来的职业发展,这些积累都会成为重要的财富。
本地编写代码
调整心态,保持冷静
输入输出完整性
快捷键使用
Ctrl+D
,复制、粘贴和撤销分别为 Ctrl+C
,Ctrl+V
,Ctrl+Z
,这些可以正常使用。Ctrl+S
,以免触发浏览器的保存功能。浏览器要求
交卷相关
时间和分数安排
考试环境准备
技术问题处理
祝你考试顺利,取得理想成绩!