[ 多源dfs ]417. 太平洋大西洋水流问题

207 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第27天,点击查看活动详情

每日刷题 2021.04.27

题目

  • 有一个 m × n 的矩形岛屿,与 太平洋 和 大西洋 相邻。 “太平洋” 处于大陆的左边界和上边界,而 “大西洋” 处于大陆的右边界和下边界。
  • 这个岛被分割成一个由若干方形单元格组成的网格。给定一个 m x n 的整数矩阵 heights , heights[r][c] 表示坐标 (r, c) 上单元格 高于海平面的高度 。
  • 岛上雨水较多,如果相邻单元格的高度 小于或等于 当前单元格的高度,雨水可以直接向北、南、东、西流向相邻单元格。水可以从海洋附近的任何单元格流入海洋。
  • 返回网格坐标 result 的 2D 列表 ,其中 result[i] = [ri, ci] 表示雨水从单元格 (ri, ci) 流动 既可流向太平洋也可流向大西洋 。

示例

  • 示例1 image.png
输入: heights = [[1,2,2,3,5],[3,2,3,4,4],[2,4,5,3,1],[6,7,1,4,5],[5,1,1,2,4]]
输出: [[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]]
  • 示例2
输入: heights = [[2,1],[1,2]]
输出: [[0,0],[0,1],[1,0],[1,1]]

提示

m == heights.length
n == heights[r].length
1 <= m, n <= 200
0 <= heights[r][c] <= 10^5

解题思路

  • 分析题目含义: 需要查找能够到达两个洋的节点(分别能够到达两个大洋的相交节点)

最开始的思路

  • dfs常规题,就是板子题。
  • 主要的问题在:如何记录:节点流入大洋中,位于当前的最后一个节点,如何将这一路可以流下来的节点记录下来??
  • 现在看来,其实可以通过判断:当前节点的横纵坐标来判断其是否可以流入大洋,并且判断流入的是哪个大洋。

思路错误分析

  • 误以为:只能从左上角和右下角的两个节点往回进行遍历,所能到的节点就是答案。(❌错误一直没有发现的原因,根本没有动手实践自己的想法是否正确)=> 即:同时能够到达两个大洋的节点

正确的思路

  • m = lenR ,n = lenC
  • 正向思维:dfs遍历heights中的每一个元素,将每一个遍历的节点存储在栈中。如果路径最后找到可以入洋的节点,那么就将栈中存储的所有的节点加入到结果数组中;如果最后一个节点不能入洋,那么就从栈中将其删除,返回上一层继续查找。
    • 还存在一种情况:就是如果当前节点是入洋的节点,并且已经被其他节点标记访问过,那么就需要额外的处理。可以声明一个入洋节点的数组,判断是否为入洋节点。
  • 逆向思维:遍历能够流入太平洋和大西洋的节点,倒着推能够流入当前节点的节点。**从入洋的节点开始遍历,可以实现对每条回溯路径的查找,反过来的话,可能会出一些问题,比如入洋节点已经被其他路径遍历过,visited已经为true,那么下一次其他节点再遍历有可能会误以为不能入洋。**因此正向思维比较复杂,可以逆转思维想想。
    • 能够流入太平洋的节点:x == 0 && y [0 ~ lenC - 1] || y == 0 && x [0 ~ lenC - 1]
    • 能够流入大西洋的节点:y == lenC - 1 && x [0 ~ lenR - 1] || x == lenR - 1 && y [0 ~ lenC - 1]
    • 最后取能够流入太平洋的节点 与 能够流入大西洋的节点的交集。

AC代码

var pacificAtlantic = function(heights) {
  // 对于每一个节点,需要先判断:其自身是否可以从两个方向流出,如果可以的话,那么就记录下节点坐标,或者使用标记数组,最后循环输出
  // 然后再去遍历,当前这个节点可以走的下一个节点
  // 一次循环就可以,直到下标不符合就返回
  // dfs, 多源dfs
  // 书写dfs三步骤:
  // 1.临界条件和参数:下标的范围、无返回值、参数传递当前可走的元素
  // 2.单层的循环:
  let lenR = heights.length,lenC = heights[0].length;
  let road = new Array(lenR).fill(0).map(() => new Array(lenC).fill(0));
  let visited = new Array(lenR).fill(false).map(() => new Array(lenC).fill(false));
  // 可能倒着想,就不会存在这种问题了,倒着想只需要考虑能走通,就可以打标记
  let nx = [-1,0,1,0],ny = [0,1,0,-1];
  function dfs (x, y) {
    // 判断当前的元素是否可以同时流向太平洋和大西洋,判断条件:
    // 应该将其路径上走过的所有符合的节点,全部都标记出来
    // 也就是说,当找到出口的时候,当前这条路全部标记,可以使用数组来处理
    for(let i = 0,j = 0;i < 4; i++,j++) {
        let newX = x + nx[i];
        let newY = y + ny[j];
        // 往旁边流
        if(newX >= 0 && newX < lenR && newY >= 0 && newY < lenC && !visited[newX][newY] && heights[x][y] <= heights[newX][newY]){
          road[newX][newY]++;
          visited[newX][newY] = true;
          dfs(newX, newY);
        }
    }
  }
  // 搜两遍,重复的输出即可
  for(let j = 0; j < lenC; j++) {
    if(!visited[0][j]){
      visited[0][j] = true;
      road[0][j]++;
      dfs(0, j);
    }
  }
  for(let i = 0; i < lenR; i++) {
    if(!visited[i][0]){
      visited[i][0] = true;
      road[i][0]++;
      dfs(i, 0);
    }
  }
  visited = new Array(lenR).fill(false).map(() => new Array(lenC).fill(false));
  for(let j = 0; j < lenC; j++) {
    if(!visited[lenR - 1][j]){
    visited[lenR - 1][j] = true;
    road[lenR - 1][j]++;
    dfs(lenR - 1, j);
    }
  }
  for(let i = 0; i < lenR; i++) {
    if(!visited[i][lenC - 1]){
    visited[i][lenC - 1] = true;
    road[i][lenC - 1]++;
    dfs(i, lenC - 1);
    }
  }
  let res = [];
  for(let i = 0;i < lenR; i++) {
    for(let j = 0; j < lenC; j++) {
      if(road[i][j] == 2) res.push([i,j]);
    }
  }
  return res;
};

总结

  • 切记眼高手低,所有的样例都需要能够通过自己所想的方法验证一下,答案是否一样,再开始上手写代码。