1162.地图分析

[TOC]

一、题目描述

你现在手里有一份大小为 N x N 的『地图』(网格) grid,上面的每个『区域』(单元格)都用 01 标记好了。其中 0 代表海洋,1 代表陆地,你知道距离陆地区域最远的海洋区域是哪一个吗?请返回该海洋区域到离它最近的陆地区域的距离。

PS:不知道你们是怎么想的,反正我当时读到这一题中文描述的时候被绕晕了。附上这一段的英文描述:
Given an N x N grid containing only values 0 and 1, where 0 represents water and 1 represents land, find a water cell such that its distance to the nearest land cell is maximized and return the distance.

我们这里说的距离是『曼哈顿距离』( Manhattan Distance):(x0, y0)(x1, y1) 这两个区域之间的距离是 |x0 - x1| + |y0 - y1|

如果我们的地图上只有陆地或者海洋,请返回 -1

示例 1:

1 0 1
0 0 0
1 0 1
输入:[[1,0,1],[0,0,0],[1,0,1]]
输出:2
解释: 
海洋区域 (1, 1) 和所有陆地区域之间的距离都达到最大,最大距离为 2。

示例 2:

1 0 0
0 0 0
0 0 0
输入:[[1,0,0],[0,0,0],[0,0,0]]
输出:4
解释: 
海洋区域 (2, 2) 和所有陆地区域之间的距离都达到最大,最大距离为 4。

提示:

  1. 1 <= grid.length == grid[0].length <= 100
  2. grid[i][j] 不是 0 就是 1

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/as-far-from-land-as-possible
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

二、解题方法

1. BFS(暴力法)


思路:

题意很清晰,要求找到一个海洋节点,它到最近的陆地节点的距离,是所有海洋节点中最远的。
那么对每个海洋节点,其实就是求它到陆地节点的最短距离,一个最直观的思路,必然就是枚举每一个海洋节点,利用BFS找出最短距离,然后求出所有最短距离中最大者即可。

如果将每次入队视为基本操作,最坏情况全是海洋节点,时间复杂度:O((mn)^2),空间复杂度:O(mn)

当然,毫无悬念的,超时


代码:

class Solution {
public:
    int maxDistance(vector<vector<int>>& grid) {
        auto ans = -1;
        for (auto i = 0; i < grid.size(); ++i)
            for (auto j = 0; j < grid[i].size(); ++j)
                if (grid[i][j] == 0)
                    ans = max(ans, bfs(grid, i, j));
        return ans;
    }

    int bfs(vector<vector<int>> &grid, int i, int j) {
        vector<int> r = { -1, 0, 1, 0 };
        vector<int> c = { 0, 1, 0, -1 }; 
        queue<pair<int, int>> pos;
        pos.push({i, j}); 
        while (!pos.empty()) {
            auto cur = pos.front();
            pos.pop();
            grid[cur.first][cur.second] = -1;
            for (auto k = 0; k < r.size(); ++k) {
                auto row = cur.first + r[k];
                auto col = cur.second + c[k];
                if (valid(grid, row, col)) {
                    if (grid[row][col] == 1) {
                        reset(grid);
                        return abs(row - i) + abs(col - j);
                    }
                    else if (grid[row][col] == 0)
                        pos.push({row, col});
                }
            }
        }
        reset(grid);
        return -1;
    }

    inline bool valid(const vector<vector<int>> &grid, int i, int j) {
        return -1 < i && i < grid.size() && -1 < j && j < grid[i].size();
    }

    void reset(vector<vector<int>> &grid) {
        for (auto i = 0; i < grid.size(); ++i)
            for (auto j = 0; j < grid[i].size(); ++j)
                grid[i][j] = grid[i][j] == -1 ? 0 : grid[i][j];
    }
};

2. BFS(一种设想)


思路:

解法1最主要的问题在于重复计算量太大,例如输入:

0 1
0 0 1
1 1 1

首先在计算grid[0][0]时,grid[0][1]grid[1][0]也会入队列,进而计算出结果。然而接下来我们还会分别计算grid[0][1]grid[1][0],这里就存在了重复计算。

划重点,重复计算。一种可能想到的方法来解决重复计算,是使用DFS配合记忆化搜索,定义:
\begin{equation} grid[i][j]= \begin{cases} 2 & \text{正在搜索} \\ 1 & \text{陆地} \\ 0 & \text{海洋} \\ -x & \text{到陆地的最近距离为x} \end{cases} \end{equation}
看起来好像美滋滋,然并卵。DFS并不能行得通,举例:

a b c
d e f
g h i

如果此时正在计算b,需要用到e,按照DFS的思想,会去先计算e,得到通往e的最短距离。但如果此时,e的最短距离的路径中,包含了b该怎么办?先有鸡还是先有蛋?

当然,如果有大佬有DFS的解法,我是真心希望可以不吝赐教,因为我也很好奇还有啥解法。
已使用动态规划解决,见解法4。

那么,接下来还有什么优化方案吗?
这里的一个设想是,既然我已经使用BFS搜索出一些节点了,我可不可以利用已经确切计算出最短距离的那些节点,作为我搜索次数的上界?
就是说,如果计算grid[i][j]时,入队了grid[i-1][j],而grid[i-1][j]已经确实计算好了距离,我们就可以用grid[i-1][j]的值作为接下来最大的搜索层数(最坏的情况无外乎经过grid[i-1][j]那条路径),并维护该层数(为了防止有比经过grid[i-1][j]更短的路径)。

