太平洋大西洋水流问题(力扣第417题)
题目:
给定一个 m x n 的非负整数矩阵来表示一片大陆上各个单元格的高度。“太平洋”处于大陆的左边界和上边界,而“大西洋”处于大陆的右边界和下边界。规定水流只能按照上、下、左、右四个方向流动,且只能从高到低或者在同等高度上流动。请找出那些水流既可以流动到“太平洋”,又能流动到“大西洋”的陆地单元的坐标。
提示:
输出坐标的顺序不重要
m 和 n 都小于150
示例:
给定下面的 5x5 矩阵: 太平洋 ~ ~ ~ ~ ~ ~ 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]] (上图中带括号的单元).
分析:
由题意可知,水流可以走四个方向:上下左右;驱动水流动的条件是当前陆地单元的高度大于等于下一步要流向的陆地单元的高度;其中还有两个比较特殊的陆地单元,那就是位于两个海洋交界位置的点,左下角和右上角的陆地单元,它们直接毗邻两个海洋,所以可以直接流向这两个海洋。
那么还是可以通过深度优先遍历的方法来解决,只不过这里不再需要从一个点一直遍历一个完整的连通分量,而是只要能够确认这个点可达两个海洋即可停止深度优先遍历。
其中,遍历的过程,当遇到(0,j)或者(i,0)这些点时,那么就说明可达太平洋;当遇到(m-1,j)或者(i,n-1)这些点时,说明可达大西洋。这里我们借助于DFS的非递归遍历,设置一个访问标记数组,用于标记从当前给定的点出发时,矩阵中的点是否已被访问,然后设置两个标记变量,分别用于标记大西洋和太平洋是否可达,如果二者皆可达,那么说明其就是符合要求的坐标,否则不是。
代码:
private int m; private int n; public List<List<Integer>> pacificAtlantic(int[][] matrix) { if (matrix == null || matrix.length==0 || matrix[0].length==0){ return new ArrayList<>(); } m = matrix.length; n = matrix[0].length; ArrayList<List<Integer>> reslist = new ArrayList<>(); List<Integer> midres; for (int i = 0; i < m; i++) { for (int i1 = 0; i1 < n; i1++) { if (flowDFS(matrix,i,i1)){ midres = new ArrayList<>(); midres.add(i); midres.add(i1); reslist.add(midres); } } } return reslist; } private boolean flowDFS(int[][] matrix,int i,int j){ if ((i == 0 && j == n-1) || (i == m-1 && j == 0)){ return true; } boolean[][] isVisited = new boolean[m][n]; int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}}; Stack<Pair<Integer, Integer>> stack = new Stack<>(); stack.push(new Pair<>(i,j)); isVisited[i][j] = true; int flag = 0; int flag_west = 0; while (!stack.isEmpty()){ Pair<Integer, Integer> u = stack.peek(); int cur_i = u.getKey(); int cur_j = u.getValue(); if (cur_i == 0 || cur_j==0){ flag = 1; } if (cur_i == m-1 || cur_j == n-1){ flag_west = 1; } int count = 0; for (int[] dir : dirs) { int next_i = dir[0] + cur_i; int next_j = dir[1] + cur_j; if (next_i < 0 || next_i >= m || next_j < 0 || next_j >= n){ continue; } if (matrix[cur_i][cur_j] >= matrix[next_i][next_j] && !isVisited[next_i][next_j]){ isVisited[next_i][next_j] = true; if (next_i == 0 || next_j==0){ flag = 1; }else if (next_i == m || next_j == n-1){ flag_west = 1; } stack.push(new Pair<>(next_i,next_j)); count = 1; break; } } if (flag == 1 && flag_west == 1){ return true; } if (count == 0){ stack.pop(); } } return false; }
程序虽然通过了,但是性能十分的不好,这里我也参考了cyc2018大神的解法,我感觉很有借鉴意义,那么在这里记录一下大神的解法思想:
它主要是设置两个访问标记数组,分别用于记录可以到达大西洋和太平洋的陆地单元,它们是布尔类型的,只要对应位置的元素值为true时,那么就说明这个陆地单元可达相应的“海洋”;而是如何进行遍历的呢?我们来分析,首先一个陆地单元如果可达某个海洋,那么”沿海“的陆地单元也必然”反向“可达该陆地单元(反向可达的意思是从低的位置流向高的位置),所以,就可以从”沿海“的陆地单元出发,进行DFS遍历,此时我们深度优先遍历的结果要求不能中断,要竭尽所有可以到达的点,那么最终得到的连通分量中的点都是可以到达起始点所挨着的那个”海洋的“,所以,我们就可以只遍历边界的点了,而不要每个点都进行一个DFS点遍历然后判断是否可达两个海洋。
我们分别从两个海洋的各自拥有的两个沿海边界出发,凡是遍历到的点,就在对应的访问标记数组对应的元素值设置为true,最终两个标记数组相同点都为true的陆地单元,就是所求的坐标。
代码如下:
private int m, n; private int[][] matrix; private int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; public List<List<Integer>> pacificAtlantic(int[][] matrix) { List<List<Integer>> ret = new ArrayList<>(); if (matrix == null || matrix.length == 0) { return ret; } m = matrix.length; n = matrix[0].length; this.matrix = matrix; boolean[][] canReachP = new boolean[m][n]; boolean[][] canReachA = new boolean[m][n]; for (int i = 0; i < m; i++) { dfs(i, 0, canReachP); dfs(i, n - 1, canReachA); } for (int i = 0; i < n; i++) { dfs(0, i, canReachP); dfs(m - 1, i, canReachA); } for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (canReachP[i][j] && canReachA[i][j]) { ret.add(Arrays.asList(i, j)); } } } return ret; } private void dfs(int r, int c, boolean[][] canReach) { if (canReach[r][c]) { return; } canReach[r][c] = true; for (int[] d : direction) { int nextR = d[0] + r; int nextC = d[1] + c; if (nextR < 0 || nextR >= m || nextC < 0 || nextC >= n || matrix[r][c] > matrix[nextR][nextC]) { continue; } dfs(nextR, nextC, canReach); } }