推箱子

标签: 广度优先搜索 数组 矩阵 堆(优先队列)

难度: Hard

「推箱子」是一款风靡全球的益智小游戏,玩家需要将箱子推到仓库中的目标位置。

游戏地图用大小为 m x n 的网格 grid 表示,其中每个元素可以是墙、地板或者是箱子。

现在你将作为玩家参与游戏,按规则将箱子 'B' 移动到目标位置 'T'

  • 玩家用字符 'S' 表示,只要他在地板上,就可以在网格中向上、下、左、右四个方向移动。
  • 地板用字符 '.' 表示,意味着可以自由行走。
  • 墙用字符 '#' 表示,意味着障碍物,不能通行。 
  • 箱子仅有一个,用字符 'B' 表示。相应地,网格上有一个目标位置 'T'
  • 玩家需要站在箱子旁边,然后沿着箱子的方向进行移动,此时箱子会被移动到相邻的地板单元格。记作一次「推动」。
  • 玩家无法越过箱子。

返回将箱子推到目标位置的最小 推动 次数,如果无法做到,请返回 -1

示例 1:

输入:grid = [["#","#","#","#","#","#"],
             ["#","T","#","#","#","#"],
             ["#",".",".","B",".","#"],
             ["#",".","#","#",".","#"],
             ["#",".",".",".","S","#"],
             ["#","#","#","#","#","#"]]
输出:3
解释:我们只需要返回推箱子的次数。

示例 2:

输入:grid = [["#","#","#","#","#","#"],
             ["#","T","#","#","#","#"],
             ["#",".",".","B",".","#"],
             ["#","#","#","#",".","#"],
             ["#",".",".",".","S","#"],
             ["#","#","#","#","#","#"]]
输出:-1

示例 3:

输入:grid = [["#","#","#","#","#","#"],
             ["#","T",".",".","#","#"],
             ["#",".","#","B",".","#"],
             ["#",".",".",".",".","#"],
             ["#",".",".",".","S","#"],
             ["#","#","#","#","#","#"]]
输出:5
解释:向下、向左、向左、向上再向上。

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 20
  • grid 仅包含字符 '.', '#''S' , 'T', 以及 'B'
  • grid 中 'S', 'B' 和 'T' 各只能出现一个。

Submission

运行时间: 50 ms

内存: 16.4 MB

class Solution:

    def minPushBox(self, grid: List[List[str]]) -> int:
        rows, cols = len(grid), len(grid[0])
        dirs = ((1, 0, -1, 0), (-1, 0, 1, 0), (0, -1, 0, 1), (0, 1, 0, -1))

        # 检查人能否到达指定位置
        def check(start, target, bpos):
            # 已经在了
            if start == target: return True 
            tr, tc = target
            # 目标出界或在墙里
            if (not (rows > tr >= 0 <= tc < cols)) or grid[tr][tc] == '#': return False
            # BFS判断
            q = deque([start])
            vis = {start, bpos}
            while q:
                r, c = q.popleft()
                for dr, dc, _, _ in dirs:
                    nr, nc = r + dr, c + dc
                    if rows > nr >= 0 <= nc < cols and (nr, nc) not in vis and grid[nr][nc] != '#':
                        if (nr, nc) == target:
                            return True
                        vis.add((nr, nc))
                        q.append((nr, nc))
            return False

        for r in range(rows):
            for c in range(cols):
                if grid[r][c] == 'S':
                    s = (r, c)
                elif grid[r][c] == 'B':
                    b = (r, c)
                elif grid[r][c] == 'T':
                    t = (r, c)

        q = deque([(*b, *s)])
        vis = {(*b, *s)}  # !状态是 箱子位置和方向(人的位置)
        step = 0
        while q:
            l = len(q)
            for _ in range(l):
                r, c, sr, sc = q.popleft()
                for dr, dc, dr2, dc2 in dirs:
                    nr, nc = r + dr, c + dc  # 箱子目标位置
                    nsr, nsc = r + dr2, c + dc2  # 人的目标位置
                    if rows > nr >= 0 <= nc < cols and (nr, nc, nsr, nsc) not in vis and grid[nr][nc] != '#' and check((sr, sc), (nsr, nsc), (r, c)):
                        if (nr, nc) == t:
                            return step + 1
                        vis.add((nr, nc, nsr, nsc))
                        q.append((nr, nc, r, c))
            step += 1
        return -1

