leetcode
leetcode 1751 ~ 1800
你能穿过矩阵的最后一天

你能穿过矩阵的最后一天

难度:

标签:

题目描述

给你一个下标从 1 开始的二进制矩阵,其中 0 表示陆地,1 表示水域。同时给你 row 和 col 分别表示矩阵中行和列的数目。

一开始在第 0 天,整个 矩阵都是 陆地 。但每一天都会有一块新陆地被  淹没变成水域。给你一个下标从 1 开始的二维数组 cells ,其中 cells[i] = [ri, ci] 表示在第 i 天,第 ri 行 ci 列(下标都是从 1 开始)的陆地会变成 水域 (也就是 0 变成 1 )。

你想知道从矩阵最 上面 一行走到最 下面 一行,且只经过陆地格子的 最后一天 是哪一天。你可以从最上面一行的 任意 格子出发,到达最下面一行的 任意 格子。你只能沿着 四个 基本方向移动(也就是上下左右)。

请返回只经过陆地格子能从最 上面 一行走到最 下面 一行的 最后一天 。

 

示例 1:

输入:row = 2, col = 2, cells = [[1,1],[2,1],[1,2],[2,2]]
输出:2
解释:上图描述了矩阵从第 0 天开始是如何变化的。
可以从最上面一行到最下面一行的最后一天是第 2 天。

示例 2:

输入:row = 2, col = 2, cells = [[1,1],[1,2],[2,1],[2,2]]
输出:1
解释:上图描述了矩阵从第 0 天开始是如何变化的。
可以从最上面一行到最下面一行的最后一天是第 1 天。

示例 3:

输入:row = 3, col = 3, cells = [[1,2],[2,1],[3,3],[2,2],[1,1],[1,3],[2,3],[3,2],[3,1]]
输出:3
解释:上图描述了矩阵从第 0 天开始是如何变化的。
可以从最上面一行到最下面一行的最后一天是第 3 天。

 

提示:

  • 2 <= row, col <= 2 * 104
  • 4 <= row * col <= 2 * 104
  • cells.length == row * col
  • 1 <= ri <= row
  • 1 <= ci <= col
  • cells 中的所有格子坐标都是 唯一 的。

代码结果

运行时间: 126 ms, 内存: 24.4 MB


/*
思路:
1. 初始化一个row x col的矩阵,初始全为0。
2. 依次将cells中的位置变为1,记录每一天的变化。
3. 使用二分查找找到能够从上到下通过陆地的最后一天。
4. 使用流处理来代替循环和条件判断。
*/

import java.util.Arrays;
import java.util.stream.IntStream;

public class Solution {
    private int[] directions = {-1, 0, 1, 0, -1};
    
    public int latestDayToCross(int row, int col, int[][] cells) {
        return IntStream.rangeClosed(1, cells.length)
                .reduce((left, right) -> {
                    int mid = right - (right - left) / 2;
                    return canWalk(row, col, cells, mid) ? mid : mid - 1;
                }).getAsInt();
    }

    private boolean canWalk(int row, int col, int[][] cells, int day) {
        int[][] grid = new int[row][col];
        Arrays.stream(cells, 0, day).forEach(cell -> grid[cell[0] - 1][cell[1] - 1] = 1);
        return IntStream.range(0, col).anyMatch(i -> grid[0][i] == 0 && dfs(grid, 0, i));
    }

    private boolean dfs(int[][] grid, int x, int y) {
        if (x == grid.length - 1) return true;
        grid[x][y] = 1;
        return IntStream.range(0, 4)
                .mapToObj(i -> new int[]{x + directions[i], y + directions[i + 1]})
                .filter(nxNy -> nxNy[0] >= 0 && nxNy[1] >= 0 && nxNy[0] < grid.length && nxNy[1] < grid[0].length && grid[nxNy[0]][nxNy[1]] == 0)
                .anyMatch(nxNy -> dfs(grid, nxNy[0], nxNy[1]));
    }
}

解释

方法:

该题解采用深度优先搜索(DFS)来检测从最上面一行到最下面一行是否存在一条完全由陆地构成的路径。该方法逐天模拟每一块陆地被水淹没的过程。对于每一天淹没的陆地,算法检查该陆地的相邻格子是否已经访问过(即已经被淹没并验证过)。如果相邻的格子已被访问,算法会尝试从当前格子开始执行DFS,探索是否能够从顶部到达底部。使用两个集合管理状态:'visited' 存储已经被探索过的陆地格子,'wait' 存储等待探索的陆地格子。

时间复杂度:

O(n^2)

空间复杂度:

O(n)

代码细节讲解

🦆
在实现DFS时,为什么选择移除等待集合中的元素并添加到已访问集合,在算法上这样的处理有什么好处?
在DFS实现中,移除等待集合中的元素并添加到已访问集合是为了防止重复访问同一个格子,这样可以避免无限循环和不必要的重复计算。这种处理方式有助于降低时间复杂度,使算法更加高效。此外,它还可以帮助算法正确地追踪已探索和未探索的格子,确保每个格子在适当的时间被访问,从而维护算法的正确性。
🦆
为什么在陆地变为水域时,只有当变为水域的格子在第一列或相邻格子已被访问过时才尝试进行DFS?这样的条件判断对算法效率有何影响?
当陆地变为水域的格子位于第一列时,这意味着有可能形成从顶部到底部的直接路径,因此必须检查是否可以通过这个新变化的格子找到一条路径。如果相邻格子已经被访问过,这意味着从其他路径可能已经接近底部,因此需要重新检查从当前格子开始是否能到达底部。这种条件判断有助于减少不必要的DFS调用,因为只在可能形成路径的情况下才进行深入探索,从而提高算法的效率。
🦆
DFS函数中,检查到达最右边列即认为成功从顶部到底部,但题目要求是从最上面一行到最下面一行,是否存在逻辑错误?
确实存在逻辑错误。在题解中提到的检查到达最右边列来认定成功路径的逻辑是不正确的。正确的逻辑应该是检查是否到达最下面一行。这个错误可能是由于对问题描述的误解造成的。在实际实现中,应当修正这部分逻辑,确保只有当DFS到达矩阵的最下面一行时,才判断为成功找到从顶部到底部的路径。
🦆
在题解中提到每次淹没一块陆地后,DFS可能会被触发,在最坏情况下,每次淹没可能触发对剩余所有陆地的完整探索,那么这种方法在处理大规模数据时的性能如何?
在最坏情况下,每次淹没一块陆地都可能触发一次完整的DFS探索,这会导致算法的性能显著下降,尤其是在大规模数据集上。因为每次淹没都可能需要探索整个矩阵的剩余部分,这样的重复探索会导致时间复杂度增加,可能达到O(n*m)的复杂度,其中n和m分别是矩阵的行数和列数。对于大规模数据集,这种方法可能不够高效,并且可能需要考虑更优化的算法,如并查集等,以改善性能。

相关问题