2. Leetcode算法笔记 - 太平洋大西洋水流问题,深度优先搜索

依然是算法笔记,这次记录下Leetcode上个星期三(4月27日)太平洋大西洋水流问题,基于官方题解整理的算法笔记,使用的是深度优先遍历,昨天终于出了一道困难题(第一反应:完蛋,连续打卡要断了),但是那题其实注意下细节,还是能做出来的,例如说<A></A><B></B>是不合法的,虽然在html中似乎是有效的,那是因为浏览器已经帮我们加上了<html></html>所以有效了。

(English version translate by GPT-3.5)

#原题

原题链接: 417. 太平洋大西洋水流问题 - 力扣

1
2
3
4
5
6
7
8
9
10
11
12
13
有一个 m × n 的矩形岛屿,与 太平洋 和 大西洋 相邻。 “太平洋” 处于大陆的左边界和上边界,而 “大西洋” 处于大陆的右边界和下边界。

这个岛被分割成一个由若干方形单元格组成的网格。给定一个 m x n 的整数矩阵 heights , heights[r][c] 表示坐标 (r, c) 上单元格 高于海平面的高度 。

岛上雨水较多,如果相邻单元格的高度 小于或等于 当前单元格的高度,雨水可以直接向北、南、东、西流向相邻单元格。水可以从海洋附近的任何单元格流入海洋。

返回网格坐标 result 的 2D 列表 ,其中 result[i] = [ri, ci] 表示雨水从单元格 (ri, ci) 流动 既可流向太平洋也可流向大西洋 。

输入: 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]]

输入: heights = [[2,1],[1,2]]
输出: [[0,0],[0,1],[1,0],[1,1]]

image-20220503110410488

深入理解

这题当时刚开始做的时候没啥思路(我发现我涉及深度遍历就犯晕,看到那转来转去就晕了),但是万事就缺思路,有了思路,这题就会一半了。这题作为中等题,要求求出同时往太平洋和大西洋流出的水流,其实可以这么算(官方题解思路),先算出太平洋流向大西洋的水流,然后算出大西洋流向太平洋的水流,最后对2个数组求交集就是最终结果。分解思路:

  1. 首先就是得定义2个数组,用来存储太平洋流向大西洋,以及大西洋流向太平洋的水流方块
  2. 然后构建4个循环,分别递归计算太平洋横向流向大西洋的水流,然后计算太平洋纵向流向大西洋的水流,以及大西洋流向太平洋横纵向的水流
  3. 最后合并2个数组中存在共同流向的部分
  4. 在递归中,不断判断并递归4个方向(上下左右)的“山”是否能够流向对面,同时判断好这几个方向的岩石是否更高,如果更高,递归这个更高的岩石能够流向哪里。

编码开始

  1. 首先就是定义几个变量,分别是记录行,列的,以及它高度的变量

    1
    2
    3
    4
    5
    6
    // 定义数组行,即【】,【】,【】
    private int row;
    // 定义数组列,即【【....】】,【[....]】
    private int col;
    // 暂存高度值,备用
    private int[][] heights;
  2. 然后,定义2个数组,用来记录太平洋流向大西洋,大西洋流向太平洋的水流

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 赋值
    this.row = heights.length;
    this.col = heights[0].length;
    this.heights = heights;

    // 记录太平洋流向大西洋水流的数组
    boolean[][] pacificOceanToAtlantic = new boolean[row][col];
    // 记录大西洋流向太平洋水流的数组
    boolean[][] atlanticOceanToPacific = new boolean[row][col];
  3. 构建4个循环,分别从行列出发来递归出太平洋,大西洋往对方流向的水流

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 从行出发,固定住列,求出行方向流向大西洋的水流
    for (int i = 0; i < row; i++) {
    dfs(i, 0, pacificOceanToAtlantic);
    }
    // 从列出发,固定住行,求出列方向流向大西洋的水流
    for (int i = 0; i < col; i++) {
    dfs(0, i, pacificOceanToAtlantic);
    }
    // 然后从行出发,固定住列,求出大西洋流向太平洋的水流,因为从右下角开始遍历,所以col - 1
    for (int i = 0; i < row; i++) {
    dfs(i, col - 1, atlanticOceanToPacific);
    }
    // 然后从列出发,固定住行,求出大西洋流向太平洋的水流,因为从右下角开始遍历,所以row - 1
    for (int i = 0; i < col - 1; i++) {
    dfs(row - 1, i, atlanticOceanToPacific);
    }
  4. 然后合并得到结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 遍历数组的所有元素,做交集,输出2个数组中共同的值
    List<List<Integer>> ans = new ArrayList<>();
    for (int i = 0; i < pacificOceanToAtlantic.length; i++) {
    for (int j = 0; j < pacificOceanToAtlantic[i].length; j++) {
    // 取得交集
    if(pacificOceanToAtlantic[i][j] && atlanticOceanToPacific[i][j]) {
    ans.add(Arrays.asList(i, j));
    }
    }
    }
    return ans;

    合并效果

  5. 最后写出递归相关代码,这里需要定义个值,方向值,当然,这个其实可以移动到成员变量上去

    1
    2
    // 4个方向,上下左右
    int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
  6. 然后在各个方向上判断水流方向

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 如果这块高度被之前判断过了,直接跳过
    if(oceans[row][col]) {
    return;
    }
    // 然后将这块标记为true,即表示这块肯定被流过
    oceans[row][col] = true;
    for (int[] direction : directions) {
    // 计算新的方向,循环4次方位,得到新的方向
    int newRow = row + direction[0];
    int newCol = col + direction[1];
    // 判断这个方向是否比当前高度更高,如果是,则去这个方向进行遍历(因为如果目标方向比当前更高,表示当前的水流一定是从目标方向流过来的)
    // 当然新的方向如果和当前方向高度相同,只是表示水流会互流(水可能从旁边流过来,也可能从这个高度流过去),也就是当在新的高度判断时,当判断到当前高度
    // 与新的高度相同,也会回到当前高度重新判断,但是由于当前高度已经被标记为True,那么它会被直接return掉
    if(newRow >= 0 && newRow < this.row && newCol >= 0 && newCol < this.col && this.heights[newRow][newCol] >= this.heights[row][col]) {
    dfs(newRow, newCol, oceans);
    }
    }

