
专栏导读
本专栏收录于《华为OD机试真题(Java/Python/JS/C/C++/Go)》。
刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新。
一、题目内容
小菊听说在一个神秘的城堡里藏着宝藏,她决定去寻宝。城堡由 n n n 个房间组成,某些房间之间有通道相连。小菊从房间 s s s 出发,想要到达藏有宝藏的房间 t t t。
给定房间数量
n
n
n、通道列表 edges、起点
s
s
s 和终点
t
t
t,求小菊从起点到终点经过通道最短的路径长度(通道长度相同且长度为 1,例如:房间 1 → 房间 2 → 房间 3,经过 2 条通道,长度 = 2)。
如果起点终点相同,返回 0;如果房间无法到达,返回 −1。
二、输入描述
顶点数量 n n n:
- 1 ≤ n ≤ 10000 1 \le n \le 10000 1≤n≤10000
- 顶点编号:从 1 到 n n n(连续编号)
- 单顶点情况: n = 1 n = 1 n=1 时,起点终点相同,路径长度为 0
边数量和边参数 edges:
- 边数量 m m m: 0 ≤ m ≤ min ( n ( n − 1 ) 2 , 100000 ) 0 \le m \le \min\left(\frac{n(n-1)}{2},100000\right) 0≤m≤min(2n(n−1),100000)
- 每条边 [ u , v ] [u,v] [u,v]:
- 1 ≤ u , v ≤ n 1 \le u,v \le n 1≤u,v≤n(顶点编号合法)
- u u u 和 v v v 可以相等(允许自环,但不影响路径长度)
- 边是无向的: ( u , v ) (u,v) (u,v) 表示房间 u u u 和房间 v v v 之间有一条双向通道
- 允许重边:多条边连接同一对顶点(不影响最短路径)
起点和终点参数:
- 起点 s s s: 1 ≤ s ≤ n 1 \le s \le n 1≤s≤n
- 终点 t t t: 1 ≤ t ≤ n 1 \le t \le n 1≤t≤n
三、输出描述
返回从起点 s s s 到终点 t t t 的最短路径长度(经过的通道数)。若 s = t s = t s=t 则返回 0;若不可达则返回 −1。
四、测试用例
测试用例1:
1、输入
4
1,2 2,3 3,4 1,3
1 4
2、输出
2
3、说明
最短路径为 1 → 3 → 4,经过 2 条通道;而 1 → 2 → 3 → 4 需要 3 条,因此输出 2。
测试用例2:
1、输入
4
1,2 2,1 3,4 4,3
1 3
2、输出
-1
3、说明
房间 1 和 2 属于一个连通分量,房间 3 和 4 属于另一个连通分量。即使存在反向重复边,也无法从 1 到达 3。
五、解题思路
1. 问题分析
城堡中的每个房间可以看作图中的一个顶点,每条通道可以看作连接两个顶点的无向边。
由于每条通道的长度均为 1,因此本题本质上是求无权无向图中,从起点 s 到终点 t 的最短路径长度。
如果起点和终点相同,直接返回 0;如果起点无法到达终点,则返回 -1。
2. 数据结构选择
使用邻接表存储无向图,使用队列辅助广度优先搜索,使用距离数组 dist 记录最短路径长度。
- 邻接表:存储每个房间直接连接的其他房间。
- 队列:保证 BFS 按距离从近到远搜索。
- 距离数组:初始化为 -1,既表示未访问状态,也记录最短距离。
相比邻接矩阵,邻接表只需 O(n+m) 的空间,更适合 n 最大为 10000、m 最大为 100000 的数据规模。
3. 算法步骤
- 读取房间数量 n、边列表、起点 s 和终点 t。
- 如果 s == t,直接输出 0。
- 根据边列表构建无向图,每条有效边需要双向添加。
- 初始化距离数组为 -1,设置 dist[s]=0,并将 s 加入队列。
- 从队列取出当前房间,遍历其相邻房间。
- 如果相邻房间尚未访问,则将其距离设置为当前距离加 1,并加入队列。
- 如果首次发现终点 t,立即返回对应距离。
- 如果队列为空仍未找到终点,输出 -1。
4. 正确性分析
BFS 按路径长度逐层扩展。由于所有边的长度均为 1,首次访问一个房间时,所经过的通道数一定最少。
因此,首次到达终点时得到的距离就是最短路径长度。
自环不影响最短路径,可以忽略;重边通过距离数组避免重复访问,不影响结果。
5. 复杂度分析
- 时间复杂度:O(n+m),每个顶点最多入队一次,每条边最多检查两次。
- 空间复杂度:O(n+m),主要用于邻接表、距离数组及 BFS 队列。
该算法能够高效处理题目规定的数据规模。
六、Python算法源码
import re
import sys
from collections import deque
def main():
n = int(sys.stdin.readline().strip())
# 第二行允许为空,表示 m=0。
# 按行读取,避免丢失空的边列表。
edges_line = sys.stdin.readline()
s, t = map(int, sys.stdin.readline().split())
if s == t:
print(0)
return
# 使用邻接表存储无向图。
# 正则支持逗号两侧存在空格的输入。
graph = [[] for _ in range(n + 1)]
for a, b in re.findall(
r'(\d+)\s*,\s*(\d+)', edges_line
):
u, v = int(a), int(b)
if u != v:
# 自环不影响最短路径。
graph[u].append(v)
graph[v].append(u)
# dist=-1 表示房间尚未访问。
# BFS 首次访问一个房间时,
# 记录的距离就是最短距离。
dist = [-1] * (n + 1)
dist[s] = 0
queue = deque([s])
while queue:
u = queue.popleft()
for v in graph[u]:
if dist[v] != -1:
# 跳过已访问房间,
# 避免重边造成重复入队。
continue
dist[v] = dist[u] + 1
if v == t:
# 首次到达终点,
# 得到最少经过的通道数。
print(dist[v])
return
queue.append(v)
# 搜索结束仍未找到终点。
print(-1)
if __name__ == '__main__':
main()
七、JavaScript算法源码
const fs = require('fs');
// 固定三行输入:n、边列表、s t。
// 保留空的第二行,支持 m=0。
const lines = fs.readFileSync(0, 'utf8').split(/\r?\n/);
const n = Number(lines[0].trim());
const edgeLine = lines[1] ?? '';
const [s, t] = lines[2].trim().split(/\s+/).map(Number);
if (s === t) {
console.log(0);
} else {
// 使用邻接表表示无向图。
const graph = Array.from(
{ length: n + 1 },
() => []
);
// 匹配每条边,允许逗号旁边存在空格。
const pattern = /(\d+)\s*,\s*(\d+)/g;
for (const match of edgeLine.matchAll(pattern)) {
const u = Number(match[1]);
const v = Number(match[2]);
if (u !== v) {
// 每条无向边添加两个方向。
// 自环不影响最短路,直接忽略。
graph[u].push(v);
graph[v].push(u);
}
}
// dist=-1 表示尚未访问。
const dist = Array(n + 1).fill(-1);
// 使用数组模拟 BFS 队列。
// head 指向队头,避免使用 shift()
// 导致数组元素反复移动。
const queue = [s];
let head = 0;
dist[s] = 0;
let answer = -1;
while (head < queue.length && answer === -1) {
const u = queue[head++];
for (const v of graph[u]) {
if (dist[v] !== -1) {
// 重边以及已经访问的节点,
// 都不需要再次搜索。
continue;
}
dist[v] = dist[u] + 1;
if (v === t) {
// BFS 首次发现终点,
// 对应的距离一定最短。
answer = dist[v];
break;
}
queue.push(v);
}
}
console.log(answer);
}
八、C算法源码
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
// 邻接表中的边结构。
// to 表示目标顶点。
// next 表示同一起点的下一条边。
typedef struct {
int to;
int next;
} Edge;
// 动态读取一整行。
// 边列表可能非常长,因此不能使用较小的固定数组。
static char *read_line(void) {
size_t capacity = 128;
size_t length = 0;
char *line = malloc(capacity);
int ch;
while ((ch = getchar()) != EOF && ch != '\n') {
if (length + 1 >= capacity) {
capacity *= 2;
char *new_line = realloc(line, capacity);
if (new_line == NULL) {
free(line);
exit(EXIT_FAILURE);
}
line = new_line;
}
line[length++] = (char)ch;
}
line[length] = '\0';
return line;
}
// 向链式前向星中添加一条有向边。
// 无向边需要调用两次,分别添加 u->v 和 v->u。
static void add_edge(
Edge **edges,
int *count,
int *capacity,
int head[],
int u,
int v
) {
if (*count == *capacity) {
*capacity *= 2;
Edge *new_edges = realloc(
*edges,
(size_t)(*capacity) * sizeof(Edge)
);
if (new_edges == NULL) {
free(*edges);
exit(EXIT_FAILURE);
}
*edges = new_edges;
}
(*edges)[*count].to = v;
(*edges)[*count].next = head[u];
head[u] = (*count)++;
}
int main(void) {
int n, s, t;
if (scanf("%d", &n) != 1) {
return 0;
}
// 清理第一行剩余字符,
// 然后完整读取第二行的边列表。
int ch;
while ((ch = getchar()) != '\n' && ch != EOF) {
}
char *edges_line = read_line();
if (scanf("%d %d", &s, &t) != 2) {
free(edges_line);
return 0;
}
if (s == t) {
puts("0");
free(edges_line);
return 0;
}
// head[u] 保存顶点 u 的第一条边编号。
// -1 表示没有相邻边。
int *head = malloc(
(size_t)(n + 1) * sizeof(int)
);
for (int i = 0; i <= n; ++i) {
head[i] = -1;
}
int edge_count = 0;
int capacity = 16;
Edge *edges = malloc(
(size_t)capacity * sizeof(Edge)
);
// 解析边列表,每条边格式为 u,v。
// strtol 支持连续读取整数,
// 同时处理逗号两侧的空格。
char *p = edges_line;
while (*p) {
while (isspace((unsigned char)*p)) {
++p;
}
if (!*p) {
break;
}
char *end;
int u = (int)strtol(p, &end, 10);
p = end;
while (isspace((unsigned char)*p)) {
++p;
}
if (*p == ',') {
++p;
}
while (isspace((unsigned char)*p)) {
++p;
}
int v = (int)strtol(p, &end, 10);
p = end;
if (u != v) {
// 无向边必须双向添加。
// 忽略自环,降低不必要的遍历开销。
add_edge(
&edges, &edge_count,
&capacity, head, u, v
);
add_edge(
&edges, &edge_count,
&capacity, head, v, u
);
}
}
// BFS 使用数组实现队列。
// 队列最大只需容纳 n 个不同的房间。
int *dist = malloc(
(size_t)(n + 1) * sizeof(int)
);
int *queue = malloc(
(size_t)(n + 1) * sizeof(int)
);
for (int i = 0; i <= n; ++i) {
dist[i] = -1;
}
int front = 0;
int back = 0;
int answer = -1;
dist[s] = 0;
queue[back++] = s;
while (front < back && answer == -1) {
int u = queue[front++];
// 沿着链式前向星遍历 u 的全部邻接点。
for (int e = head[u]; e != -1;
e = edges[e].next) {
int v = edges[e].to;
if (dist[v] != -1) {
// 已访问的房间不重复入队。
continue;
}
dist[v] = dist[u] + 1;
if (v == t) {
// 第一次到达终点,距离最短。
answer = dist[v];
break;
}
queue[back++] = v;
}
}
printf("%d\n", answer);
// 释放动态申请的内存。
free(queue);
free(dist);
free(edges);
free(head);
free(edges_line);
return 0;
}
九、C++算法源码
#include <cctype>
#include <cstdlib>
#include <iostream>
#include <queue>
#include <string>
#include <vector>
using namespace std;
int main() {
int n, s, t;
cin >> n;
string discarded;
string edgesLine;
// 消耗第一行末尾的换行符。
getline(cin, discarded);
// 第二行是边列表,允许为空。
getline(cin, edgesLine);
cin >> s >> t;
if (s == t) {
cout << 0 << '\n';
return 0;
}
// 邻接表存储无向图。
// graph[u] 中保存与 u 直接相连的所有顶点。
vector<vector<int>> graph(n + 1);
// 使用 strtol 依次读取边的两个端点,
// 可以兼容逗号两侧存在空格的情况。
const char *p = edgesLine.c_str();
while (*p) {
while (isspace(
static_cast<unsigned char>(*p)
)) {
++p;
}
if (!*p) {
break;
}
char *end;
int u = static_cast<int>(
strtol(p, &end, 10)
);
p = end;
while (isspace(
static_cast<unsigned char>(*p)
)) {
++p;
}
if (*p == ',') {
++p;
}
while (isspace(
static_cast<unsigned char>(*p)
)) {
++p;
}
int v = static_cast<int>(
strtol(p, &end, 10)
);
p = end;
if (u != v) {
// 无向图双向添加邻接点。
// 自环不会影响最短路径。
graph[u].push_back(v);
graph[v].push_back(u);
}
}
// dist=-1 表示未访问。
// 同时记录每个顶点到起点的最短距离。
vector<int> dist(n + 1, -1);
queue<int> q;
dist[s] = 0;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : graph[u]) {
if (dist[v] != -1) {
// 防止重边和环导致重复访问。
continue;
}
dist[v] = dist[u] + 1;
if (v == t) {
// BFS 首次找到终点,
// 经过的通道数量一定最少。
cout << dist[v] << '\n';
return 0;
}
q.push(v);
}
}
// 搜索完成后仍未找到终点。
cout << -1 << '\n';
return 0;
}
十、Go算法源码
package main
import (
"bufio"
"fmt"
"os"
"regexp"
"strconv"
"strings"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
// 第二行可能包含 10 万条边,
// 长度超过 Scanner 默认的 64 KB。
// 因此设置最大 4 MB 的输入缓冲区。
scanner.Buffer(
make([]byte, 64*1024),
4*1024*1024,
)
scanner.Scan()
n, _ := strconv.Atoi(
strings.TrimSpace(scanner.Text()),
)
// 读取边列表,第二行可能为空。
scanner.Scan()
edgesLine := scanner.Text()
// 第三行读取起点和终点。
scanner.Scan()
parts := strings.Fields(scanner.Text())
s, _ := strconv.Atoi(parts[0])
t, _ := strconv.Atoi(parts[1])
if s == t {
fmt.Println(0)
return
}
// 使用邻接表存储无向图。
graph := make([][]int, n+1)
// 匹配 u,v 格式的边,
// 允许逗号两侧存在空格。
pattern := regexp.MustCompile(
`(\d+)\s*,\s*(\d+)`,
)
for _, match := range pattern.FindAllStringSubmatch(
edgesLine, -1,
) {
u, _ := strconv.Atoi(match[1])
v, _ := strconv.Atoi(match[2])
if u != v {
// 无向边需要双向存储。
// 自环可直接忽略。
graph[u] = append(graph[u], v)
graph[v] = append(graph[v], u)
}
}
// dist=-1 表示房间未访问。
// BFS 首次访问一个房间时,
// 记录的就是最短距离。
dist := make([]int, n+1)
for i := range dist {
dist[i] = -1
}
dist[s] = 0
// 使用切片和 head 下标实现队列。
// 避免反复删除切片头部元素。
queue := []int{s}
for head := 0; head < len(queue); head++ {
u := queue[head]
for _, v := range graph[u] {
if dist[v] != -1 {
// 已访问的房间不再入队,
// 同时处理重复边。
continue
}
dist[v] = dist[u] + 1
if v == t {
// BFS 首次发现终点,
// 路径长度一定最短。
fmt.Println(dist[v])
return
}
queue = append(queue, v)
}
}
// 所有可达房间已遍历,终点仍未找到。
fmt.Println(-1)
}
🏆下一篇:华为OD机试真题 - 简易内存池(Python/JS/C/C++/Go 新系统 200分)
🏆本文收录于,华为OD机试真题(Python/JS/C/C++/Go)
刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新。

本博客详解华为OD机试真题'小菊的寻宝之旅',核心是求无权无向图中起点到终点的最短路径长度(边数)。采用广度优先搜索(BFS)算法,利用邻接表建图、队列层序遍历及距离数组记录最短距离。支持自环与重边,时间复杂度O(n+m),空间复杂度O(n+m),适用于n≤10000、m≤100000规模。
246

被折叠的 条评论
为什么被折叠?