具体并没有测试过,性能提升是一定有的,只是不知道会有多大。

3. BFS(填海造田)


思路:

这是真的骚操作,
题目说的很清楚,

“你知道距离陆地区域最远的海洋区域是哪一个吗?”

提示都给了,到陆地的最短距离最大的那个海洋,反过来说是不是从陆地来看离得最远的那片海洋?
那我只要“填海造田”,看看直到填完最后一块海洋需要用几步,不就找到了离陆地最远的那片海洋了?

将所有陆地入队,然后每次将该陆地周围一圈的海洋变为陆地,新的陆地入队,看看多少步之后队列为空即可。

如果将每次入队视为基本操作,每个节点均需且仅需入队一次,时间复杂度:O(mn),空间复杂度:O(mn)


代码:

class Solution {
public:
    int maxDistance(vector<vector<int>>& grid) {
        queue<pair<int, int>> land;
        for (auto i = 0; i < grid.size(); ++i)
            for (auto j = 0; j < grid[i].size(); ++j)
                if (grid[i][j] == 1)
                    land.push({i, j});

        if (land.empty() || land.size() == grid.size() * grid[0].size())
            return -1;

        auto ans = 0;
        const int r[] = { -1, 0, 1, 0 };
        const int c[] = { 0, 1, 0, -1 };
        queue<pair<int, int>> next;
        while (!land.empty()) {
            ans++;
            while (!land.empty()) {
                auto cur = land.front();
                land.pop();
                for (auto k = 0; k < 4; ++k) {
                    auto i = cur.first + r[k];
                    auto j = cur.second + c[k];
                    if (valid(grid, i, j) && grid[i][j] == 0) {
                        grid[i][j] = 1;
                        next.push({i, j});
                    }
                }
            }
            land.swap(next);
        }
        return ans - 1;      // 为了取消判断next是否为空的if,
                             // 令next队列为空的那一次迭代就说明没有海了,
                             // ans不应该自增
    }

    inline bool valid(const vector<vector<int>> &grid, int i, int j) {
        return -1 < i && i < grid.size() && -1 < j && j < grid[i].size();
    }
};

4. 动态规划


思路:

正如我们在解法2中考虑的那样,解法1最让人难以接受的就是重复计算了。

其实我觉得自己DFS那一段的设想还是可以的,毕竟如果我求grid[i][j]的最短距离,一定可以通过其周围四格的最短距离推断得到,因为它们是相邻的。
但苦于先有鸡还是先有蛋,没有办法实际操作。

等等,利用周围四格来求出grid[i][j],怎么有点动态规划的味道???难道这一题可以使用动态规划来做???

说干就干,让我们想想到底该怎么弄。
先考虑初始条件,很明显:
\begin{equation} dp[i][j] = \begin{cases} 0 & grid[i][j] = 1 \\ +\infty & grid[i][j] = 0 \end{cases} \end{equation}
接下来是状态转移方程,最直观的一定是:
dp[i][j] = min(dp[i][j],min(dp[i-1][j], dp[i][j+1], dp[i+1][j], dp[i][j-1]) + 1)
但如果是常规扫描的话,在求解dp[i][j]时,dp[i][j+1]dp[i+1][j]都还是未知的,无法使用。也许,我们可以在求解顺序上做文章?
常规扫描只能保证在求解dp[i][j]时,已经求结果一次dp[i-1][j]dp[i][j-1],它们等价于从上往下从左往右的最短距离。那我是不是只要再来一次从右往左从下往上的最短距离,就可以得到最终解了?
那我如何保证在求解dp[i][j]时,dp[i][j+1]dp[i+1][j]已经求出来了呢?只要从下往上从右往左扫描grid数组即可。

于是我们有,
第一次扫描时:
dp[i][j] = min(dp[i][j], min(dp[i-1][j], dp[i][j-1]) + 1)
第二次扫描时:dp[i][j] = min(dp[i][j], min(dp[i+1][j], dp[i][j+1]) + 1)

时间复杂度:O(mn),空间复杂度:O(mn)


代码:

class Solution {
public:
    int maxDistance(vector<vector<int>>& grid) {
        auto rows = int(grid.size());
        auto cols = int(grid[0].size());
        
        auto waters = 0;
        for (auto i = 0; i < rows; ++i)
            for (auto j = 0; j < cols; ++j)
                waters += 0 == grid[i][j] ? 1 : 0;
        if (0 == waters || rows * cols == waters)
            return -1;

        vector<vector<int>> dp(rows, vector<int>(cols, numeric_limits<int>::max() - 1));
        for (auto i = 0; i < rows; ++i) {
            for (auto j = 0; j < cols; ++j) {
                dp[i][j] = 1 == grid[i][j] ? 0 : dp[i][j];
                if (0 < i)  dp[i][j] = min(dp[i][j], dp[i-1][j] + 1);
                if (0 < j)  dp[i][j] = min(dp[i][j], dp[i][j-1] + 1);
            }
        }
        auto ans = -1;
        for (auto i = rows - 1; i > -1; --i) {
            for (auto j = cols - 1; j > -1; --j) {
                if (i < rows - 1)   dp[i][j] = min(dp[i][j], dp[i+1][j] + 1);
                if (j < cols - 1)   dp[i][j] = min(dp[i][j], dp[i][j+1] + 1);
                ans = max(ans, dp[i][j]);
            }
        }
        return ans;
    }
};
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342