华为OD机试 - 迷宫相遇 - 广度优先搜索BFS(Python/JS/C/C++ 新系统 200分)

在这里插入图片描述

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

专栏导读

本专栏收录于《华为OD机试真题(Python/JS/C/C++)》

刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新。

一、题目描述

一个封闭迷宫,有 n 个房间(编号 0 到 n − 1),相邻房间由双向门连接。玩家 A 自由移动,玩家 B 沿给定路径来回巡逻。求玩家 A 的最优移动策略,使得与玩家 B 在最少回合相遇。

注意: A 和 B 初始出发点不同。

规则:

  • 每回合,A 可以移动到相邻房间或停留在当前房间

  • B 按给定路径循环移动:从起点开始,每回合移动到路径下一个位置,到达末端后折返,回到起点后再折返,如此循环往复。间内无需停留,如:单程 [0,1,2],完整路径为 [0,1,2,1,0,1,…]。初始在 0 点,第一回合进到 1…

  • A 和 B 某回合结束后在同一房间,视为相遇

二、输入描述

第一行: n 房间数量 1 < n < 100

第二行:edges 房间连接关系,例如 0,1 1,2 2,3 3,4,双向连通,空数组 Q 表示无元连接

第三行: startA A 的初始房间编号

第四行: patrolPath B 的巡逻路径(一次单程),例如 0,1,2 表示 B 从 0 出发,经过 1 到达 2,然后原路返回,路径为 0,1,2,1,0,1,2,.....。至少有一个元素,表示 B 原地不动

三、输出描述

A 和 B 相遇的最小回合次数,若无法相遇返回 −1

四、测试用例

测试用例1:

1、输入

5
0,1 1,2 2,3 3,4
4
0,1,2,3,4

2、输出

2

3、说明

第 1 回合:

A: 4 -> 3
B: 0 -> 1

第 2 回合:

A: 3 -> 2
B: 1 -> 2

因此答案为 2。

测试用例2:

1、输入

8
0,1 1,2 2,3 3,4 4,5 5,6 6,7 7,5
0
5,6,7,5

2、输出

6

3、说明

B 的完整周期为:

5,6,7,5,7,6

之后重新循环到 5。

一种最优策略为:

回合0:A=0,B=5
回合1:A=0,B=6
回合2:A=1,B=7
回合3:A=2,B=5
回合4:A=3,B=7
回合5:A=4,B=6
回合6:A=5,B=5

所以最少 6 回合。

五、解题思路

B 的移动是完全确定且周期性的,因此问题不能只记录 A 当前在哪个房间,还必须记录“当前 B 走到了巡逻周期的哪个阶段”。

首先把 B 给出的单程路径转换成完整往返周期。例如单程路径 [0,1,2],实际周期可表示为 [0,1,2,1],之后再次循环到 0;如果路径只有一个房间,则 B 永远停在该房间,周期长度为 1。

接下来使用 BFS。状态定义为:

(A 当前房间, B 当前巡逻周期下标)

假设当前状态是 (a, phase),进入下一回合后,B 一定移动到
cycle[(phase + 1) % period]。

与此同时,A 有两类选择:停留在房间 a,或者移动到 graph[a] 中任意相邻房间。对于 A 的每一个下一位置,都和 B 本回合结束后的房间比较;如果相同,就立即返回当前回合数 + 1。

由于 BFS 按回合数逐层扩展,所以第一次找到相遇状态时,一定对应最少回合。

visited 不能只记录 A 的房间,因为 A 即使再次到达同一房间,只要 B 所处的巡逻阶段不同,后续结果就可能完全不同。因此访问标记必须是 visited[room][phase]。

整个状态空间是有限的,最多只有 n × period 个状态。如果所有状态都访问完仍没有相遇,则以后只会重复之前的状态,所以可以确定永远无法相遇,返回 -1。

设 B 单程路径长度为 m,则周期 P=1(m=1)或 P=2(m-1)。时间复杂度约为 O(P×(n+E)),空间复杂度为 O(n×P+E)。

六、Python算法源码

from collections import deque
import sys


