华为OD机试 - 小菊的寻宝之旅 - 广度优先搜索(Python/JS/C/C++/Go 新系统 100分)

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

在这里插入图片描述

华为OD机试 新系统 统一考试题库清单(持续收录中)以及考点说明(Python/JS/C/C++)。

专栏导读

本专栏收录于《华为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. 算法步骤

  1. 读取房间数量 n、边列表、起点 s 和终点 t。
  2. 如果 s == t,直接输出 0。
  3. 根据边列表构建无向图,每条有效边需要双向添加。
  4. 初始化距离数组为 -1,设置 dist[s]=0,并将 s 加入队列。
  5. 从队列取出当前房间,遍历其相邻房间。
  6. 如果相邻房间尚未访问,则将其距离设置为当前距离加 1,并加入队列。
  7. 如果首次发现终点 t,立即返回对应距离。
  8. 如果队列为空仍未找到终点,输出 -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算法的适用场景,发现新题目,随时更新。

在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哪 吒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值