题目地址:
https://leetcode.com/problems/the-maze-ii/
给定一个二维 0 − 1 0-1 0−1矩阵, 0 0 0代表空地, 1 1 1代表障碍物。在某个空地上有个小球,它可以沿着四个方向滑动,每次滑动时只有遇到边界或者障碍物才会停下来。再给定一个空地作为终点,问该小球是否可能滑到终点,若可能,则返回最短路径的长度(不包括起点,包括终点);若不可能,返回 − 1 -1 −1。
题目类似于无向带权图的单源最短路问题,所以可以用Dijkstra算法。先将源点加入一个最小堆(按照与源点的路径距离排序),然后将源点出堆,判断其是否已经到了终点,如果到了则直接返回路径长度(Dijkstra算法可以保证出堆的那个点的路径长度就是源点到该点最短路径长度),否则将这个点能到达的点加入最小堆(如果某次从堆中出来的点是 A A A,当算能到达的点的时候,将这些点的路径长度设为这个点与 A A A的距离加上源点与 A A A的距离,即两段距离的和。另外注意,由于算法保证了出堆的点对应的距离就是最短路的距离,所以要标记其为已访问过,以后再遇到这个点的时候就直接跳过),加入堆后再重复上述过程即可。
代码如下:
class Solution {
public:
using PIII = pair<int, pair<int, int>>;
#define x first
#define y second
const int INF = 1e9;
int shortestDistance(vector<vector<int>>& g, vector<int>& start,
vector<int>& dest) {
int m = g.size(), n = g[0].size();
priority_queue<PIII, vector<PIII>, greater<>> heap;
int bx = start[0], by = start[1], ex = dest[0], ey = dest[1];
heap.push({0, {bx, by}});
vector<vector<int>> dist(m, vector<int>(n, INF));
dist[bx][by] = 0;
vector<vector<bool>> vis(m, vector<bool>(n, false));
// 得到从(x, y)出发能滚到的位置
auto get_ne = [&](int x, int y) {
vector<PIII> res;
static int d[] = {-1, 0, 1, 0, -1};
for (int i = 0; i < 4; i++) {
int dx = d[i], dy = d[i + 1];
int delta;
for (delta = 1;; delta++) {
int nx = x + delta * dx, ny = y + delta * dy;
if (0 <= nx && nx < m && 0 <= ny && ny < n && !g[nx][ny]) continue;
else break;
}
// 略过前进不了的方向
if (delta == 1) continue;
int nx = x + (delta - 1) * dx, ny = y + (delta - 1) * dy;
// 略过最短路已经算出来过的点
if (vis[nx][ny]) continue;
res.push_back({delta - 1, {nx, ny}});
}
return res;
};
while (heap.size()) {
auto [d, cur] = heap.top(); heap.pop();
auto [x, y] = cur;
if (vis[x][y]) continue;
if (x == ex && y == ey) return d;
vis[x][y] = true;
dist[x][y] = d;
for (auto [dis, ne] : get_ne(x, y)) {
auto [nx, ny] = ne;
if (d + dis < dist[nx][ny]) {
dist[nx][ny] = d + dis;
heap.push({dist[nx][ny], {nx, ny}});
}
}
}
return -1;
}
};
时间复杂度 O ( m n log ( m n ) ) O(mn\log (mn)) O(mnlog(mn)),空间 O ( m n ) O(mn) O(mn)。
需要注意:
if (vis[x][y]) continue;
这句话是必要的。它不影响正确性,但会提高效率。虽然在get_ne
这个方法里,返回的是未算过最短路的点,但是仍然有可能某个点已经在堆中未算过最短路,然后在get_ne
里又加入了堆里。这时等到这个点算过最短路后,后面这个点可能又出堆一次,而这一次需要略过,即用continue
跳过下面的部分。