def min_meet_rounds(n, graph, start_a, path):
    """
    将 B 的单程巡逻路径展开成完整周期。

    例如:
    [0, 1, 2] -> [0, 1, 2, 1]

    循环后自然再次回到 0。
    """
    if len(path) == 1:
        cycle = path[:]
    else:
        cycle = path + path[-2:0:-1]

    # 题目保证二者起点不同,这里做兼容处理。
    if start_a == cycle[0]:
        return 0

    period = len(cycle)

    /*
    Python 不支持这种注释语法,下面使用 #。
    */

    # visited[A所在房间][B的周期下标]
    # 同一个 A 房间在 B 不同巡逻阶段下必须视为不同状态。
    visited = [[False] * period for _ in range(n)]

    # 队列元素:
    # (A所在房间, B当前周期下标, 已经过的回合数)
    queue = deque([(start_a, 0, 0)])
    visited[start_a][0] = True

    while queue:
        a_room, phase, rounds = queue.popleft()

        # 下一回合结束时 B 所在的位置。
        next_phase = (phase + 1) % period
        b_next_room = cycle[next_phase]

        next_rounds = rounds + 1

        # A 可以选择:
        # 1. 停留在当前房间
        # 2. 移动到任意相邻房间
        possible_rooms = [a_room] + graph[a_room]

        for next_a in possible_rooms:

            # 每回合结束后判断是否相遇。
            if next_a == b_next_room:
                return next_rounds

            if not visited[next_a][next_phase]:
                visited[next_a][next_phase] = True

                queue.append((
                    next_a,
                    next_phase,
                    next_rounds
                ))

    # 有限状态全部访问仍没有相遇,之后只会重复。
    return -1


def main():
    lines = sys.stdin.read().splitlines()

    n = int(lines[0].strip())
    edges_line = lines[1].strip()
    start_a = int(lines[2].strip())

    path = [
        int(x.strip())
        for x in lines[3].strip().split(',')
    ]

    # 邻接表保存无向图。
    graph = [[] for _ in range(n)]

    # 兼容空行、[]、Q。
    if (
        edges_line
        and edges_line != '[]'
        and edges_line.upper() != 'Q'
    ):
        for token in edges_line.split():
            u, v = map(int, token.split(','))

            graph[u].append(v)
            graph[v].append(u)

    print(min_meet_rounds(
        n,
        graph,
        start_a,
        path
    ))


if __name__ == '__main__':
    main()

七、JavaScript算法源码

const fs = require('fs');

const lines = fs.readFileSync(0, 'utf8').split(/\r?\n/);

const n = Number(lines[0].trim());
const edgesLine = (lines[1] || '').trim();
const startA = Number(lines[2].trim());

const path = lines[3]
    .trim()
    .split(',')
    .map(s => Number(s.trim()));

// 使用邻接表保存无向图。
const graph = Array.from(
    { length: n },
    () => []
);

// 兼容空行、[] 和 Q。
if (
    edgesLine
    && edgesLine !== '[]'
    && edgesLine.toUpperCase() !== 'Q'
) {
    for (const token of edgesLine.split(/\s+/)) {
        const [u, v] = token
            .split(',')
            .map(Number);

        graph[u].push(v);
        graph[v].push(u);
    }
}

function minMeetRounds(n, graph, startA, path) {

    /*
     * 构造 B 的完整巡逻周期。
     *
     * [0,1,2]
     * =>
     * [0,1,2,1]
     *
     * 周期结束后重新回到 0。
     */
    let cycle;

    if (path.length === 1) {
        cycle = [path[0]];
    } else {
        cycle = [...path];

        // 端点不能重复,否则就相当于 B 在端点多停一回合。
        for (let i = path.length - 2; i >= 1; i--) {
            cycle.push(path[i]);
        }
    }

    if (startA === cycle[0]) {
        return 0;
    }

    const period = cycle.length;

    /*
     * visited[A房间][B巡逻阶段]
     *
     * A 在同一房间、但 B 的巡逻阶段不同,
     * 必须认为是两个不同的 BFS 状态。
     */
    const visited = Array.from(
        { length: n },
        () => Array(period).fill(false)
    );

    /*
     * queue 中保存:
     * [A房间, B周期下标, 已经过回合数]
     *
     * 使用 head 下标而不是 shift(),
     * 可以避免数组频繁整体移动。
     */
    const queue = [[startA, 0, 0]];
    let head = 0;

    visited[startA][0] = true;

    while (head < queue.length) {

        const [aRoom, phase, rounds] = queue[head++];

        const nextPhase =
            (phase + 1) % period;

        const bNextRoom =
            cycle[nextPhase];

        const nextRounds =
            rounds + 1;

        /*
         * 情况1:
         * A 原地不动。
         */
        if (aRoom === bNextRoom) {
            return nextRounds;
        }

        if (!visited[aRoom][nextPhase]) {
            visited[aRoom][nextPhase] = true;

            queue.push([
                aRoom,
                nextPhase,
                nextRounds
            ]);
        }

        /*
         * 情况2:
         * A 移动到相邻房间。
         */
        for (const nextA of graph[aRoom]) {

            if (nextA === bNextRoom) {
                return nextRounds;
            }

            if (!visited[nextA][nextPhase]) {

                visited[nextA][nextPhase] = true;

                queue.push([
                    nextA,
                    nextPhase,
                    nextRounds
                ]);
            }
        }
    }

    return -1;
}

