• LeetCode LCP 13. 寻宝 | Python


    LCP 13. 寻宝


    题目来源:力扣(LeetCode)https://leetcode-cn.com/problems/xun-bao

    题目


    我们得到了一副藏宝图,藏宝图显示,在一个迷宫中存在着未被世人发现的宝藏。

    迷宫是一个二维矩阵,用一个字符串数组表示。它标识了唯一的入口(用 'S' 表示),和唯一的宝藏地点(用 'T' 表示)。但是,宝藏被一些隐蔽的机关保护了起来。在地图上有若干个机关点(用 'M' 表示),只有所有机关均被触发,才可以拿到宝藏。

    要保持机关的触发,需要把一个重石放在上面。迷宫中有若干个石堆(用 'O' 表示),每个石堆都有无限个足够触发机关的重石。但是由于石头太重,我们一次只能搬一个石头到指定地点。

    迷宫中同样有一些墙壁(用 '#' 表示),我们不能走入墙壁。剩余的都是可随意通行的点(用 '.' 表示)。石堆、机关、起点和终点(无论是否能拿到宝藏)也是可以通行的。

    我们每步可以选择向上/向下/向左/向右移动一格,并且不能移出迷宫。搬起石头和放下石头不算步数。那么,从起点开始,我们最少需要多少步才能最后拿到宝藏呢?如果无法拿到宝藏,返回 -1 。

    示例 1:

    输入: ["S#O", "M..", "M.T"]
    
    输出:16
    

    解释: 最优路线为: S->O, cost = 4, 去搬石头 O->第二行的M, cost = 3, M机关触发 第二行的M->O, cost = 3, 我们需要继续回去 O 搬石头。 O->第三行的M, cost = 4, 此时所有机关均触发 第三行的M->T, cost = 2,去T点拿宝藏。 总步数为16。

    示例图

    示例 2:

    输入: ["S#O", "M.#", "M.T"]
    
    输出:-1
    
    解释:我们无法搬到石头触发机关
    

    示例 3:

    输入: ["S#O", "M.T", "M.."]
    
    输出:17
    
    解释:注意终点也是可以通行的。
    

    限制:

    • 1 <= maze.length <= 100
    • 1 <= maze[i].length <= 100
    • maze[i].length == maze[j].length
    • S 和 T 有且只有一个
    • 0 <= M的数量 <= 16
    • 0 <= O的数量 <= 40,题目保证当迷宫中存在 M 时,一定存在至少一个 O 。

    解题思路


    思路:状态压缩动态规划

    先大致说下题意,题目中要求我们在迷宫中,从起点 S 走到终点 T。但是,迷宫中有一些规则以及限制:

    • '#':表示墙壁,
    • '.':表示可通行
    • 'M':表示机关点,需要重石触发
    • 'O':表示石堆,一次移动只能搬动一个石头。石堆中的重石无限。

    其中,要到达终点 T 需要将所有机关点 M 都被触发,求最少步数到达终点。

    题目中说明,人物可进行四个方位的移动。根据上面的题意简介,那么可能出现的情况如下(字符含义见上):

    • 从起点 S 到达 O 因为无法直接从 SM 因为触发机关 M 需要重石;
    • OM 搬重石触发机关
    • M 再到 O 继续搬重石(假设后面还有机关)
    • M 到终点 T 当所有机关都触发,可以到终点。

    题目要我们求最少步数到达终点,也就是说,我们可以考虑将上面可能出现的情况,将两个点之间的最短距离计算出来用以后续计算。

    在这里,首先要先解决的是采取怎样的路线去取重石和触发机关。这里先进行大致的分析:

    先看上面罗列的可能情况,开始一定是从 S 出发,经过某个 O,取得重石,然后到达某个 M,触发机关。在这里,对于某个特定的 M 来说,一定存在某个 O,使得 S 经过 O 到达 M 的距离最短。也就说,固定 M,枚举 O,就可以求得 S 到达每个 M 的最短距离。

    机关点 M,有可能存在不止一个。那么到达某个 M 之后,还需要再次去某个 O 点取得重石再去其他 M 点触发机关,直至所有机关都被触发。在这里,假设先到达 M1,要取重石触发机关 M2,那么同样,可以枚举 O,求得 M1 到达 M2 的最短距离。

    当所有的机关都触发了,那么剩下的就是从 M 到达 T 的距离,这样同样可以计算出来。

    上面是大致的分析,计算逐个情况下的最短距离,进而求得整体的最短距离。但是这里有个需要注意的地方,因为需要所有的机关点 M 都被触发,那么触发 M 的顺序不同时,那么将会导致结果不同。

    根据前面的分析,在给定的迷宫 maze 中,无论是否触发机关,每个位置到其他任意位置的距离是不会改变的。那么用 BFS 的思路计算点到点之间的距离。

    定义二维数组,保存机关 Mi 到 Mj 的最短距离,这里枚举所有的石头堆 O 进行计算。

    定义 dp,利用二进制形式的 mask 表示状态,dp[mask][i] 表示当前处于第 i 个 M 的位置,当前的状态为 mask 的最短距离。机关的状态有两种:已触发和未触发。

    当所有的机关都触发,那么从最后被触发的机关点出发,前往终点,比较得出最短距离。

    具体实现代码如下(含注释)。

    代码实现


    from typing import List
    
    class Solution:
        def minimalSteps(self, maze: List[str]) -> int:
            # 四个方位
            directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]
            m = len(maze)
            n = len(maze[0])
    
            # 起点,终点
            start_x, start_y, end_x, end_y = -1, -1, -1, -1
    
            # 机关,石堆
            buttons = []
            stones = []
    
            # 先将迷宫中字符所代表的意义进行标记
            for i in range(m):
                for j in range(n):
                    if maze[i][j] == 'M':
                        buttons.append((i, j))
                    elif maze[i][j] == 'O':
                        stones.append((i, j))
                    elif maze[i][j] == 'S':
                        start_x = i
                        start_y = j
                    elif maze[i][j] == 'T':
                        end_x = i
                        end_y = j
    
            # 机关以及石堆数
            num_b = len(buttons)
            num_s = len(stones)
    
            # 计算出发点到其他所有位置的距离
            start_to_any_pos = self.bfs(start_x, start_y, maze, m, n, directions)
    
            # 如果没有石堆,那么就是直接找出出发点到终点的最短距离
            if num_b == 0:
                return start_to_any_pos[end_x][end_y]
    
            # 定义数组,记录其中一个机关到另外一个机关以及到起点,终点的距离
            # dist[i][num_b] 表示到起点的距离,dist[i][num_b+1] 表示到终点的距离
            dist = [[-1] * (num_b + 2) for _ in range(num_b)]
    
            # 迷宫中,机关 M 可能有多个。先计算触发机关后,每个 M 到达终点的距离
            # im 记录机关到其他点的距离
            button_to_any_pos = []
            for i in range(num_b):
                b_x, b_y = buttons[i]
                # 计算第 i 个机关 M 到其他点的距离,存入 button_to_any_pos 中
                button_to_any_pos.append(self.bfs(b_x, b_y, maze, m, n, directions))
                # 机关到终点的距离
                dist[i][num_b+1] = button_to_any_pos[i][end_x][end_y]
    
            # 计算出发点到每个机关的距离
            for i in range(num_b):
                # S 到 M 的距离
                start_to_m = -1
                for j in range(num_s):
                    s_x, s_y = stones[j]
                    # 固定某个机关,计算出发点 S 经过每个石堆到达机关的距离
                    # 也可以理解为机关到起始点 S 的距离,由变量 button_to_any_pos 存储
                    if button_to_any_pos[i][s_x][s_y] != -1 and start_to_any_pos[s_x][s_y] != -1:
                        # 计算距离,比较取小值
                        tmp = button_to_any_pos[i][s_x][s_y] + start_to_any_pos[s_x][s_y]
                        if start_to_m == -1 or start_to_m > tmp:
                            start_to_m = tmp
    
                # 将此时 S 到 M 的最短距离存入 dist 数组中
                dist[i][num_b] = start_to_m
    
                # 这里计算第 i 个机关到第 j 个机关的距离
                # 例如:M1 经过 O 到达 M2的最短距离
                for j in range(i+1, num_b):
                    m_to_another_m = -1
                    for k in range(num_s):
                        s_x, s_y = stones[k]
                        if button_to_any_pos[i][s_x][s_y] != -1 and button_to_any_pos[j][s_x][s_y] != -1:
                            tmp = button_to_any_pos[i][s_x][s_y] + button_to_any_pos[j][s_x][s_y]
                            if m_to_another_m == -1 or m_to_another_m > tmp:
                                m_to_another_m = tmp
    
                    # 因为距离是无向图,那么两点之间的距离是对称的
                    dist[i][j] = m_to_another_m
                    dist[j][i] = m_to_another_m
            
            # 如果一个机关,从出发点无法到达,或者从机关点出发无法到终点,说明无路径,返回 -1
            for i in range(num_b):
                if dist[i][num_b] == -1 or  dist[i][num_b+1] == -1:
                    return -1
            
            # 定义 dp 数组,-1 表示为被遍历的状态
            # 假设有 n 个 M,机关的状态有 2^n 个状态
            # dp[mask][i] 表示当前处于第 i 个 M 的位置,当前的状态为 mask 的最短距离
            dp = [[-1] * num_b for _ in range(1<<num_b)]
            # 用二进制的形式表示 mask
            # 初始化,从 S 到第 i 个机关,此时 mask 的第 i 位为 1,其他为 0
            # 将前面计算的值赋值给 dp 数组
            for i in range(num_b):
                dp[1<<i][i]=dist[i][num_b]
            
            # 开始遍历,计算 mask 不同状态,每个机关点 i 的值
            # mask 索引从 1 开始
            for mask in range(1, (1<<num_b)):
                for i in range(num_b):
                    # 如果当前 dp 合法
                    if mask & (1<<i) != 0:
                        for j in range(num_b):
                            # 选择下一个机关 j,要 j 还未触发,此时 mask 的 j 位置应该为 0
                            if mask & (1<<j) == 0:
                                next_mask = mask | (1<<j)
                                tmp = dp[mask][i] + dist[i][j]
                                if dp[next_mask][j] == -1 or dp[next_mask][j] > tmp:
                                    dp[next_mask][j] = tmp
    
            # 这里计算全部触发后,最后一个机关到达终点的距离,取小值
            ans = -1
            final_mask = (1<<num_b) - 1
            for i in range(num_b):
                tmp = dp[final_mask][i] + dist[i][num_b+1]
                if ans == -1 or ans > tmp:
                    ans = tmp
            
            return ans
    
    
        def bfs(self, x, y, maze, m, n, directions):
            """计算点(x, y) 到其他点之间的距离
            """
            res = [[-1] * n for _ in range(m)]
            # 设点 (x, y) 为起点
            res[x][y] = 0
            
            from collections import deque
            queue = deque()
            queue.append([x, y])
    
            while queue:
                # 出队,计算点到点之间的距离
                cur_x, cur_y = queue.popleft()
                # 限制边界,往四个方位移动,遇到可通行且未被访问,则进行移动,记录距离
                for dx, dy in directions:
                    nx = cur_x + dx
                    ny = cur_y + dy
                    if 0 <= nx < m and 0 <= ny < n and maze[nx][ny] != '#' and res[nx][ny] == -1:
                        res[nx][ny] = res[cur_x][cur_y] + 1
                        queue.append([nx, ny])
            return res
    
    
    maze = ["S#O", "M..", "M.T"]
    solution = Solution()
    solution.minimalSteps(maze)
    

    实现结果


    实现结果

    欢迎关注


    公众号 【书所集录

  • 相关阅读:
    HangFire快速入门
    HangFire概述
    Lodop错误汇总
    微信支付过程遇到的问题
    动态规划算法
    几句话~
    Welcom To My Blog
    【技巧】关于素数
    安徽省小学组省赛2014年第一题 木板面积(C++)
    洛谷 P1425 小鱼的游泳时间
  • 原文地址:https://www.cnblogs.com/yiluolion/p/13399457.html
Copyright © 2020-2023  润新知