一天一大 leet(寻宝)难度:困难-Day20200729

时间:2022-07-25
本文章向大家介绍一天一大 leet(寻宝)难度:困难-Day20200729,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

题目:

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

迷宫是一个二维矩阵,用一个字符串数组表示。

它标识了唯一的入口(用 'S' 表示),和唯一的宝藏地点(用 'T' 表示)。

但是,宝藏被一些隐蔽的机关保护了起来。

在地图上有若干个机关点(用 'M' 表示),只有所有机关均被触发,才可以拿到宝藏。

要保持机关的触发,需要把一个重石放在上面。

迷宫中有若干个石堆(用 'O' 表示),每个石堆都有无限个足够触发机关的重石。

但是由于石头太重,我们一次只能搬一个石头到指定地点。

迷宫中同样有一些墙壁(用 '#' 表示),我们不能走入墙壁。剩余的都是可随意通行的点(用 '.' 表示)。

石堆、机关、起点和终点(无论是否能拿到宝藏)也是可以通行的。

我们每步可以选择向上/向下/向左/向右移动一格,并且不能移出迷宫。

搬起石头和放下石头不算步数。

那么,从起点开始,我们最少需要多少步才能最后拿到宝藏呢?

如果无法拿到宝藏,返回 -1 。

示例:

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.示例2

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

输出:-1

解释:我们无法搬到石头触发机关

3.示例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

抛砖引玉

回想下之前做过的题目

思路

