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

在这里插入图片描述

华为OD机试 新系统 题库疯狂收录中,刷题点这里

专栏导读

本专栏收录于《华为OD机试(JAVA)真题》

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

一、题目描述

一个封闭迷宫,有 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)。

六、Java算法源码

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

        int n = Integer.parseInt(scanner.nextLine().trim());
        String edgesLine = scanner.nextLine().trim();
        int startA = Integer.parseInt(scanner.nextLine().trim());
        String patrolLine = scanner.nextLine().trim();

        // 使用邻接表保存迷宫的无向图结构。
        // 相比邻接矩阵,在边数量较少时可以减少不必要的遍历。
        List<List<Integer>> graph = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            graph.add(new ArrayList<>());
        }

        // 兼容空行、[]、Q 三种没有房间连接关系的写法。
        if (!edgesLine.isEmpty()
                && !edgesLine.equals("[]")
                && !edgesLine.equalsIgnoreCase("Q")) {

            String[] edgeTokens = edgesLine.split("\\s+");

            for (String token : edgeTokens) {
                String[] uv = token.split(",");

                int u = Integer.parseInt(uv[0].trim());
                int v = Integer.parseInt(uv[1].trim());

                // 房间之间是双向门,因此两个方向都加入邻接表。
                graph.get(u).add(v);
                graph.get(v).add(u);
            }
        }

        // 解析 B 的单程巡逻路径。
        String[] pathTokens = patrolLine.split(",");
        int[] path = new int[pathTokens.length];

        for (int i = 0; i < pathTokens.length; i++) {
            path[i] = Integer.parseInt(pathTokens[i].trim());
        }

        System.out.println(minMeetRounds(n, graph, startA, path));

        scanner.close();
    }

    private static int minMeetRounds(
            int n,
            List<List<Integer>> graph,
            int startA,
            int[] path) {

        /*
         * 将 B 的单程路径转换成完整的往返周期。
         *
         * 例如:
         * path = [0,1,2]
         *
         * B 的移动为:
         * 0 -> 1 -> 2 -> 1 -> 0 -> 1 ...
         *
         * 因此只需要保存一个周期:
         * [0,1,2,1]
         *
         * 数组循环后会自然从 1 回到 0。
         */
        int[] cycle;

        if (path.length == 1) {
            // 只有一个房间时,B 始终原地不动。
            cycle = new int[]{path[0]};
        } else {
            int period = 2 * (path.length - 1);
            cycle = new int[period];

            int index = 0;

            // 正向路径。
            for (int room : path) {
                cycle[index++] = room;
            }

            /*
             * 反向加入中间节点。
             *
             * 起点和终点不能重复加入,
             * 因为 B 到达端点后会立即折返,不会额外停留一回合。
             */
            for (int i = path.length - 2; i >= 1; i--) {
                cycle[index++] = path[i];
            }
        }

        // 题目保证起点不同,这里仍做兼容处理。
        if (startA == cycle[0]) {
            return 0;
        }

        int period = cycle.length;

        /*
         * BFS 状态:
         *
         * (A 当前房间, B 当前所处的巡逻周期位置)
         *
         * visited[a][phase] 表示这个组合状态是否已经访问过。
         *
         * 不能只记录 A 所在的房间,因为即使 A 在同一个房间,
         * B 处于不同巡逻阶段时,后面的结果也完全可能不同。
         */
        boolean[][] visited = new boolean[n][period];

        ArrayDeque<int[]> queue = new ArrayDeque<>();

        // state[0] = A 所在房间
        // state[1] = B 当前周期下标
        // state[2] = 已经过的回合数
        queue.offer(new int[]{startA, 0, 0});
        visited[startA][0] = true;

        while (!queue.isEmpty()) {

            int[] state = queue.poll();

            int aRoom = state[0];
            int phase = state[1];
            int rounds = state[2];

            /*
             * 进入下一回合以后,
             * B 的位置是完全确定的。
             */
            int nextPhase = (phase + 1) % period;
            int bNextRoom = cycle[nextPhase];

            int nextRounds = rounds + 1;

            /*
             * 情况 1:
             * A 本回合选择停留在原房间。
             *
             * 注意题目规定是在“回合结束后”判断是否相遇,
             * 所以应该与 B 移动后的房间进行比较。
             */
            if (aRoom == bNextRoom) {
                return nextRounds;
            }

            if (!visited[aRoom][nextPhase]) {
                visited[aRoom][nextPhase] = true;
                queue.offer(new int[]{
                        aRoom,
                        nextPhase,
                        nextRounds
                });
            }

            /*
             * 情况 2:
             * A 移动到任意一个相邻房间。
             */
            for (int nextARoom : graph.get(aRoom)) {

                // 回合结束时两人位于同一个房间。
                if (nextARoom == bNextRoom) {
                    return nextRounds;
                }

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

                    queue.offer(new int[]{
                            nextARoom,
                            nextPhase,
                            nextRounds
                    });
                }
            }
        }

        /*
         * 所有 “A房间 × B周期位置” 状态都搜索过仍无法相遇。
         * 后面只会重复这些状态,因此一定永远无法相遇。
         */
        return -1;
    }
}

七、效果展示

1、输入

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

2、输出

3

3、说明

B 始终位于房间 0,A 的路线:

3 -> 2 -> 1 -> 0

需要 3 回合。
在这里插入图片描述


🏆下一篇:华为OD机试 - 简易内存池 - 逻辑分析(Java 新系统 200分)

🏆本专栏收录于《华为OD机试(JAVA)真题》

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

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

哪 吒

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

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

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

打赏作者

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

抵扣说明:

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

余额充值