console.log(
    minMeetRounds(
        n,
        graph,
        startA,
        path
    )
);

八、C算法源码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#define MAX_N 100
#define MAX_LINE 10000
#define MAX_PATH 5000

/*
 * n < 100,因此 C 版本直接使用邻接矩阵保存房间连接关系。
 * graph[u][v] = 1 表示 u 与 v 之间存在双向门。
 */
static int graph[MAX_N][MAX_N];

static int minMeetRounds(
        int n,
        int startA,
        const int *path,
        int pathLen) {

    int period;

    if (pathLen == 1) {
        period = 1;
    } else {
        period = 2 * (pathLen - 1);
    }

    int *cycle =
        (int *)malloc(sizeof(int) * period);

    int index = 0;

    /*
     * 构造 B 的完整往返巡逻周期。
     *
     * [0,1,2] -> [0,1,2,1]
     */
    for (int i = 0; i < pathLen; ++i) {
        cycle[index++] = path[i];
    }

    /*
     * 反向路径只加入中间节点,
     * 避免 B 在端点多停一回合。
     */
    for (int i = pathLen - 2; i >= 1; --i) {
        cycle[index++] = path[i];
    }

    if (startA == cycle[0]) {
        free(cycle);
        return 0;
    }

    /*
     * 一共最多有 n * period 个 BFS 状态。
     *
     * 将二维状态
     * (A房间, B周期下标)
     *
     * 编码为:
     *
     * state = A房间 * period + B周期下标
     */
    int totalStates = n * period;

    int *dist =
        (int *)malloc(sizeof(int) * totalStates);

    int *queue =
        (int *)malloc(sizeof(int) * totalStates);

    for (int i = 0; i < totalStates; ++i) {
        dist[i] = -1;
    }

    int head = 0;
    int tail = 0;

    int startState = startA * period;

    queue[tail++] = startState;
    dist[startState] = 0;

    while (head < tail) {

        int state = queue[head++];

        int aRoom = state / period;
        int phase = state % period;

        int rounds = dist[state];

        /*
         * 下一回合 B 一定移动到这个周期位置。
         */
        int nextPhase =
            (phase + 1) % period;

        int bNextRoom =
            cycle[nextPhase];

        int nextRounds =
            rounds + 1;

        /*
         * 情况1:
         * A 停留在原房间。
         */
        if (aRoom == bNextRoom) {
            free(cycle);
            free(dist);
            free(queue);

            return nextRounds;
        }

        int stayState =
            aRoom * period + nextPhase;

        if (dist[stayState] == -1) {

            dist[stayState] =
                nextRounds;

            queue[tail++] =
                stayState;
        }

        /*
         * 情况2:
         * A 移动到任意相邻房间。
         */
        for (int nextA = 0; nextA < n; ++nextA) {

            if (!graph[aRoom][nextA]) {
                continue;
            }

            /*
             * 只判断回合结束时的位置是否相同。
             */
            if (nextA == bNextRoom) {

                free(cycle);
                free(dist);
                free(queue);

                return nextRounds;
            }

            int nextState =
                nextA * period + nextPhase;

            if (dist[nextState] == -1) {

                dist[nextState] =
                    nextRounds;

                queue[tail++] =
                    nextState;
            }
        }
    }

    free(cycle);
    free(dist);
    free(queue);

    return -1;
}

int main(void) {

    char line[MAX_LINE];

    int n;
    int startA;

    int path[MAX_PATH];
    int pathLen = 0;

    /* 第一行:n */
    fgets(line, sizeof(line), stdin);
    n = atoi(line);

    /* 第二行:edges */
    fgets(line, sizeof(line), stdin);

    char edgesLine[MAX_LINE];
    strcpy(edgesLine, line);

    /* 第三行:startA */
    fgets(line, sizeof(line), stdin);
    startA = atoi(line);

    /* 第四行:patrolPath */
    fgets(line, sizeof(line), stdin);

    char *token =
        strtok(line, ",\r\n");

    while (token != NULL) {

        path[pathLen++] =
            atoi(token);

        token =
            strtok(NULL, ",\r\n");
    }

    /*
     * 解析 edges。
     * 兼容空行、[]、Q。
     */
    char *p = edgesLine;

    while (isspace((unsigned char)*p)) {
        ++p;
    }

    if (
        *p != '\0'
        && *p != '\n'
        && strncmp(p, "[]", 2) != 0
        && toupper((unsigned char)*p) != 'Q'
    ) {

        token =
            strtok(p, " \t\r\n");

        while (token != NULL) {

            int u;
            int v;

            if (
                sscanf(
                    token,
                    "%d,%d",
                    &u,
                    &v
                ) == 2
            ) {
                // 双向门。
                graph[u][v] = 1;
                graph[v][u] = 1;
            }

            token =
                strtok(NULL, " \t\r\n");
        }
    }

    printf(
        "%d\n",
        minMeetRounds(
            n,
            startA,
            path,
            pathLen
        )
    );

    return 0;
}