先理下题目中步数的组成:

  • 有障碍(墙#)的起点到终点
  • 未知起点,终点
  • 路径中存在指定点必须经过(机关 M,石块 O)

1. S->石块 O->机关 M

2. 机关 M_i->石块 O->机关 M_j

3. 机关 M ->T


1,3 路线中的步数,可以分解成已经做过的逻辑:

- 从 S 到 O 有障碍的路径(枚举所有 O)

- 从 O 到 M 有障碍的路径((枚举所有 O 及对应的所有 M)

- 从 M 到 T 有障碍的路径(枚举所有 M)

2 中M_i及M_j为不同点

- 那么不同的 M 点组合就会有不同的结果

- 没有不同组合下的最小路径

- 更加官方思路使用二进制数标识不同的 M 点组合,

- 1 表示已触发机关包含这个 M 点,

- 0 表示这个 M 点未触发


实现

- 找出起始点 S 和终点 T,M,O 坐标

- 记录 S 点到其他点的最小步数,无法到达的单元格填充-1 -> startDist[i][j]

- 记录第 i 个 M 点到任意 O 点再到第 j 个 M 点的最小步数,无法到达的单元格填充-1 -> dist[m_i][m_j]

设存在 nb 个 M 点则:

dist[m_i][nb] -> 记录该 M 点到起点最小步数

dist[m_i][nb+1] -> 记录该 M 点到 T 点最小步数

dist 二维数组,dist[m_i][m_j]:

- 第 i 个 M->O->第 j 个 M 的最小的步数 dist[m_i][m_j]

- S->O->第 i 个 M 最小步数 dist[m_i][nb]

- T<-O<-第 i 个 M 最小步数 dist[m_i][nb+1]

- 声明 dp 表示指定 m 点为第一个触发的 M 点是,起点到 M-O-M 组合最小步数和

- M-O-M 组合最小步数和

- 枚举不同的 M 点组合,使用二进制数标记 M 点位置,则对所有的 M 点上层循环,生产两次 m 点组合

重叠的二进制位取| 或,表示该位的 M 点已触发,

- 相邻 dp 之间关系:

- 增加一个 M 点,上一个组合的步数最小值+新增 M 点-O-M

dp[next][j] = dp[i][j]+dist[i][j]

- next :下一个组合通过增加 连接 M 点即修改二进制数中 1 的位置完成

- M-O-M 组合的最小步数及二进制数值只剩一个 0(与 T 连接)时为组合最小步数

枚举不同组合的最小步数+连接 T 的步数

返回最小的和及需要的结果

无法满足条件的情况:

- 起点无法到达终点

- 存在 M 点无法连接起点或者终点

/**
 * @param {string[]} maze
 * @return {number}
 */
var minimalSteps = function (maze) {
  let dirs = [
      [-1, 0], // 上方
      [1, 0], // 下方
      [0, -1], // 左侧
      [0, 1], // 右侧
    ],
    n = maze.length,
    m = maze[0] ? maze[0].length : 0,
    buttons = [], // 机关->M 坐标集
    stones = [], // 石头->O 坐标集
    start = [-1, -1],
    end = [-1, -1]


  // 查询起点、终点坐标
  // 记录机关、石头坐标
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < m; j++) {
      // 机关
      if (maze[i].charAt(j) == 'M') {
        buttons.push([i, j])
      }
      // 石头
      if (maze[i].charAt(j) == 'O') {
        stones.push([i, j])
      }
      // 开始
      if (maze[i].charAt(j) == 'S') {
        start = [i, j]
      }
      // 结束
      if (maze[i].charAt(j) == 'T') {
        end = [i, j]
      }
    }
  }


  let nb = buttons.length, // 机关数
    ns = stones.length, // 石头数
    // 记录起点到任一点的步数
    startDist = bfs(start[0], start[1], maze)


  // 边界情况:没有机关直接从起到走到终点及startDist中记录的start坐标到end坐标步数
  if (nb === 0) {
    let end_x = end[0],
      end_y = end[1]
    return startDist[end_x][end_y]
  }


  // 记录从每个机关M到终点步数
  let dist = Array(nb)
  for (let m_i = 0; m_i < nb; m_i++) {
    dist[m_i] = Array(nb + 2).fill(-1)
  }


  // 记录从每个机关M到其他点步数
  let dd = Array(nb)
  for (let i = 0; i < nb; i++) {
    let buttons_x = buttons[i][0],
      buttons_y = buttons[i][1],
      end_x = end[0],
      end_y = end[1],
      d = bfs(buttons_x, buttons_y, maze)
    // 第i个机关到其他点步数矩阵
    dd[i] = d
    // 第i个机关到终点的步数->存放到dist最后一列
    dist[i][nb + 1] = d[end_x][end_y]
  }
  // 遍历机关M与石头O-计算使用石头触发机关的步数
  for (let i = 0; i < nb; i++) {
    let tmp = -1
    // 包含起点到石头O部分,即起点为矩阵指定起点
    for (let k = 0; k < ns; k++) {
      // 触发一个M的步数:机关M到石头O的步数+起点到石头O的步数
      let s_x = stones[k][0],
        s_y = stones[k][1]
      // 步数为-1则说明此单元格不通
      if (dd[i][s_x][s_y] !== -1 && startDist[s_x][s_y] !== -1) {
        // 第i个M点到达O点,起点到达O点的步数之和的最小值
        let item_num = dd[i][s_x][s_y] + startDist[s_x][s_y]
        tmp = tmp !== -1 ? Math.min(tmp, item_num) : item_num
      }
    }
    // 从S->O->第i个M最小步数,存放到倒数第二列
    dist[i][nb] = tmp
    // 起点为已触发机关M
    for (let j = i + 1; j < nb; j++) {
      let tmp = -1
      for (let k = 0; k < ns; k++) {
        let s_x = stones[k][0],
          s_y = stones[k][1]
        if (dd[i][s_x][s_y] != -1 && dd[j][s_x][s_y] != -1) {
          // 第i个M->O->第j个M
          let item_num = dd[i][s_x][s_y] + dd[j][s_x][s_y]
          tmp = tmp !== -1 ? Math.min(tmp, item_num) : item_num
        }
      }
      dist[i][j] = tmp
      dist[j][i] = tmp
    }
  }
  // 有从机关M出发无法到达终点或起点则说明无法 不存在满足题意的路径
  for (let i = 0; i < nb; i++) {
    if (dist[i][nb] == -1 || dist[i][nb + 1] == -1) {
      return -1
    }
  }

  // 使用二进制为标识M点,组合不同的M-O-M路线求最小值
  let dp = Array(1 << nb)
  for (let i = 0; i < 1 << nb; i++) {
    dp[i] = Array(nb).fill(-1)
  }
  for (let i = 0; i < nb; i++) {
    // 第i个机关M作为第一个触发机关的步数
    dp[1 << i][i] = dist[i][nb]
  }
  // 由于更新的状态都比未更新的大,所以直接从小到大遍历即可
  // 遍历每个机关点M,查询个M点之间步数最小值
  for (let mask = 1; mask < 1 << nb; mask++) {
    for (let i = 0; i < nb; i++) {
      // 当前 mask 为标记的M
      if ((mask & (1 << i)) !== 0) {
        // 为指定M点mark  遍历其他M点 查询该点到其他M之间的步数
        for (let j = 0; j < nb; j++) {
          // 当前M点路线组合不包含已在组合中的M点
          if ((mask & (1 << j)) === 0) {
            let next = mask | (1 << j),
              item_num = dp[mask][i] + dist[i][j]
            dp[next][j] =
              dp[next][j] !== -1 ? Math.min(item_num, dp[next][j]) : item_num
          }
        }
      }
    }
  }


  let _result = -1,
    finalMask = (1 << nb) - 1
  for (let i = 0; i < nb; i++) {
    // 不同的M点在组合中最小的步数+M点到终点的最小步数
    let item_num = dp[finalMask][i] + dist[i][nb + 1]
    _result = _result !== -1 ? Math.min(_result, item_num) : item_num
  }


  // 参数-> 起点坐标,矩阵
  function bfs(x, y, maze) {
    let result = Array(n)
    for (let i = 0; i < n; i++) {
      result[i] = Array(m).fill(-1)
    }
    result[x][y] = 0
    let queue = [[x, y]]
    while (queue.length) {
      let p = queue.shift(),
        curx = p[0],
        cury = p[1]
      for (let k = 0; k < 4; k++) {
        let nx = curx + dirs[k][0],
          ny = cury + dirs[k][1]
        // 取出的点可走的路径->不越界非障碍物#
        if (
          inBound(nx, ny) &&
          maze[nx].charAt(ny) !== '#' &&
          result[nx][ny] === -1
        ) {
          result[nx][ny] = result[curx][cury] + 1
          queue.push([nx, ny])
        }
      }
    }
    // 返回 n*m的矩阵,填充起点为(x,y)是到达每个单元格的步数
    return result
  }


  // 判断给定坐标是否越界
  function inBound(x, y) {
    return x >= 0 && x < n && y >= 0 && y < m
  }

  return _result
}

终于做完了(ಥ_ಥ)

刷了两个月题了,刚感觉渐入佳境,遇到简单的题,呵~ 不过如此

还是遇到了教我做人的题呀~

---

这题其实还是有做过的题目的影子:

- 有障碍的矩阵步数统计

- 四个方向行进的矩阵路线

但是这道题复杂的地方就是行进的组合情况比较多:

1. S-O-M

2. M-O-M

3. M-T

M 和 O 的选不固定,任意切换是都会产生新的路径,所以在动态规划记录 M-O-M 的组合是就比较复杂

昨天又刚好赶上加班,所以花了两天时间才把逻辑弄清楚