来自灵神网格图题单。
1. 网格图
1.1. LC 200 岛屿数量
这题我一开始想繁了,想维护并查集,然后看等价类个数。其实完全没有必要。因为连通分量深搜到头就可以直接给答案计数+1。利用vis数组维护访问过的点,然后碰到新连通分量重新深搜即可。因为有vis数组,所以每个节点至多访问一次,即O(nm)的复杂度。
import java.util.Arrays;
class Solution {
boolean[] vis;
int m,n;
int[][] direction = new int[][]{
{-1,0},
{0,1},
{1,0},
{0,-1}
};
public int numIslands(char[][] grid) {
m = grid.length;
n = grid[0].length;
vis = new boolean[m*n];
int ans = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if(grid[i][j]=='1'&&!vis[i*n+j]){
dfs(grid,i,j);
ans++;
}
}
}
return ans;
}
private void dfs(char[][] grid,int x,int y){
if(vis[x*n+y]){
return;
}
vis[x*n+y] = true;
int nx,ny;
for (int[] dir : direction) {
nx = x+dir[0];
ny = y+dir[1];
if(indexValid(nx,ny)&&grid[nx][ny]=='1'){
dfs(grid,nx,ny);
}
}
}
private boolean indexValid(int x,int y){
return x>=0 && x<m && y>=0 && y<n;
}
}
1.2. LC 695 岛屿的最大面积
这题和LC 200其实一样的。前者统计连通分量个数,这个统计最大连通分量元素个数。还是维护vis防止走重复,遇到新连通分支就统计这个连通分支元素数量,维护最大值就行。
class Solution {
boolean[] vis;
int m,n;
int[][] directions = new int[][]{
{-1,0},
{0,1},
{1,0},
{0,-1}
};
public int maxAreaOfIsland(int[][] grid) {
m = grid.length;
n = grid[0].length;
vis = new boolean[n*m];
int max = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if(!vis[i*n+j] && grid[i][j]==1){
max = Math.max(max,dfs(grid,i,j));
}
}
}
return max;
}
private int dfs(int[][] g,int x,int y){
if(vis[x*n+y]){
return 0;
}
vis[x*n+y] = true;
int nx,ny;
int cnt = 0;
for (int[] direction : directions) {
nx = x+direction[0];
ny = y+direction[1];
if(indexValid(nx,ny) && g[nx][ny]==1){
cnt += dfs(g,nx,ny);
}
}
return cnt+1;
}
private boolean indexValid(int x,int y){
return x>=0 && x<m && y>=0 && y<n;
}
}
1.3. LC 面试题16.19. 水域大小
和LC695差不多。维护每个连通分量大小,排个序返回就行。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
boolean[] vis;
int m,n;
int[][] dirs = new int[][]{
{-1,0},
{0,1},
{1,0},
{0,-1},
{-1,-1},
{-1,1},
{1,-1},
{1,1}
};
public int[] pondSizes(int[][] land) {
m = land.length;
n = land[0].length;
vis = new boolean[m*n];
List<Integer> tmp = new ArrayList<>();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if(!vis[i*n+j]&&land[i][j]==0){
tmp.add(dfs(land,i,j));
}
}
}
int[] ans = new int[tmp.size()];
for (int i = 0; i < tmp.size(); i++) {
ans[i] = tmp.get(i);
}
Arrays.sort(ans);
return ans;
}
private int dfs(int[][] l,int x,int y){
if(vis[x*n+y]){
return 0;
}
vis[x*n+y]=true;
int cnt = 0;
int nx,ny;
for (int[] dir : dirs) {
nx = x+dir[0];
ny = y+dir[1];
if(indexValid(nx,ny)&&l[nx][ny]==0){
cnt += dfs(l,nx,ny);
}
}
return cnt+1;
}
private boolean indexValid(int x,int y){
return x>=0 && x<m && y>=0 && y<n;
}
}
1.4. LC 463 岛屿的周长
和200/695/16.19差不多,就是查一个连通分量。记得重复边不仅没有增量反而-1。另外由于题目明确说了就一个连通分量,因此查到一个后可以直接结束了。
class Solution {
boolean[] vis;
int m,n;
int[][] dirs = new int[][]{
{-1,0},
{0,1},
{1,0},
{0,-1}
};
public int islandPerimeter(int[][] grid) {
m = grid.length;
n = grid[0].length;
vis = new boolean[m*n];
int ans = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if(!vis[i*n+j]&&grid[i][j]==1){
ans = dfs(grid,i,j);
break;
}
}
}
return ans;
}
private int dfs(int[][] g,int x,int y){
if(vis[x*n+y]){
return 0;
}
vis[x*n+y]=true;
int cnt = 4;
int nx,ny;
for (int[] dir : dirs) {
nx = x+dir[0];
ny = y+dir[1];
if(indexValid(nx,ny)&&g[nx][ny]==1){
cnt+=dfs(g,nx,ny)-1;
}
}
return cnt;
}
private boolean indexValid(int x,int y){
return x>=0 && x<m && y>=0 && y<n;
}
}
1.5. LC 2658 网格图中鱼的最大数目
这题一开始读错题了,以为是最大化连通分量中的最大值。但实际上是最大化连通分量元素和。还是跟以前一样套路,分开深搜维护最大值即可。
class Solution {
boolean[] vis;
int m,n;
int[][] dirs = new int[][]{
{-1,0},
{0,1},
{1,0},
{0,-1}
};
public int findMaxFish(int[][] grid) {
m = grid.length;
n = grid[0].length;
vis = new boolean[m*n];
int max = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if(!vis[i*n+j]&&grid[i][j]>0){
max = Math.max(max,dfs(grid,i,j));
}
}
}
return max;
}
private int dfs(int[][] g,int x,int y){
if(vis[x*n+y]){
return 0;
}
vis[x*n+y]=true;
int nx,ny;
int cnt = g[x][y];
for (int[] dir : dirs) {
nx = x+dir[0];
ny = y+dir[1];
if(indexValid(nx,ny)&&g[nx][ny]>0){
cnt += dfs(g,nx,ny);
}
}
return cnt;
}
private boolean indexValid(int x,int y){
return x>=0 && x<m && y>=0 && y<n;
}
}
1.7. LC 1020 飞地的数量
这道题虽然A了但是思路比较差。我是直接深搜,然后打一个标记位置,深搜到边界给标记位置置true,然后深搜返回的是连通块的大小。如果标记位为false说明连通块没有到边界,这样就可以累加。
但是更好的思路是,直接从边界深搜,把能抵达的1全部标记出来,剩下没标记的都是要求的。
import java.util.Arrays;
class Solution {
boolean[][] vis;
int[][] directions = new int[][]{
{-1,0},
{0,1},
{1,0},
{0,-1}
};
int m,n;
boolean flag;
public int numEnclaves(int[][] grid) {
m = grid.length;
n = grid[0].length;
vis = new boolean[m][n];
int sum = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if(grid[i][j]==1&&!vis[i][j]){
flag = false;
int tmp = dfs(grid, i, j);
if(!flag){
sum+=tmp;
}
}
}
}
return sum;
}
private int dfs(int[][] grid,int x,int y){
if(vis[x][y]||grid[x][y]==0){
return 0;
}
vis[x][y] = true;
if(x==0||x==m-1||y==0||y==n-1){
flag = true;
}
int nx,ny,sum;
sum = 0;
for (int[] direction : directions) {
nx = x+direction[0];
ny = y+direction[1];
if(isValid(nx,ny)){
sum += dfs(grid,nx,ny);
}
}
return sum+1;
}
private boolean isValid(int x,int y){
return x>=0 && x<m && y>=0 &&y<n;
}
}
这个是我的。
class Solution {
public int numEnclaves(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
for(int i = 0; i < m; i ++){
if(grid[i][0] == 1) dfs(grid, i, 0, m, n);
if(grid[i][n - 1] == 1) dfs(grid, i, n - 1, m , n);
}
for(int i = 0; i < n; i ++){
if(grid[0][i] == 1) dfs(grid, 0, i, m, n);
if(grid[m - 1][i] == 1) dfs(grid, m - 1, i, m ,n);
}
int res = 0;
for(int i = 0; i < m; i ++){
for(int j = 0; j < n; j ++){
if(grid[i][j] == 1){
res ++;
}
}
}
return res;
}
public void dfs(int[][] grid, int i, int j, int m, int n)
{
if(i < 0 || j < 0 || i >= m || j >= n) return;
if(grid[i][j] == 0) return;
grid[i][j] = 0;
dfs(grid, i + 1, j, m, n);
dfs(grid, i - 1, j, m ,n);
dfs(grid, i, j + 1, m, n);
dfs(grid, i, j - 1, m, n);
}
}
更好的思路。
1.8. LC 1254 统计封闭岛屿的数目
可以这么想,如果一个0的连通块存在元素在边界上,那么就不是封闭岛屿,反过来就是。这样深搜的时候检查是否到达过边界。如果没有到达过的话增1就可以了。
import java.util.Arrays;
class Solution {
int m,n;
boolean[] vis;
int[][] directions = new int[][]{
{-1,0},
{0,1},
{1,0},
{0,-1}
};
public int closedIsland(int[][] grid) {
m = grid.length;
n = grid[0].length;
vis = new boolean[m*n];
Arrays.fill(vis,false);
int ans = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if(!vis[i*n+j]&&grid[i][j]!=1){
if(!dfs(grid,i,j)){
ans++;
}
}
}
}
return ans;
}
private boolean isOnEdge(int x,int y){
return x==0||y==0||x==m-1||y==n-1;
}
private boolean isValid(int x,int y){
return x>=0 && x<m && y>=0 && y<n;
}
private boolean dfs(int[][] grid,int x,int y){
if(grid[x][y]==1||vis[x*n+y]){
return false;
}
vis[x*n+y] = true;
int nx,ny;
boolean flag = isOnEdge(x,y);
for (int[] direction : directions) {
nx = x+direction[0];
ny = y+direction[1];
if(isValid(nx,ny)){
flag|=dfs(grid,nx,ny);
}
}
return flag;
}
}
1.9. LC 130 被围绕的区域
这题别和上题一样判断是否连通块有边界元素,因为判完再染之前深搜的可能漏染了。
可以这样,先从边界深搜,把边界上的O所在的连通块全访问掉。这样剩下的就是内部的了(被围绕的O),然后深搜内部染色就行。
class Solution {
int m,n;
boolean[][] vis;
int[][] directions = new int[][]{
{-1,0},
{0,1},
{1,0},
{0,-1}
};
public void solve(char[][] board) {
m = board.length;
n = board[0].length;
vis = new boolean[m][n];
for (int i = 0; i < n; i++) {
if(board[0][i]=='O'&&!vis[0][i]){
dfs(board,0,i,true);
}
}
for (int i = 0; i < n; i++) {
if(board[m-1][i]=='O'&&!vis[m-1][i]){
dfs(board,m-1,i,true);
}
}
for (int i = 0; i < m; i++) {
if(board[i][0]=='O'&&!vis[i][0]){
dfs(board,i,0,true);
}
}
for (int i = 0; i < m; i++) {
if(board[i][n-1]=='O'&&!vis[i][n-1]){
dfs(board,i,n-1,true);
}
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if(board[i][j]=='O'&&!vis[i][j]){
dfs(board,i,j,false);
}
}
}
}
private void dfs(char[][] g,int x,int y,boolean OnEdge){
if(g[x][y]=='X'||vis[x][y]){
return;
}
vis[x][y] = true;
int nx,ny;
for (int[] direction : directions) {
nx = x+direction[0];
ny = y+direction[1];
if(isValid(nx,ny)){
dfs(g,nx,ny,OnEdge);
}
}
if(!OnEdge){
g[x][y] ='X';
}
}
private boolean isValid(int x,int y){
return x>=0 && x<m && y>=0 && y<n;
}
}
1.10. LC 1391 检查网格中是否存在有效路径
这题很恶心。首先要选择不同形状街道的行走方向,比如第一个街道既可以从左往右,又可以从右到左;第二得保证下一个格子的街道形状能够对接的上这个各自的街道形状。比如1可以拼接5但不能拼接6。这两个条件我打表了两个巨繁的数组。其中directions[i]是一个二维数组,表示值为i+1的单元格的行走方向,每个行走方向都是一个二维向量。accept[i]也是一个二维数组,表示值为i+1的单元格可以接受的街道形状的对应id-1。
class Solution {
int m,n;
int[][][] directions = new int[][][]{
{
{0,-1},{0,1}
},
{
{-1,0},{1,0}
},
{
{0,-1},{1,0}
},
{
{0,1},{1,0}
},
{
{0,-1},{-1,0}
},
{
{0,1},{-1,0}
}
};
int[][][] accept = new int[][][]{
{
{0,3,5},{0,2,4}
},
{
{1,2,3},{1,4,5}
},
{
{0,3,5},{1,4,5}
},
{
{0,2,4},{1,4,5}
},
{
{0,3,5},{1,2,3}
},
{
{0,2,4},{1,2,3}
}
};
boolean[][] vis;
public boolean hasValidPath(int[][] grid) {
m = grid.length;
n = grid[0].length;
vis = new boolean[m][n];
return dfs(grid,0,0);
}
private boolean dfs(int[][] grid,int x,int y){
if(x==m-1 && y==n-1){
return true;
}
vis[x][y] = true;
int nx,ny;
int way = grid[x][y]-1;
boolean flag = false;
for (int i = 0; i < directions[way].length; i++) {
nx = x+directions[way][i][0];
ny = y+directions[way][i][1];
if(isValid(nx,ny)&&!vis[nx][ny]){
if(contains(accept[way][i],grid[nx][ny]-1)){
flag|=dfs(grid,nx,ny);
}
}
}
return flag;
}
private boolean isValid(int x,int y){
return x>=0 && x<m && y>=0 && y<n;
}
private boolean contains(int[] arr,int target){
for (int i : arr) {
if(i==target){
return true;
}
}
return false;
}
}
其实还好,O(mn)的。就是写起来要有一堆规则,很恶心。
1.11. LC 417 太平洋大西洋水流问题
先来分享一下我的一个垃圾解法。思路如下:
- 首先要解决判定是否既能到达左边界或者上边界,又能到达右边界或者下边界。状态压缩两位,如果是左边界或上边界,第一位或1;如果是右边界或者下边界,第二位或1。这样如果一个位置或完是3(11B)的话,就说明满足题意。
- 然后要解决循环访问的问题。注意这题不是简单的开一个vis数组就可以的。一个位置可能用到了某个位置(x,y)来到达边界,如果把这个(x,y)封住了,可能原本另一个位置也能用(x,y)到达边界,但由于封住就不能过了,这样就会少答案。但也不能不用vis数组。因为这样就会循环访问爆栈。
- 继第二条,如果在每次查询时重开一个vis数组的话,会多答案。假设位置(i,j)可以用(x,y)到达两个边界,那么(x,y)本身也会到达两个边界。所以就重复了。因此我选择先用一个Set存储位置保证元素唯一性,然后转成list。
import java.util.*;
class Solution {
int m,n;
boolean[][] vis;
int[][] directions = new int[][]{
{-1,0},
{0,1},
{1,0},
{0,-1}
};
Set<Integer> res;
public List<List<Integer>> pacificAtlantic(int[][] heights) {
res = new HashSet<>();
m = heights.length;
n = heights[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
fillFalse();
dfs(heights,i,j);
}
}
ArrayList<List<Integer>> ans = new ArrayList<>();
int x,y;
for (Integer cor : res) {
y = cor%n;
x = cor/n;
ans.add(List.of(x,y));
}
return ans;
}
private void fillFalse(){
vis = new boolean[m][n];
for (boolean[] vi : vis) {
Arrays.fill(vi, false);
}
}
private boolean isValid(int x,int y){
return x<m && y<n && x>=0 && y>=0;
}
private int dfs(int[][] heights,int x,int y){
vis[x][y] = true;
int access = 0;
if((x==m-1&&y==0)||(x==0&&y==n-1)){
access |= 3; // 11B
}
if(x==m-1||y==n-1){
access |= 2; // 10B
}
if(x==0||y==0){
access |= 1; // 01B
}
int nx,ny;
for (int[] direction : directions) {
nx = x + direction[0];
ny = y + direction[1];
if(isValid(nx,ny)&&!vis[nx][ny]&&heights[nx][ny]<=heights[x][y]){
access |= dfs(heights,nx,ny);
}
}
if(access==3){
res.add(x*n+y);
}
return access;
}
}
空间上O(mn),时间上最坏每次搜索都要跑全图,O((mn)²)。这个做法压着线过的。
比较好的做法还是从边界上入手,试图从边界扩散到中心。维护两个数组po,ao。po(i,j)和ao(i,j)标识是否能到达太平洋和大西洋。然后只要检查每个位置的两个数组值都为1即可。
class Solution {
public List<List<Integer>> pacificAtlantic(int[][] h) {
int r = h.length, c = h[0].length;
int[][] po = new int[r][c], ao = new int[r][c];
int min = Integer.MIN_VALUE;
for (int i = 0; i < r; i++) {
dfs(h, po, i, 0, min);
dfs(h, ao, i, c - 1, min);
}
for (int j = 0; j < c; j++) {
dfs(h, po, 0, j, min);
dfs(h, ao, r - 1, j, min);
}
List<List<Integer>> list = new ArrayList<>();
for (int i = 0; i < r; i++) {
for (int j = 0; j < c; j++) {
if (ao[i][j] == 1 && po[i][j] == 1) list.add(Arrays.asList(i, j));
}
}
return list;
}
public void dfs(int[][] h, int[][] mark, int x, int y, int pre) {
if (x < 0 || x >= h.length || y < 0 || y >= h[0].length || mark[x][y] == 1 || h[x][y] < pre) return;
mark[x][y] = 1;
int v = h[x][y];
dfs(h, mark, x + 1, y, v);
dfs(h, mark, x - 1, y, v);
dfs(h, mark, x, y + 1, v);
dfs(h, mark, x, y - 1, v);
}
}
这个时间复杂度就是O( max(m²n,mn²) )的。空间O(mn)