一天一大 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
抛砖引玉
回想下之前做过的题目
- 有障碍的矩阵20200706: 不同路径 II (难度:中等)
- 四个方向行进的矩阵20200726: 矩阵中的最长递增路径 (难度:困难)
思路
先理下题目中步数的组成:
- 有障碍(墙#)的起点到终点
- 未知起点,终点
- 路径中存在指定点必须经过(机关 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 的组合是就比较复杂
昨天又刚好赶上加班,所以花了两天时间才把逻辑弄清楚
- POJ-----C Looooops
- POJ--Strange Way to Express Integers
- HDUOJ----More is better(并查集)
- HDUOJ 1099——Lottery
- HDUOJ-----取(m堆)石子游戏
- HDUOJ-----Be the Winner
- HDUOJ-------- Fibonacci again and again
- HDUOJ----Good Luck in CET-4 Everybody!
- 进制转换
- HDUOJ--畅通工程
- poj----Ubiquitous Religions
- POJ----The Suspects
- HDUOJ----剪花布条
- HDUOJ-----F(x)
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Apache Spark 2.0 在作业完成时却花费很长时间结束
- centos 6.9 升级glibc动态库的详细过程
- Ubuntu18.04(linux)安装MySQL的方法步骤
- ubuntu18.0.4安装mysql并解决ERROR 1698 (28000): Access denied for user ''root''@''localhost''
- 动态在线扩容root根分区大小的方法详解
- centos7使用rpm安装mysql5.7的教程图解
- 关于Linux命令行下的数学运算示例详解
- Apache访问日志的配置与使用
- linux启动和重启nginx方法
- 简单谈谈centos7中配置php
- Linux动态链接库的使用
- Linux下部署springboot项目的方法步骤
- Linux、CentOS下安装zip与unzip指令功能(服务器)
- Linux上查看用户创建日期的几种方法总结
- 详解基于Linux的LVM无缝磁盘水平扩容