九、C++算法源码

#include <iostream>
#include <vector>
#include <queue>
#include <sstream>
#include <string>
#include <tuple>

using namespace std;

int minMeetRounds(
        int n,
        const vector<vector<int>>& graph,
        int startA,
        const vector<int>& path) {

    /*
     * 先保存正向路径。
     */
    vector<int> cycle = path;

    /*
     * 将单程巡逻路径展开成完整往返周期。
     *
     * [0,1,2]
     * =>
     * [0,1,2,1]
     *
     * 不重复加入两个端点,
     * 因为 B 到达端点后立即折返。
     */
    if (path.size() > 1) {

        for (
            int i =
                static_cast<int>(path.size()) - 2;
            i >= 1;
            --i
        ) {
            cycle.push_back(path[i]);
        }
    }

    if (startA == cycle[0]) {
        return 0;
    }

    int period =
        static_cast<int>(cycle.size());

    /*
     * visited[A当前房间][B巡逻周期下标]
     *
     * 二者共同确定完整状态。
     */
    vector<vector<bool>> visited(
        n,
        vector<bool>(period, false)
    );

    /*
     * 队列状态:
     *
     * A 当前房间
     * B 当前周期下标
     * 已经过回合数
     */
    queue<tuple<int, int, int>> q;

    q.emplace(
        startA,
        0,
        0
    );

    visited[startA][0] = true;

    while (!q.empty()) {

        auto [aRoom, phase, rounds] =
            q.front();

        q.pop();

        /*
         * B 下一回合的位置完全确定。
         */
        int nextPhase =
            (phase + 1) % period;

        int bNextRoom =
            cycle[nextPhase];

        int nextRounds =
            rounds + 1;

        /*
         * 情况1:
         * A 停留在原房间。
         */
        if (aRoom == bNextRoom) {
            return nextRounds;
        }

        if (!visited[aRoom][nextPhase]) {

            visited[aRoom][nextPhase] =
                true;

            q.emplace(
                aRoom,
                nextPhase,
                nextRounds
            );
        }

        /*
         * 情况2:
         * A 移动到任意相邻房间。
         */
        for (int nextA : graph[aRoom]) {

            // 每回合结束后判断是否同房。
            if (nextA == bNextRoom) {
                return nextRounds;
            }

            if (!visited[nextA][nextPhase]) {

                visited[nextA][nextPhase] =
                    true;

                q.emplace(
                    nextA,
                    nextPhase,
                    nextRounds
                );
            }
        }
    }

    /*
     * 有限状态全部遍历后仍然没有相遇。
     */
    return -1;
}

int main() {

    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    string line;

    /* 第一行:n */
    getline(cin, line);
    int n = stoi(line);

    /* 第二行:edges */
    string edgesLine;
    getline(cin, edgesLine);

    /* 第三行:startA */
    getline(cin, line);
    int startA = stoi(line);

    /* 第四行:patrolPath */
    string patrolLine;
    getline(cin, patrolLine);

    vector<vector<int>> graph(n);

    /*
     * 解析无向边。
     * 兼容 [] 和 Q。
     * 空行自然不会进入这里。
     */
    if (
        !edgesLine.empty()
        && edgesLine != "[]"
        && edgesLine != "Q"
        && edgesLine != "q"
    ) {

        stringstream ss(edgesLine);

        string token;

        while (ss >> token) {

            size_t comma =
                token.find(',');

            int u =
                stoi(
                    token.substr(
                        0,
                        comma
                    )
                );

            int v =
                stoi(
                    token.substr(
                        comma + 1
                    )
                );

            graph[u].push_back(v);
            graph[v].push_back(u);
        }
    }

    /*
     * 解析 B 的单程路径。
     */
    vector<int> path;

    stringstream pathStream(
        patrolLine
    );

    string token;

    while (
        getline(
            pathStream,
            token,
            ','
        )
    ) {
        path.push_back(
            stoi(token)
        );
    }

    cout
        << minMeetRounds(
            n,
            graph,
            startA,
            path
        )
        << '\n';

    return 0;
}


🏆下一篇:华为OD机试真题 - 简易内存池(Python/JS/C/C++ 新系统 200分)

🏆本文收录于,华为OD机试真题(Python/JS/C/C++)

刷的越多,抽中的概率越大,私信哪吒,备注华为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、付费专栏及课程。

余额充值