模拟过程

附上完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class Solution {
// 定义数组行,即【】,【】,【】
private int row;
// 定义数组列,即【【....】】,【[....]】
private int col;
// 暂存高度值,备用
private int[][] heights;
// 4个方向,上下左右
int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

public List<List<Integer>> pacificAtlantic(int[][] heights) {
// 赋值
this.row = heights.length;
this.col = heights[0].length;
this.heights = heights;

// 记录太平洋流向大西洋水流的数组
boolean[][] pacificOceanToAtlantic = new boolean[row][col];
// 记录大西洋流向太平洋水流的数组
boolean[][] atlanticOceanToPacific = new boolean[row][col];

// 从行出发,固定住列,求出行方向流向大西洋的水流
for (int i = 0; i < row; i++) {
dfs(i, 0, pacificOceanToAtlantic);
}
// 从列出发,固定住行,求出列方向流向大西洋的水流
for (int i = 0; i < col; i++) {
dfs(0, i, pacificOceanToAtlantic);
}
// 然后从行出发,固定住列,求出大西洋流向太平洋的水流,因为从右下角开始遍历,所以col - 1
for (int i = 0; i < row; i++) {
dfs(i, col - 1, atlanticOceanToPacific);
}
// 然后从列出发,固定住行,求出大西洋流向太平洋的水流,因为从右下角开始遍历,所以row - 1
for (int i = 0; i < col - 1; i++) {
dfs(row - 1, i, atlanticOceanToPacific);
}
// 遍历数组的所有元素,做交集,输出2个数组中共同的值
List<List<Integer>> ans = new ArrayList<>();
for (int i = 0; i < pacificOceanToAtlantic.length; i++) {
for (int j = 0; j < pacificOceanToAtlantic[i].length; j++) {
// 取得交集
if(pacificOceanToAtlantic[i][j] && atlanticOceanToPacific[i][j]) {
ans.add(Arrays.asList(i, j));
}
}
}
return ans;
}

private void dfs(int row, int col, boolean[][] oceans) {
// 如果这块高度被之前判断过了,直接跳过
if(oceans[row][col]) {
return;
}
// 然后将这块标记为true,即表示这块肯定被流过
oceans[row][col] = true;
for (int[] direction : directions) {
// 计算新的方向,循环4次方位,得到新的方向
int newRow = row + direction[0];
int newCol = col + direction[1];
// 判断这个方向是否比当前高度更高,如果是,则去这个方向进行遍历(因为如果目标方向比当前更高,表示当前的水流一定是从目标方向流过来的)
// 当然新的方向如果和当前方向高度相同,只是表示水流会互流(水可能从旁边流过来,也可能从这个高度流过去),也就是当在新的高度判断时,当判断到当前高度
// 与新的高度相同,也会回到当前高度重新判断,但是由于当前高度已经被标记为True,那么它会被直接return掉
if(newRow >= 0 && newRow < this.row && newCol >= 0 && newCol < this.col && this.heights[newRow][newCol] >= this.heights[row][col]) {
dfs(newRow, newCol, oceans);
}
}
}
}