题目来源于leetcode,解法和思路仅代表个人观点。传送门。
难度:困难
用时:08:00:00以上,构思大半天了
我们得到了一副藏宝图,藏宝图显示,在一个迷宫中存在着未被世人发现的宝藏。
迷宫是一个二维矩阵,用一个字符串数组表示。它标识了唯一的入口(用 ‘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 。
首先可以思考一下,最短路径是怎么样的?
仔细拆解每一个不步骤就可以发现,主要是以下5步
更进一步的分析
*特殊情况:不存在M,则S-T的最短距离为所求。
所以此题需要解决的问题就是,
两点距离的搜索+预处理数据+旅行商问题
两点间的距离需要用BFS,而不是DFS。这题DFS会超时。(我一开始只想到了dfs,是我太菜了)
对于结点的全遍历,二者性能相当。
对于求最短路径,BFS快,BFS从起点开始一圈圈往外扩,但凡见到终点即结束遍历,大概率不会把结点全遍历完。但是对于DFS,必须每条路径都走一遍,经过所有路径长度比较后,才敢确定最短的那条,所以,DFS一定要遍历完所有结点,时间当然长了。
可以用回溯(排列树)或者dp的方法解决。这里我用了dp的方法。
注:例如:i 为 0000 、 0001 、 0010 …二进制枚举一共2^4种可能。
d p [ i ] [ j ] = { s 2 m [ j ] , i = 2 k ( 0 < = k < m ) m i n 0 < = k < m & & k ! = j { d p [ i x o r 2 j ] [ k ] + m 2 m [ k ] [ j ] } , 0 < i < 2 m dp[i][j]=\begin{cases} s2m[j] &, & {i=2^k (0<=k
class Solution {
//最短路径是怎么样的?
/*
1. 起点到石堆S->O
2. 石堆到其中一个机关O->M
3. 机关到其中一个石堆M->O
3. 重复(2)(3)中的步骤,直到剩下最后一个M
5. 从最后一个M走到终点T。M->T
*/
//更进一步
/*
1. 对于给定的Mi,计算S-Oi-Mi的最短路径
2. 对于给定的Mj,计算Mi-Oi-Mj的最短路径
3. 对于给定的T,计算Mi-T的最短路径
*特殊情况,不存在M,则S-T的最短距离为所求。
4. 求旅行商问题。从S出发,经过所有的M,最后到T的旅行商问题
*/
char[][] maze;
class Point{
int x;
int y;
Point(){}
Point(int x,int y){
this.x = x;
this.y = y;
}
}
//主函数
public int minimalSteps(String[] maze) {
this.maze = new char[maze.length][maze[0].length()];
//一维数组转换成二维数组
for(int i=0;i<maze.length;i++){
for(int j=0;j<maze[0].length();j++){
this.maze[i][j] = maze[i].charAt(j);
}
}
//开始点
Point startPoint = new Point();
//结束点
Point targetPoint = new Point();
//机关点
List<Point> Ms = new ArrayList();
//石头点
List<Point> Os = new ArrayList();
//遍历所有点,记录需要的点
for(int i=0;i<this.maze.length;i++){
for(int j=0;j<this.maze[0].length;j++){
if(this.maze[i][j] == 'S'){
startPoint.x = i;
startPoint.y = j;
}
if(this.maze[i][j] == 'T'){
targetPoint.x = i;
targetPoint.y = j;
}
if(this.maze[i][j] == 'M'){
Ms.add(new Point(i,j));
}
if(this.maze[i][j] == 'O'){
Os.add(new Point(i,j));
}
}
}
//特殊情况处理
//如果没有机关,直接起点到终点
if(Ms.size() == 0){
int ansSP = bfs(startPoint,targetPoint);
if(ansSP>=10000){
ansSP = -1;
}
return ansSP;
}
//--------下面开始计算
//预处理一下,先计算好每个Mi到O的距离
int[][] m2o = new int[Ms.size()][Os.size()];
for(int i=0;i<m2o.length;i++) {
for(int j=0;j<m2o[0].length;j++) {
m2o[i][j] = bfs(Ms.get(i),Os.get(j));
}
}
//第一部分,对于给定的Mi,计算S-Oi-Mi的最短路径
int[] s2m = new int[Ms.size()];
//初始化s2m
for(int i=0;i<s2m.length;i++){
s2m[i] = 10000;
}
//对于每个M
for(int i=0;i<Ms.size();i++){
//遍历每个O
for(int j=0;j<Os.size();j++){
//取S->某个O->Mi的最短距离
s2m[i] = Math.min(s2m[i], bfs(startPoint,Os.get(j))+m2o[i][j]);
}
}
//第二部分,对于给定的Mj,计算Mi-Oi-Mj的最短路径
int[][] m2m = new int[Ms.size()][Ms.size()];
//初始化m2m
for(int i=0;i<m2m.length;i++){
for(int j=0;j<m2m[0].length;j++){
m2m[i][j] = 10000;
}
}
//对于每个Mi
for(int i=0;i<Ms.size();i++){
//到另外的Mj
for(int j=0;j<Ms.size();j++){
//遍历每个Ok
for(int k=0;k<Os.size();k++){
//取Mi->某个O->Mj的最短距离
m2m[i][j] = Math.min(m2m[i][j],m2o[i][k]+m2o[j][k]);
}
}
}
//第三部分,对于给定的T,计算Mi-T的最短路径
int[] m2t = new int[Ms.size()];
for(int i=0;i<Ms.size();i++){
//每个Mi到T的最短距离
m2t[i] = bfs(Ms.get(i),targetPoint);
}
//第四部分,从S出发,遍历每一个M,最后到T的旅行商问题
//dp或回溯(排列树)
/*
枚举2^n种可能性
在i的机关触发形式中,以最后一步到Mj的最短步数
m为机关M的个数
dp[i][j] = min(0<=k
int[][] dp = new int[(int)Math.pow(2,Ms.size())][Ms.size()];
for(int i=0;i<dp.length;i++){
for(int j=0;j<dp[0].length;j++){
dp[i][j] = 10000;
}
}
for(int i=0;i<dp.length;i++) {
if(i==0) {
continue;
}
for(int j=0;j<dp[0].length;j++) {
if(i==j) {
continue;
}
int jmask = (int) Math.pow(2, j);
if(jmask == i) {
dp[i][j] = s2m[j];
continue;
}
//j需要在i中(机关已经被触发)
if((jmask&i) == 0) {
continue;
}
//枚举每个机关
for(int k=0;k<Ms.size();k++) {
//需要去掉的机关和枚举的机关不得重复
if(j==k) {
continue;
}
//制作mask
int kmask = (int) Math.pow(2, k);
//在机关k在i中
if((kmask&i) !=0 ) {
//在i中去掉其中一个j,从k到j的最短步数
dp[i][j] = Math.min(dp[i][j],dp[i^jmask][k]+m2m[k][j]);
}
}
}
}
int ans = 10000;
for(int i=0;i<dp[0].length;i++) {
ans = Math.min(ans,dp[dp.length-1][i]+m2t[i]);
}
return ans>=10000?-1:ans;
}
//dfs(不用这个)
//对于给定的两个点,中间存在障碍物,求P1到P2的最短距离
//i,j为当前的点.x,y目标点
public int dfs(int i,int j,int[][] flag,int x,int y){
//如果越界,或者该点为障碍物,或者该点已经走过,说明该方向无法到达目标点。置一个【大】值。
if(i<0 || j<0 || i>=maze.length || j >= maze[0].length || maze[i][j] == '#' || flag[i][j] == 1){
return 10000;
}
flag[i][j] = 1;
//如果该点就是终点
if(i == x && j == y){
flag[i][j] = 0;
return 0;
}
int up = 0;
int down = 0;
int left = 0;
int right = 0;
//进行bfs+记忆的方式
//四个方向-上下左右
//上
up = dfs(i-1,j,flag,x,y);
//下
down = dfs(i+1,j,flag,x,y);
//左
left = dfs(i,j-1,flag,x,y);
//右
right = dfs(i,j+1,flag,x,y);
flag[i][j] = 0;
//取四个方向中最短的长度
int minLength = Math.min(Math.min(up,down),Math.min(left,right));
return minLength+1;
}
//bfs
private int bfs(Point from, Point to) {
int[][] dist = new int[150][150];;
Queue<Point> queue = new LinkedList<>();;
int[] dir = {-1, 0, 1, 0, -1}; // 压缩方向数组,二维变一维, {-1,0},{0,1},{1,0},{0,-1}
// 特判: 如果是墙壁, 返回 -1
if (maze[from.x][from.y] == '#') {
return 10000;
}
// 初始化 dist 数组
for (int[] a : dist) {
Arrays.fill(a, -1);
}
queue.offer(from);
dist[from.x][from.y] = 0;
while (!queue.isEmpty()) {
Point cur = queue.poll();
int x = cur.x;
int y = cur.y;
for (int i = 0; i < 4; i++) {
int nx = x + dir[i];
int ny = y + dir[i + 1];
if (nx < 0 || nx >= maze.length || ny < 0 || ny >= maze[0].length || maze[nx][ny] == '#') continue;
if (dist[nx][ny] == -1) {
dist[nx][ny] = dist[x][y] + 1;
queue.offer(new Point(nx, ny));
}
}
}
return dist[to.x][to.y]==-1?10000:dist[to.x][to.y];
}
}
(来自leetcode官方)
假设迷宫的面积为 s,M 的数量为 m,O 的数量为 o。
时间复杂度:O(ms + m2o + 2mm2)。单次 BFS 的时间代价为 O(s),m 次 BFS 的时间代价为 O(ms);预处理任意两个 M 经过 O 的最短距离的时间代价是 O(m2o);动态规划的时间代价是 O(2mm2)
空间复杂度:O(s + bs + 2mm)。BFS 队列的空间代价是 O(s);预处理 Mi到各个点的最短距离的空间代价是 O(bs);动态规划数组的空间代价是 O(2mm)。