Explain

This solution uses a Breadth-First Search (BFS) approach to simulate the movement of both the player and the box in a grid-based puzzle game. The algorithm involves checking every possible movement of the box and corresponding positioning of the player necessary to push the box. For every box move, the player's ability to reach the position needed to make that push is verified through another BFS that takes into account the current box's position as an obstacle.

时间复杂度: O((m*n)^3)

空间复杂度: O((m*n)^2)

class Solution:

    def minPushBox(self, grid: List[List[str]]) -> int:
        rows, cols = len(grid), len(grid[0])
        dirs = ((1, 0, -1, 0), (-1, 0, 1, 0), (0, -1, 0, 1), (0, 1, 0, -1))

        def check(start, target, bpos):
            if start == target: return True 
            tr, tc = target
            if not (rows > tr >= 0 <= tc < cols) or grid[tr][tc] == '#': return False
            q = deque([start])
            vis = {start, bpos}
            while q:
                r, c = q.popleft()
                for dr, dc, _, _ in dirs:
                    nr, nc = r + dr, c + dc
                    if rows > nr >= 0 <= nc < cols and (nr, nc) not in vis and grid[nr][nc] != '#':
                        if (nr, nc) == target:
                            return True
                        vis.add((nr, nc))
                        q.append((nr, nc))
            return False

        for r in range(rows):
            for c in range(cols):
                if grid[r][c] == 'S':
                    s = (r, c)
                elif grid[r][c] == 'B':
                    b = (r, c)
                elif grid[r][c] == 'T':
                    t = (r, c)

        q = deque([(*b, *s)])
        vis = {(*b, *s)}
        step = 0
        while q:
            l = len(q)
            for _ in range(l):
                r, c, sr, sc = q.popleft()
                for dr, dc, dr2, dc2 in dirs:
                    nr, nc = r + dr, c + dc
                    nsr, nsc = r + dr2, c + dc2
                    if rows > nr >= 0 <= nc < cols and (nr, nc, nsr, nsc) not in vis and grid[nr][nc] != '#' and check((sr, sc), (nsr, nsc), (r, c)):
                        if (nr, nc) == t:
                            return step + 1
                        vis.add((nr, nc, nsr, nsc))
                        q.append((nr, nc, r, c))
            step += 1
        return -1

Explore

在这个问题中,游戏的状态可以通过箱子和玩家的位置来定义。使用队列支持广度优先搜索(BFS)可以确保我们以最短的步数找到解决方案,因为BFS会层层推进,首先探索所有可能的一步操作,然后是两步操作,依此类推。这样可以保证一旦我们找到目标位置(箱子被推到目标点),那么这肯定是最小的推动次数。而访问集合则用来记录已经探索过的状态,避免重复处理相同的状态,这样可以大幅提高搜索效率并减少不必要的计算。

函数 `check` 用于确认玩家能否到达指定位置以推动箱子。此函数会返回 `False` 的情况包括:1) 目标位置已经超出边界或是障碍物(即格子中为'#');2) 起始位置和目标位置虽然有效,但玩家无法通过未被阻挡的路径到达目标位置,因为所有可能的路径都被障碍物或箱子阻挡。

如果玩家和箱子的初始位置使得玩家无法开始移动箱子,算法将不会找到任何可行的解决方案。在这种情况下,广度优先搜索(BFS)队列最终将变为空,因为没有更多的合法移动可以执行。一旦队列为空,算法会结束搜索,并返回 `-1`,表示没有找到将箱子推到目标位置的方法。

选择 BFS 而不是 DFS 或其他搜索算法的主要原因是 BFS 提供了在此类问题中找到最短路径的能力。在推箱子的游戏中,我们需要确保玩家可以用最少的步数到达指定位置来推动箱子。BFS 从起点开始,按层次扩展搜索区域,确保一旦找到目标,那么找到的路径就是最短的。而 DFS 则可能会深入探索某条路径,但不一定能保证找到的是最短路径。此外,BFS 在处理此类网格移动问题时通常更直观和容易管理。