青训营X豆包MarsCode 技术训练营第六课 | 豆包MarsCode AI 刷题

71 阅读12分钟

题目解析

  • 思路

    • 首先,明确这是一个求最优路径相关的问题,需要找到从起点(数组 airports 的第一个元素对应的机场)到终点(数组 airports 的最后一个元素对应的机场)的最少起飞次数,也就是最小步数,很适合用广度优先搜索(BFS)算法来解决。

    • 初始化一些必要的数据结构和变量。创建一个队列 queue(这里使用 LinkedList 实现)用于 BFS 过程中存储待访问的机场索引,一个布尔数组 visited 用于标记各个机场是否已经被访问过,避免重复访问,还创建了一个 HashMap 类型的 airlineMap,用于记录每个航空公司所管理的机场索引列表,方便后续查找同属一家航空公司的其他机场。

    • 通过循环遍历 airports 数组,将每个航空公司对应的机场索引添加到 airlineMap 中,构建起航空公司与旗下机场的对应关系。

    • 接着将起点机场索引 0 加入队列 queue,并标记起点已访问 visited[0] = true,同时初始化起飞次数(步数)计数器 steps 为 0

    • 进入 BFS 的主循环,只要队列 queue 不为空,就进行一轮搜索。在每一轮中,先获取当前队列的大小 size,这代表当前层的节点数量(在本题中就是同一轮可到达的机场数量),然后通过循环遍历这一层的节点(即当前可到达的各个机场):

      • 取出队列头部的机场索引 current,代表当前所在机场。
      • 首先判断是否到达终点(current == n - 1n 是 airports 数组长度),如果到达则直接返回当前的起飞次数 steps
      • 接着考虑相邻机场,如果当前机场索引减 1 后合法(不越界且未被访问过),则将其标记为已访问,并加入队列,同理对于当前机场索引加 1 后的机场也做相同操作,这样就可以探索相邻机场这条飞行规则。
      • 再查找与当前机场同属一家航空公司的其他机场,通过 airlineMap 获取同航空公司的机场索引列表 sameAirlineAirports,遍历这个列表,如果某个同航空公司的机场未被访问过,就将其标记为已访问并加入队列,同时为了避免后续重复访问这些同航空公司的机场(因为同属一家航空公司的机场之间是相互可达的,一次访问即可),将当前航空公司的记录从 airlineMap 中移除。
    • 每完成一层(一轮)的搜索,也就是遍历完当前队列中的所有机场后,起飞次数 steps 加 1,代表需要多一次起飞来进入下一轮探索的机场。如果最终循环结束还未到达终点,说明无法到达,返回 -1(理论上按照题目的设定应该能到达,这里只是做一个完整性的返回处理)。

  • 图解(以 airports = [10, 12, 13, 12, 14] 为例)

    1. 初始化阶段

      • airports 数组长度 n = 5,创建 queuevisited 和 airlineMap
      • 遍历 airports 数组构建 airlineMap,结果为 {10=[0], 12=[1, 3], 13=[2], 14=[4]},表示航空公司 10 管理机场 0,航空公司 12 管理机场 1 和 3 等等。
      • 将起点机场索引 0 加入 queue,标记 visited[0] = truesteps = 0
    2. 第一轮 BFS(steps = 0

      • 当前队列 queue 中有元素 0(代表机场索引),队列大小 size = 1

      • 取出 0,判断未到终点。

      • 查看相邻机场:

        • 索引 0 - 1 = -1 不合法(越界),不做操作。
        • 索引 0 + 1 = 1 合法且未被访问,标记 visited[1] = true,将 1 加入 queue
      • 查看同航空公司机场:航空公司 10 只有机场 0,无其他同公司机场可探索。

      • 第一轮结束,steps 加 1,变为 1

    3. 第二轮 BFS(steps = 1

      • 当前队列 queue 中有元素 1,队列大小 size = 1

      • 取出 1,判断未到终点。

      • 查看相邻机场:

        • 索引 1 - 1 = 0 已访问,不做操作。
        • 索引 1 + 1 = 2 合法且未被访问,标记 visited[2] = true,将 2 加入 queue
      • 查看同航空公司机场:航空公司 12 还有机场 3 未访问,标记 visited[3] = true,将 3 加入 queue,然后移除航空公司 12 在 airlineMap 中的记录(防止重复访问)。

      • 第二轮结束,steps 加 1,变为 2

    4. 第三轮 BFS(steps = 2

      • 当前队列 queue 中有元素 2 和 3,队列大小 size = 2

      • 先取出 2,判断未到终点。

        • 查看相邻机场:

          • 索引 2 - 1 = 1 已访问,不做操作。
          • 索引 2 + 1 = 3 已在队列中,不做操作。
        • 查看同航空公司机场:航空公司 13 只有机场 2,无其他同公司机场可探索。

      • 再取出 3,判断未到终点。

        • 查看相邻机场:

          • 索引 3 - 1 = 2 已访问,不做操作。
          • 索引 3 + 1 = 4 合法且未被访问,标记 visited[4] = true,将 4 加入 queue
        • 查看同航空公司机场:航空公司 12 已被移除记录,无操作。

      • 第三轮结束,steps 加 1,变为 3

    5. 第四轮 BFS(steps = 3

      • 当前队列 queue 中有元素 4,队列大小 size = 1
      • 取出 4,此时 4 为终点(n - 1 = 4),直接返回 steps = 3,代表最少起飞次数为 3
  • 代码详解

收起

java

复制

import java.util.*;

public class Main {
    public static int solution(int[] airports) {
        int n = airports.length;
        if (n == 1) return 0; // 只有一个机场,起飞次数为0

        // 使用队列进行广度优先搜索
        Queue<Integer> queue = new LinkedList<>();
        boolean[] visited = new boolean[n];
        Map<Integer, List<Integer>> airlineMap = new HashMap<>();

        // 记录相同航空公司机场的索引
        for (int i = 0; i < n; i++) {
            airlineMap.putIfAbsent(airports[i], new ArrayList<>());
            airlineMap.get(airports[i]).add(i);
        }

        // 初始条件
        queue.offer(0);
        visited[0] = true;
        int steps = 0;

        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                int current = queue.poll();

                // 如果到达终点
                if (current == n - 1) {
                    return steps;
                }

                // 相邻机场
                if (current - 1 >= 0 &&!visited[current - 1]) {
                    visited[current - 1] = true;
                    queue.offer(current - 1);
                }
                if (current + 1 < n &&!visited[current + 1]) {
                    visited[current + 1] = true;
                    queue.offer(current + 1);
                }

                // 相同航空公司机场
                List<Integer> sameAirlineAirports = airlineMap.get(airports[current]);
                if (sameAirlineAirports!= null) {
                    for (int next : sameAirlineAirports) {
                        if (!visited[next]) {
                            visited[next] = true;
                            queue.offer(next);
                        }
                    }
                    // 清空相同航空公司的记录,防止重复访问
                    airlineMap.remove(airports[current]);
                }
            }
            steps++;
        }

        return -1; // 如果无法到达终点(理论上不应该发生)
    }

    public static void main(String[] args) {
        // 测试用例
        int[] airports1 = {10, 12, 13, 12, 14};
        System.out.println(solution(airports1)); // 应该输出3
    }
}
  • 函数开头部分

    • 获取输入的 airports 数组长度 n,先判断如果 n 等于 1,说明只有一个机场,无需起飞就能到达(其实就是已经在终点了),直接返回 0
  • 数据结构初始化部分

    • 创建一个 LinkedList 实现的队列 queue,用于 BFS 存储待访问的机场索引,创建布尔数组 visited 并初始化为 false,用于标记机场是否已访问,还创建了 HashMap 类型的 airlineMap,其键为航空公司编号,值为该航空公司管理的机场索引的 ArrayList
    • 通过 for 循环遍历 airports 数组,对于每个机场元素 airports[i],使用 putIfAbsent 方法确保在 airlineMap 中该航空公司对应的 ArrayList 已存在(如果不存在则创建一个新的空 ArrayList),然后将当前机场索引 i 添加到对应的 ArrayList 中,这样就构建好了每个航空公司旗下机场索引的映射关系。
  • BFS 准备阶段

    • 将起点机场索引 0 通过 queue.offer(0) 加入队列,表示从起点开始搜索,标记 visited[0] = true 表示起点已访问,初始化起飞次数(步数)计数器 steps 为 0
  • BFS 主循环部分

    • while (!queue.isEmpty()) 循环表示只要队列不为空,就持续进行 BFS 搜索。

    • 在每一轮循环开始时,获取当前队列的大小 size,它代表当前层(同一轮可到达的机场集合)的节点数量。

    • 内层 for 循环通过 i 从 0 到 size - 1 遍历当前层的节点,每次取出队列头部的机场索引 current = queue.poll()

    • 接着进行判断和操作:

      • if (current == n - 1) 判断当前机场索引是否为终点索引(n - 1),如果是,则直接返回当前的起飞次数 steps,表示找到了到达终点的最少起飞次数。

      • 处理相邻机场部分:

        • if (current - 1 >= 0 &&!visited[current - 1]) 判断当前机场索引减 1 后的机场是否合法(不越界)且未被访问过,如果满足条件,就标记 visited[current - 1] = true 表示已访问,并通过 queue.offer(current - 1) 将其加入队列,以便后续继续探索该机场能到达的其他机场。
        • 同理,if (current + 1 < n &&!visited[current + 1]) 对当前机场索引加 1 后的机场做相同的合法性、未访问判断以及相应的标记和入队操作。
      • 处理同航空公司机场部分:

        • 通过 airlineMap.get(airports[current]) 获取与当前机场 current 同属一家航空公司的其他机场索引列表 sameAirlineAirports,如果该列表不为 null(说明存在同航空公司的其他机场),则遍历这个列表:

          • 对于列表中的每个机场索引 next,通过 if (!visited[next]) 判断是否未被访问过,如果未被访问,就标记 visited[next] = true 表示已访问,并通过 queue.offer(next) 将其加入队列,这样就能继续探索从这个同航空公司机场出发能到达的其他机场。
          • 最后,通过 airlineMap.remove(airports[current]) 移除当前航空公司的记录,防止后续重复访问同属该航空公司的机场(因为同一家航空公司的机场之间是相互可达的,一次访问即可探索完所有相关机场)。
    • 在每一轮(外层 while 循环的一次完整循环)结束后,通过 steps++ 将起飞次数(步数)加 1,表示进入下一层(下一轮可到达的机场集合)需要多一次起飞操作。

  • 返回结果部分

    • 如果整个 BFS 循环结束后还未到达终点(也就是 while 循环正常结束,队列变为空了),则返回 -1,表示按照给定规则无法到达终点(理论上按照题目设定这种情况不应该出现,但这里做一个完整性的返回处理)。
  • 主函数 main 部分

    • 在 main 方法中,创建了一个测试用例数组 airports1,并调用 solution 方法传入该数组,将返回结果输出到控制台,用于简单验证 solution 方法在这个特定测试用例下是否能正确返回预期的最少起飞次数(这里预期输出为 3)。

知识总结

  • 新知识点梳理

    • 广度优先搜索(BFS)算法的应用场景和实现方式:本题通过 BFS 来解决在特定规则下求最少起飞次数(最短路径)的问题,了解到 BFS 适合用于求从起点到终点的最短步数、最小代价等场景,通过队列来存储待访问的节点,按照层次依次向外扩展搜索,保证先访问到的节点距离起点的步数是最短的。
    • HashMap 与 ArrayList 配合构建复杂映射关系:学会了使用 HashMap 来存储不同键(本题中的航空公司编号)对应的值(同航空公司的机场索引列表,用 ArrayList 存储),这种组合可以方便地处理一对多的映射情况,在需要根据某个标识查找相关的多个元素时非常实用,比如本题中根据航空公司查找其管理的所有机场。
    • 标记数组(布尔数组)在避免重复访问中的作用:认识到通过创建布尔数组来标记节点(机场)是否已被访问,能够有效地避免在搜索过程中重复访问同一个节点,防止陷入死循环,提高搜索效率,这是很多搜索类算法中常用的技巧。
  • 理解

    • BFS 算法就像是在一张地图上从起点开始一层一层地向外探索,每一层的节点都是距离起点相同步数能到达的地方,通过队列来有序地管理待探索的节点,保证了搜索的顺序性和最短性。而 HashMap 和 ArrayList 的组合则像是一个分类清晰的索引库,能够快速定位到某个类别(航空公司)下的多个具体元素(机场)。标记数组则是给已经探索过的 “地方” 做个记号,避免重复走冤枉路,它们共同配合帮助我们高效地解决这类路径搜索和遍历相关的问题。
  • 学习建议(针对入门同学)

    • 对于 BFS 算法,先从简单的图结构(比如用二维数组表示的方格地图等)入手学习其基本原理和代码实现框架,可以手动模拟算法执行过程,画一下每一轮搜索时队列的变化、节点的访问情况等,加深对层次搜索的理解。然后尝试做一些基础的 BFS 练习题,比如在简单迷宫中求从起点到终点的最短步数等题目,逐渐掌握如何根据具体题目设定来构建队列、处理节点访问和判断终止条件等操作。
    • 在学习 HashMap 和 ArrayList 配合使用时,先分别熟悉 HashMap 和 ArrayList 的基本操作(如 HashMap 的 putgetputIfAbsent 等方法,ArrayList 的 addgetremove 等方法),通过简单的代码示例去创建、操作这两种数据结构。然后再结合实际的一对多映射场景,像本题这样,练习如何将数据按照逻辑关系存入 HashMap 中,以及如何从 HashMap 中取出对应的 ArrayList 并进行遍历操作,多写一些类似的代码来巩固理解。
    • 对于标记数组的使用,要明白其目的是避免重复访问,在做练习题时,每次遇到需要遍历搜索的题目,都思考一下是否可能出现重复访问的情况,尝试主动运用标记数组来解决,通过不断实践来养成使用这个技巧的习惯,提高代码的正确性和效率。