题目:https://leetcode.com/problems/pacific-atlantic-water-flow/?tab=Description
这是一道很好的练习bfs和dfs的题目。
题目给出了一个矩阵,矩阵数字代表山地的高度,水流只能从高处流到地处或者至少相等。找出所有的坐标,从这个坐标可以到达左边界或者上边界中的一个并且也可以到达右边界或者下边界中的一个。
刚开始做这个题目,最直接的思路就是对每一个坐标进行遍历,看是否可以到达两个边界。
代码如下:
public List<int[]> pacificAtlantic(int[][] matrix) {
List<int[]> r = new ArrayList<>();
if(matrix == null || matrix.length == 0) return r;
for(int i = 0; i < matrix.length; i++){
for(int j = 0; j < matrix[0].length; j++){
int[] sr = canReach(matrix, i, j);
if(sr[0] == 1 && sr[1] == 1)
r.add(new int[]{i, j});
}
}
return r;
}
private int[] canReach(int[][] matrix, int x, int y){
int[] r = new int[2];
if(x == 0 || y == 0) r[0] = 1;
if(x == matrix.length - 1 || y == matrix[0].length - 1) r[1] = 1;
int h = matrix[x][y];
matrix[x][y] = Integer.MAX_VALUE;
List<int[]> sub = new ArrayList<>();
if(x - 1 >= 0 && matrix[x - 1][y] <= h)
sub.add(canReach(matrix, x - 1, y));
if(x + 1 < matrix.length && matrix[x + 1][y] <= h)
sub.add(canReach(matrix, x + 1, y));
if(y - 1 >= 0 && matrix[x][y - 1] <= h)
sub.add(canReach(matrix, x, y - 1));
if(y + 1 < matrix[0].length && matrix[x][y + 1] <= h)
sub.add(canReach(matrix, x, y + 1));
for(int[] sr : sub){
if(sr[0] == 1)
r[0] = 1;
if(sr[1] == 1)
r[1] = 1;
}
matrix[x][y] = h;
return r;
}
遍历的子函数返回结果,因为有两个结果,所以返回了数组。最后检测每一个位置的遍历结果,如果都是1说明两种边界在这个位置可以被到达。这个算法的复杂度是
O(mn*(m+n))。
提交以后发现超时。。。
后来发现了一个更牛逼的算法。要找能够同时到达两个边界的所有坐标,那么转换一下思维,那不就是找从两个边界出发能到达目的地的交集吗?每一个边界有m+n个坐标,因此复杂度为O((m+n)*(m+n))。确实叼。
代码:
public List<int[]> pacificAtlantic(int[][] matrix) {
List<int[]> r = new ArrayList<>();
if(matrix == null || matrix.length == 0) return r;
int m = matrix.length, n = matrix[0].length;
int[][] pacific = new int[m][n];
int[][] atlantic = new int[m][n];
int[][] dir = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
for(int i = 0; i < m; i++){
dfs(dir, matrix, pacific, i, 0, Integer.MIN_VALUE);
dfs(dir, matrix, atlantic, i, n - 1, Integer.MIN_VALUE);
}
for(int i = 0; i < n; i++){
dfs(dir, matrix, pacific, 0, i, Integer.MIN_VALUE);
dfs(dir, matrix, atlantic, m - 1, i, Integer.MIN_VALUE);
}
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(pacific[i][j] == 1 && atlantic[i][j] == 1)
r.add(new int[]{i, j});
}
}
return r;
}
dfs:
private void dfs(int[][] dir, int[][] matrix, int[][] visited, int x, int y, int preh){
if(x < 0 || x >= matrix.length || y < 0 || y >= matrix[0].length || visited[x][y] == 1 || matrix[x][y] < preh) return;
visited[x][y] = 1;
for(int[] d : dir){
dfs(dir, matrix, visited, d[0] + x, d[1] + y, matrix[x][y]);
}
}
bfs:
private void bfs(int[][] dir, int[][] matrix, int[][] visited, int x, int y, int preh){
Queue<int[]> queue = new LinkedList<>();
queue.add(new int[]{x, y});
while(!queue.isEmpty()){
int[] pos = queue.poll();
visited[pos[0]][pos[1]] = 1;
for(int[] d : dir){
int xx = pos[0] + d[0];
int yy = pos[1] + d[1];
if(0 <= xx && xx < matrix.length && 0 <= yy && yy < matrix[0].length && visited[xx][yy] != 1 && matrix[xx][yy] >= matrix[pos[0]][pos[1]])
queue.add(new int[]{xx, yy});
}
}
}
这里可以dfs也可以bfs,作为练习,我全写了一遍。但是bfs部分还是超时,可能需要略微加工一下,比如一开始就把一列的或者一排的起始点全加入queue中,由于时间关系没有继续。
小结:
(1)对于可达性问题,尤其是找出所有坐标的问题,逆向思维是不错的选择,就是从目的地开始遍历,所到达的点就是全部解。
(2)dfs的特征就是递归;bfs的特征是使用一个queue。
(3)回溯法和dfs或者bfs的区别,个人感觉回溯法是dfs的加强版,也就是在dfs的基础上加入了path路径,即之前走过的路径。从含义上讲,dfs一般应用于遍历,而回溯是解决问题的每一步,有很多选择。
(4)这里的dfs的写法很简洁,使用了一个方向数组,这样在方向上可以用for处理,而不用一一罗列出来。还有一点,关于dfs的pos合法性检测,有两种,第一种是在return case里面,那么递归调用处就不用管是否越界之类的,直接调用即可;另一种是在调用处检测,return case简单些。但是只要是在调用处出现了数组之类的检测(其他的要求,比如这里的高度),那么就必须先进行越界检测,这样不如把其他约束也一起放在return case里面,写起来更加简洁。