🌞LCP 13. 寻宝
2020.7.29 LeetCode
好难啊
题目描述
我们得到了一副藏宝图,藏宝图显示,在一个迷宫中存在着未被世人发现的宝藏。
迷宫是一个二维矩阵,用一个字符串数组表示。它标识了唯一的入口(用 'S' 表示),和唯一的宝藏地点(用 'T' 表示)。但是,宝藏被一些隐蔽的机关保护了起来。在地图上有若干个机关点(用 'M' 表示),只有所有机关均被触发,才可以拿到宝藏。
要保持机关的触发,需要把一个重石放在上面。迷宫中有若干个石堆(用 'O' 表示),每个石堆都有无限个足够触发机关的重石。但是由于石头太重,我们一次只能搬一个石头到指定地点。
迷宫中同样有一些墙壁(用 '#' 表示),我们不能走入墙壁。剩余的都是可随意通行的点(用 '.' 表示)。石堆、机关、起点和终点(无论是否能拿到宝藏)也是可以通行的。
我们每步可以选择向上/向下/向左/向右移动一格,并且不能移出迷宫。搬起石头和放下石头不算步数。那么,从起点开始,我们最少需要多少步才能最后拿到宝藏呢?如果无法拿到宝藏,返回 -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。
![](https://img2020.cnblogs.com/blog/1922094/202007/1922094-20200729100707468-980981759.png)
示例二:
输入: ["S#O", "M.#", "M.T"]
输出:-1
解释:我们无法搬到石头触发机关
示例三:
输入: ["S#O", "M.T", "M.."]
输出:17
解释:注意终点也是可以通行的。
题解
class Solution {
private static final int MAX_VALUE = Integer.MAX_VALUE/2;
static class Point{
int x;
int y;
int dis;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public Point setDis(int dis) {
this.dis = dis;
return this;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point point = (Point) o;
return x == point.x &&
y == point.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}
int[] sToMdis;
int[][] mToMdis;
int[] sToOdis;
int[][] mToOdis;
int[][] oToMdis;
int[] mToTdis;
Map<Point, Integer> pointToIndex = new HashMap<>();
char[][] map;
public int minimalSteps(String[] maze) {
map = new char[maze.length+2][maze[0].length()+2];
for(char[] row : map) {
Arrays.fill(row, '#');
}
int oLen = 0;
int mLen = 0;
Point start = null;
Point end = null;
List<Point> oList = new ArrayList<>();
List<Point> mList = new ArrayList<>();
for(int y = 0;y<maze.length;y++) {
for(int x = 0; x < maze[0].length();x++) {
map[y+1][x+1] = maze[y].charAt(x);
Point p = new Point(x+1, y+1);
if (map[y+1][x+1] == 'O') {
oList.add(p);
pointToIndex.put(p, oLen++);
} else if (map[y+1][x+1] == 'M') {
mList.add(p);
pointToIndex.put(p, mLen++);
}else if(map[y+1][x+1] == 'S') {
pointToIndex.put(p, 0);
start = new Point(x+1,y+1);
}else if (map[y+1][x+1] == 'T') {
pointToIndex.put(p,0);
end = p;
}
}
}
Map<Integer,Integer> sToTMap = bfs(start.x, start.y, 'T');
// 如果s到不了T,直接返回-1
if (sToTMap.isEmpty()) {
return -1;
}
// 如果M的个数为0,那么直接返回S到T的距离
if (mLen == 0) {
return sToTMap.get(0);
}
// 计算s到所有O的距离
sToOdis = new int[oLen];
Arrays.fill(sToOdis, MAX_VALUE);
Map<Integer,Integer> sToOMap = bfs(start.x, start.y, 'O');
// 如果s连1个O都到不了,肯定不行。
if (sToOMap.isEmpty()) {
return -1;
}
for (Map.Entry<Integer, Integer> entry : sToOMap.entrySet()) {
sToOdis[entry.getKey()] = entry.getValue();
}
// 如果s到不了所有M,则也不行
Map<Integer,Integer> sToMMap = bfs(start.x, start.y, 'M');
if (sToMMap.size() < mLen) {
return -1;
}
// 计算所有o到M的距离
oToMdis = new int[oLen][mLen];
for (int[] oom : oToMdis) {
Arrays.fill(oom, MAX_VALUE);
}
int i = 0;
for (Point op : oList) {
Map<Integer,Integer> oToMMap = bfs(op.x, op.y, 'M');
if (oToMMap.isEmpty()) {
i++;
continue;
}
for (Map.Entry<Integer, Integer> entry : oToMMap.entrySet()) {
oToMdis[i][entry.getKey()] = entry.getValue();
}
i++;
}
mToOdis = new int[mLen][oLen];
mToTdis = new int[mLen];
i = 0;
// 计算所有m到O的距离,和所有m到T的距离
for (Point mp : mList) {
Map<Integer,Integer> mToOMap = bfs(mp.x, mp.y, 'O');
// 有一个M找不到任何和O的路径,那么就是错误的
if (mToOMap.isEmpty()) {
return -1;
}
for (Map.Entry<Integer, Integer> entry : mToOMap.entrySet()) {
mToOdis[i][entry.getKey()] = entry.getValue();
}
// 有一个找不到T,那也是错误的
Map<Integer,Integer> mToTMap = bfs(mp.x, mp.y, 'T');
if (mToTMap.isEmpty()) {
return -1;
}
mToTdis[i] = mToTMap.get(0);
i++;
}
sToMdis = new int[mLen];
Arrays.fill(sToMdis, MAX_VALUE);
mToMdis = new int[mLen][mLen];
for (int[] mmm : mToMdis) {
Arrays.fill(mmm, MAX_VALUE);
}
for (int m = 0; m < mLen;m++) {
for (int o = 0; o < oLen;o++) {
// 计算s到各m且至少经过1个O的最小距离
int dis = sToOdis[o] + oToMdis[o][m];
if (dis < sToMdis[m]) {
sToMdis[m] = dis;
}
// 计算m到各mEnd且至少经过1个O的最小距离
for (int mEnd = 0;mEnd<mLen;mEnd++) {
int mmDis = mToOdis[m][o] + oToMdis[o][mEnd];
if (mmDis < mToMdis[m][mEnd]) {
mToMdis[m][mEnd] = mmDis;
}
}
}
}
int mStatu = (1 << mLen);
int[][][] dp = new int[mLen+1][mStatu][mLen];
int bigStatu = mStatu - 1;
for (int m = 0; m < mLen;m++) {
dp[mLen][bigStatu][m] = mToTdis[m];
}
// dp指该状态下该位置的m到终点的最短距离
// 对于那个m他要选1个m
for (int mCount = mLen-1;mCount>=1;mCount--) {
for(int statu = bigStatu; statu>=0;statu--) {
for (int m = 0; m < mLen;m++) {
int minDis = MAX_VALUE;
for(int endM = 0;endM < mLen;endM++) {
// endM必须是0
if ((statu & (1<<endM)) != 0 || m == endM) {
continue;
}
int realDis = mToMdis[m][endM] + dp[mCount+1][statu | (1<<endM)][endM];
if (realDis < minDis) {
minDis = realDis;
}
}
dp[mCount][statu][m] = minDis;
}
}
}
int result = MAX_VALUE;
for (int m = 0; m < mLen;m++) {
int sToMToTDis = sToMdis[m] + dp[1][1<<m][m];
if (sToMToTDis < result) {
result = sToMToTDis;
}
}
return result == MAX_VALUE?-1:result;
}
Map<Integer, Integer> bfs(int x, int y, char findChar) {
boolean[][] vis = new boolean[map.length][map[0].length];
Queue<Point> queue = new LinkedList<>();
queue.offer(new Point(x, y));
vis[y][x] = true;
Map<Integer, Integer> indexAndDisMap = new HashMap<>();
while(!queue.isEmpty()) {
Point p = queue.poll();
int px = p.x;
int py = p.y;
if (map[py][px] == findChar) {
indexAndDisMap.put(pointToIndex.get(p), p.dis);
}
deal(px+1,py,queue,vis, p.dis + 1);
deal(px-1,py,queue,vis, p.dis + 1);
deal(px,py-1,queue,vis, p.dis + 1);
deal(px,py+1,queue,vis, p.dis + 1);
}
return indexAndDisMap;
}
void deal(int x,int y, Queue<Point> queue, boolean[][] vis, int dis) {
if (!vis[y][x] && map[y][x] != '#') {
vis[y][x] = true;
queue.offer(new Point(x,y).setDis(dis));
}
}
}
思路
我是真的不会
第一轮状态压缩
先利用bfs
求出起点S到所有O的最短路径s2oDis[] (2就是英文to的意思)
求出所有O到所有M的最短路径o2mDis[][]
求出所有M到所有O的最短路径m2oDis[][] (这个其实把上面的o2mDis反一下就行了,我写的时候忘记了,又重新跑一边)
求出所有M到终点T的最短路径m2tDis[]
这时候相当于已经对图做了一轮压缩了。
但这时候还是不能起做搜索, O的节点有40个,M的节点有16个,合起来相当于最多有六十多层,铁定超时。
第二轮压缩
那么还能压缩吗?
可以!
S到每个M的时候,中间至少要经过1个O
每个M到其他的M时,也至少要经过1个O
那么S到每个M的距离s2mDis[mIndex] = Min(s2oDis[oIndex] + o2mDis[oIndex][mIndex])
直接对oIndex和mIndex做2个for循环搞定。
for (int m = 0; m < mLen;m++) {
for (int o = 0; o < oLen;o++) {
// 计算s到各m且至少经过1个O的最小距离
int dis = sToOdis[o] + oToMdis[o][m];
if (dis < sToMdis[m]) {
sToMdis[m] = dis;
}
}
}
M到各M同理,于是我们又得到了m2mDis[][]
那么整个图就压缩成了3个数组,并且节点最多也就18个。
起点S到个M的最短距离 s2mDis[]
各M到各M的最短距离 m2mDis[][]
各M到终点的最短距离 m2tDis[]
搜索和剪枝
那么来就是个搜索问题了,dfs或者bfs都可以 ,最多也就18层。
但是这里有个条件: 必须经过所有的M。以至于你得注意剪枝。避免反复搜重复的局面。
那么如何判断局面是否经历过?
题目把M的最大个数正好设定成16,其实就是在暗示你用位运算来记录已走路线。
假设M有4个,则二进制(1001)=9代表了此时第0个M和第3个M已被走过,可以用statu来表示
还得注意一下此时所处M的位置, 用nowMIndex表示
则我们就可以用数组minDis[statu][nowMIndex]来记录已搜索到的最短路径了,做一下剪枝,就能AC了。
非搜索的dp方式
除了用bfs和dfs做最后的搜索, 也可以用dp,我是用dp的,因为之前做过类似的题目。
dp[已走的M个数][当前M局面]][当前所处M位置] 等于该状态下到T的最短距离
dp[已走的M个数][当前M局面]][当前所处M位置]
= min( dp[已走M个数+1][选择下一个M后的局面][下一个M的位置] )
dp的时候从最后的局面一步步往前搜即可。
注:第一个下标[已走M的个数]也可以不需要,避免局面值已经能计算出M的个数了
但我为了方便写还是用了,代价就是最长例子用了1.4秒差点超了,如果会超时我就去掉这个下标
int mStatu = (1 << mLen);
int[][][] dp = new int[mLen+1][mStatu][mLen];
int bigStatu = mStatu - 1;
for (int m = 0; m < mLen;m++) {
dp[mLen][bigStatu][m] = mToTdis[m];
}
// dp指该状态下该位置的m到终点的最短距离
//dp[已走的M个数][当前M局面]][当前所处M位置]
for (int mCount = mLen-1;mCount>=1;mCount--) {
for(int statu = bigStatu; statu>=0;statu--) {
for (int m = 0; m < mLen;m++) {
int minDis = MAX_VALUE;
for(int endM = 0;endM < mLen;endM++) {
// endM所在位必须是0。
if ((statu & (1<<endM)) != 0 || m == endM) {
continue;
}
int realDis = mToMdis[m][endM] + dp[mCount+1][statu | (1<<endM)][endM];
if (realDis < minDis) {
minDis = realDis;
}
}
dp[mCount][statu][m] = minDis;
}
}
}
需要注意的情况(如果用搜索可能会陷入这个坑)
不存在M和O的情况
只存在O,不存在M( 这个贼坑)
S到不了T
S到不了所有的M
存在M到不了任意一个O
本文来自博客园,作者:两小无猜,转载请注明原文链接:https://www.cnblogs.com/charlottepl/p/13395507.html