SWE / MLE · AI-enabled Coding

Meta AI 迷宫题:修复打印、BFS 与带钥匙的单向门

从路径显示与重复遍历修复开始,解释单向地形、钥匙状态、前驱重建和最短路径的正确性。

题目重点:从路径显示与重复遍历修复开始,解释单向地形、钥匙状态、前驱重建和最短路径的正确性。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 示例采用矩形四邻网格、每步成本一;#墙、.空地、a–z永久可复用钥匙、A–Z对应锁。起终点用坐标传入,避免显示标记e/E与钥匙锁冲突。
  • 箭头训练规则为必须沿箭头方向进入和离开;不自动跨墙、不自动滑行。禁止左右移动的地形、能穿墙的传送带属于不同变体,必须重新给出转移函数,不能套用此规则。
  • 返回任意一条最短可行坐标路径;无路返回None。只在显示层将路径空地变*,保留特殊地形并显示入口/出口;未提供的工程类名和接口不补造。
  • 普通0通路/1墙变体用solve_binary_maze,明确4邻接、返回任意最短坐标path,属于无钥匙无门的子集。允许8邻接的是独立训练合同,不从题目名字猜方向。

解题路线

  1. 先最小化失败用例:打印器核对path是否含入口出口,避免先覆盖再判断类型;BFS在入队时标记访问,每个状态只排队一次,不能等出队再允许无限重复入队。
  2. 访问状态为(row,col,keyMask)。同一个位置携带不同钥匙可以拥有不同后续路径。移动前检查边界、墙、当前格出方向、目标格入方向和锁权限;进入钥匙格后再更新位掩码。
  3. 每条合法转移成本一,BFS第一次取出目标即最短。parent保存完整前驱状态,终点沿链回溯再反转;每个队列元素只保存状态,避免复制整条路径。
  4. 改动前先读Maze、Cell、Printer、Solver的实际调用关系,再让AI处理明确的局部修改;用diff检查visited键、方向和标记,逐阶段回跑测试。AI输出不能取代状态不变量和用例解释。
  5. solve_binary_maze只做0→地板、1→墙的入口适配,复用同一BFS和前驱;无钥匙时mask始终0,因此退化到坐标visited。普通迷宫无需缓存每条完整候选路径。

手动走一遍

网格["..A.",".##.","a..."],起点(0,0)、终点(0,3)。先向下取得a,再回到上排穿过A,共7步;仅以坐标去重会在回到起点时误剪枝。显示层["...."]的一条直线路径为e**E,含箭头的路径e>E保留箭头。

Python 参考实现

下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。

代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。

from collections import deque

DIRECTIONS = ((-1, 0), (0, 1), (1, 0), (0, -1))
ARROWS = {'^': 0, '>': 1, '↓': 2, '<': 3}


def solve_maze(board, start, goal):
    """Training contract: # wall, . floor, lowercase key, uppercase lock.

    Down arrow is ↓, leaving the letter v available as a key.
    Arrows require entering and leaving in their indicated movement direction.
    Keys persist, may be reused, and do not remove corresponding locks.
    Board uses coordinates for endpoints, avoiding E/e marker-key ambiguity.
    Returns a shortest coordinate path or None. Input is not modified.
    """
    if not board or not board[0]:
        raise ValueError('nonempty rectangular board required')
    h, w = len(board), len(board[0])
    if any(len(row) != w for row in board):
        raise ValueError('ragged board')
    for p in (start, goal):
        if len(p) != 2 or not (0 <= p[0] < h and 0 <= p[1] < w):
            raise ValueError('endpoint outside board')
    allowed = set('#.^>↓<abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
    if any(c not in allowed for row in board for c in row):
        raise ValueError('unknown terrain')
    if board[start[0]][start[1]] == '#' or board[goal[0]][goal[1]] == '#':
        return None
    first_cell = board[start[0]][start[1]]
    if first_cell.isupper():  # Initially no keys: a locked start is inaccessible.
        return None
    initial_keys = 1 << (ord(first_cell) - ord('a')) if first_cell.islower() else 0
    first = (*start, initial_keys)
    parent = {first: None}
    queue = deque([first])
    while queue:
        current = queue.popleft()
        r, c, keys = current
        if (r, c) == goal:
            path = []
            while current is not None:
                path.append(current[:2])
                current = parent[current]
            return path[::-1]
        cell = board[r][c]
        for direction, (dr, dc) in enumerate(DIRECTIONS):
            if cell in ARROWS and ARROWS[cell] != direction:
                continue
            nr, nc = r + dr, c + dc
            if not (0 <= nr < h and 0 <= nc < w):
                continue
            target = board[nr][nc]
            if target == '#' or (target in ARROWS and ARROWS[target] != direction):
                continue
            if target.isupper() and not (keys & (1 << (ord(target.lower()) - ord('a')))):
                continue
            next_keys = keys | (1 << (ord(target) - ord('a'))) if target.islower() else keys
            state = (nr, nc, next_keys)
            if state not in parent:
                parent[state] = (r, c, keys)
                queue.append(state)
    return None


def render_path(board, path, start_marker='e', goal_marker='E'):
    """Separate display contract; preserve non-floor tiles and endpoint markers."""
    result = [list(row) for row in board]
    for r, c in path or ():
        if result[r][c] == '.':
            result[r][c] = '*'
    # Marker letters are a display option, not solver keys/locks.
    if path:
        r, c = path[0]; result[r][c] = start_marker
        r, c = path[-1]; result[r][c] = goal_marker
    return [''.join(row) for row in result]


def solve_binary_maze(grid,start,goal):
    """Plain 0/1 four-neighbor variant, rectangular; no keys or directed terrain."""
    if not grid or not grid[0]:return None
    width=len(grid[0])
    if any(len(row)!=width or any(value not in (0,1) for value in row) for row in grid):raise ValueError('binary rectangle')
    board=[''.join('#' if value else '.' for value in row) for row in grid]
    return solve_maze(board,start,goal)

复杂度

有V=H×W格、K种实际钥匙,状态至多V×2^K,四邻转移下时间和parent/visited空间均O(V2^K),路径重建O(L)。无钥匙退化O(V)。当前实现按字母位编码,最坏K=26;不宣称任意大型钥匙迷宫都高效。

易错点与追问

容易失分的边界

  • 把visited写成坐标会遗漏拿钥匙后的新状态;以整条路径为状态则膨胀,必须只保存足以决定未来的状态。
  • BFS解决单位步长,不适用于不同地形成本;改为0–1 BFS或Dijkstra。自动传送还要核对落点、环与代价。
  • 入口出口显示字母不能直接当普通key/lock;示例将两层分开。遇到墙后的单向格不能凭方向允许穿墙。
  • 向下箭头代码用↓,字母v仍可作钥匙;用v兼作箭头和钥匙会错误赋钥匙状态。

继续深挖

  • 同坐标的钥匙集合A包含B且到达距离不大于B,可做支配剪枝;实现要保存距离和集合,不能简单只保留最新mask。
  • 若只通过机器人move/turn接口探索未知地图,如何恢复位置与朝向?该题是实体探索,不等于直接访问完整网格的BFS。
  • 大型工程中如何确认错误是printer、状态读取还是solver?先复现最小失败测试,限制修复范围,再做端到端回归。
✓已复制微信号!