Minimum Number of Visited Cells in a Grid

Minimum Number of Visited Cells in a Grid

You are given a 0-indexed m x n integer matrix grid . Your initial position is at the top-left cell (0, 0).

Starting from the cell (i, j), you can move to one of the following cells:

  • Cells (i, k) with j < k <= grid[i][j] + j (rightward movement), or
  • Cells (k, j) with i < k <= grid[i][j] + i (downward movement).

Return the minimum number of cells you need to visit to reach the bottom-right cell (m - 1, n - 1). If there is no valid path, return -1.

Example 1:

Input: grid = [[3,4,2,1],[4,2,3,1],[2,1,0,0],[2,4,0,0]]
Output: 4
Explanation: The image above shows one of the paths that visits exactly 4 cells.

Example 2:

Input: grid = [[3,4,2,1],[4,2,1,1],[2,1,1,0],[3,4,1,0]]
Output: 3
Explanation: The image above shows one of the paths that visits exactly 3 cells.

Example 3:

Input: grid = [[2,1,0],[1,0,0]]
Output: -1
Explanation: It can be proven that no path exists.

Constraints:

  • m == grid.length
  • n == grid[i].length
  • 1m,n105
  • 1m×n105
  • 0grid[i][j]<m×n
  • grid[m1][n1] == 0

 

解题思路

  与方格迷宫很像,每个节点能够到达的节点数很多,因此在bfs时需要枚举的边数很多。但这题只有向右和向下两个方向,其他的条件与方格迷宫也很像,因此可以用相同的剪枝策略。

  即从(x,y)枚举能到达的格子(x,y),如果有dx,y<dx,y+1,那么就break,因为用(x,y)更新后面的格子得到的结果不比(x,y)差。这样可以保证在同一个方向上所有格子最多会被枚举到2次,两个方向一共最多会枚举4×nm次,因此时间复杂度就是O(nm),证明的话可以参考方格迷宫

  这题在剪枝的时候还要个地方需要注意,因为每个格子能扩展的距离不一定相同,比如从(x,y)向右最大能扩展到(x,y+g[x][y])(x,y)向右最大能扩展到(x,y+g[x][y]),而y+g[x][y]<y+g[x][y],现在有dx,y<dx,y+1,如果此时直接break,那么[y+g[x][y]+1y+g[x][y]]中还没有枚举到的格子就无法被(x,y)更新,因此还应该继续从y+g[x][y]+1向右枚举。向下的方向也同理。

  AC代码如下:

复制代码
 1 class Solution {
 2 public:
 3     int minimumVisitedCells(vector<vector<int>>& grid) {
 4         int n = grid.size(), m = grid[0].size();
 5         vector<vector<int>> dist(n, vector<int>(m, 0x3f3f3f3f));
 6         dist[0][0] = 0;
 7         queue<pair<int, int>> q({{0, 0}});
 8         while (!q.empty()) {
 9             int x = q.front().first, y = q.front().second;
10             q.pop();
11             // 向下枚举 
12             for (int i = x + 1; i <= grid[x][y] + x; i++) {
13                 if (i >= n) break;
14                 if (dist[i][y] > dist[x][y] + 1) {
15                     dist[i][y] = dist[x][y] + 1;
16                     q.push({i, y});
17                 }
18                 else if (dist[i][y] < dist[x][y] + 1) {
19                     if (grid[x][y] + x > grid[i][y] + i) i += grid[i][y];    // 从(x,y)能比(i,y)走到更下的地方 
20                     else break;
21                 }
22             }
23             // 向右枚举 
24             for (int j = y + 1; j <= grid[x][y] + y; j++) {
25                 if (j >= m) break;
26                 if (dist[x][j] > dist[x][y] + 1) {
27                     dist[x][j] = dist[x][y] + 1;
28                     q.push({x, j});
29                 }
30                 else if (dist[x][j] < dist[x][y] + 1) {
31                     if (grid[x][y] + y > grid[x][j] + j) j += grid[x][j];    // 从(x,y)能比(x,j)走到更右的地方 
32                     else break;
33                 }
34             }
35         }
36         return dist[n - 1][m - 1] == 0x3f3f3f3f ? -1 : dist[n - 1][m - 1] + 1;
37     }
38 };
复制代码

  对于边数很多的情况,可以再参考Minimum Reverse Operations中用并查集和平衡树的做法。

  先简单给出平衡树的思路。本质上就是根据bfs中更新过的节点不会再被更新这个性质,我们在枚举相邻节点时可以把那些已经更新过的节点跳过。做法就是开个平衡树,然后找到能更新的范围,对这个范围内还存在未更新过的节点进行更新,并将其从平衡树中删除,这样下次就不会再枚举到这个已更新的节点了。而本题是二维的情况,因此可以给每一行开一个平衡树,一共n个;每一列开一个平衡树,一共m个。当更新了位置(x,y)时,不管是哪个方向,在维护第x行的平衡树中删除元素y,在维护第y列的平衡树中删除元素x

  AC代码如下,时间复杂度为O(nm(logn+logm))

复制代码
 1 class Solution {
 2 public:
 3     int INF = 1e9;
 4     
 5     int minimumVisitedCells(vector<vector<int>>& grid) {
 6         int n = grid.size(), m = grid[0].size();
 7         vector<set<int>> st1(n), st2(m);
 8         for (int i = 0; i < n; i++) {
 9             for (int j = 0; j < m; j++) {
10                 st1[i].insert(j);
11             }
12             st1[i].insert({-INF, INF});    // 插入哨兵,保证每次都能二分出结果 
13         }
14         for (int i = 0; i < m; i++) {
15             for (int j = 0; j < n; j++) {
16                 st2[i].insert(j);
17             }
18             st2[i].insert({-INF, INF});
19         }
20         vector<vector<int>> dist(n, vector<int>(m, 0x3f3f3f3f));
21         dist[0][0] = 1;
22         st1[0].erase(0), st2[0].erase(0);    // (0,0)已经更新过了,因此在维护第0行的平衡树中删除0,在维护第0列的平衡树中删除0
23         queue<pair<int, int>> q({{0, 0}});
24         while (!q.empty()) {
25             auto t = q.front();
26             q.pop();
27             auto l = st1[t.first].lower_bound(t.second + 1), r = st1[t.first].upper_bound(t.second + grid[t.first][t.second]);
28             // 向右枚举 
29             while (l != r) {
30                 dist[t.first][*l] = dist[t.first][t.second] + 1;
31                 q.push({t.first, *l});    // 更新了位置(t.first, l) 
32                 st2[*l].erase(t.first);    // 在维护第l列的平衡树中删除t.first
33                 l = st1[t.first].erase(l);    // 在维护第t.first行的平衡树中删除l 
34             }
35             l = st2[t.second].lower_bound(t.first + 1), r = st2[t.second].upper_bound(t.first + grid[t.first][t.second]);
36             while (l != r) {
37                 dist[*l][t.second] = dist[t.first][t.second] + 1;
38                 q.push({*l, t.second});    // 更新了位置(l, t.second) 
39                 st1[*l].erase(t.second);    // 在维护第l行的平衡树中删除t.second
40                 l = st2[t.second].erase(l);    // 在维护第t.second列的平衡树中删除l
41             }
42         }
43         return dist[n - 1][m - 1] == 0x3f3f3f3f ? -1 : dist[n - 1][m - 1];
44     }
45 };
复制代码

  并查集就是维护删除元素的连通性,每次直接向右找到第一个还没被删除的位置,然后删除这个位置再维护连通性。也需要对每一行和每一列都开并查集维护。当更新了位置(x,y)时,需要在维护第x行的并查集中将y所在的集合合并到y+1所在的集合;在维护第y列的并查集中将x所在的集合合并到x+1所在的集合。

  AC代码如下,时间复杂度为O(nm)

复制代码
 1 class Solution {
 2 public:
 3     int minimumVisitedCells(vector<vector<int>>& grid) {
 4         int n = grid.size(), m = grid[0].size();
 5         vector<vector<int>> fa1(n, vector<int>(m + 1)), fa2(m, vector<int>(n + 1));
 6         for (int i = 0; i < n; i++) {
 7             for (int j = 0; j <= m; j++) {
 8                 fa1[i][j] = j;
 9             }
10         }
11         for (int i = 0; i < m; i++) {
12             for (int j = 0; j <= n; j++) {
13                 fa2[i][j] = j;
14             }
15         }
16         vector<vector<int>> dist(n, vector<int>(m, 0x3f3f3f3f));
17         dist[0][0] = 1;
18         fa1[0][0] = fa2[0][0] = 1;
19         queue<pair<int, int>> q({{0, 0}});
20         function<int(vector<int>&, int)> find = [&](vector<int> &fa, int x) {
21             return fa[x] == x ? fa[x] : fa[x] = find(fa, fa[x]);
22         };
23         while (!q.empty()) {
24             auto t = q.front();
25             q.pop();
26             int y = find(fa1[t.first], t.second + 1);
27             while (y <= t.second + grid[t.first][t.second] && y < m) {
28                 dist[t.first][y] = dist[t.first][t.second] + 1;
29                 q.push({t.first, y});
30                 fa1[t.first][y] = find(fa1[t.first], y + 1);
31                 fa2[y][t.first] = find(fa2[y], t.first + 1);
32                 y = find(fa1[t.first], y + 1);
33             }
34             int x = find(fa2[t.second], t.first + 1);
35             while (x <= t.first + grid[t.first][t.second] && x < n) {
36                 dist[x][t.second] = dist[t.first][t.second] + 1;
37                 q.push({x, t.second});
38                 fa2[t.second][x] = find(fa2[t.second], x + 1);
39                 fa1[x][t.second] = find(fa1[x], t.second + 1);
40                 x = find(fa2[t.second], x + 1);
41             }
42         }
43         return dist[n - 1][m - 1] == 0x3f3f3f3f ? -1 : dist[n - 1][m - 1];
44     }
45 };
复制代码

 

参考资料

  暴力 BFS | BFS + 并查集 | BFS + 平衡树 | 线段树:https://leetcode.cn/problems/minimum-number-of-visited-cells-in-a-grid/solution/bfs-bing-cha-ji-by-xjliang-3ito/

posted @   onlyblues  阅读(38)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
历史上的今天:
2021-04-16 并查集算法
Web Analytics
点击右上角即可分享
微信分享提示