算法-由岛屿问题引出网格dfs通用解法


问题

在这里插入图片描述

一、从二叉树到网格

网格实际上是一类特殊的图【简化版】,如果没有技巧很啰嗦

二叉树dfs解题模板:

void dfs(TreeNode root) {
    if (root == null) return;
    dfs(root.left);
    dfs(root.right);
}

算法主体分为两个部分:

(1)baseline:即root == null

(2)访问邻接点:

二叉树的邻接点很简单,就是左右子树,且子树不存在循环引用,因此无需考虑访问重复点问题。


将二叉树思想引入网格问题:

(1)baseline:
出到网格外面

网格实际上是二维矩阵,因此只要下标过了范围(<0 或者 > len - 1)就是非法的,即直接return

与二叉树 root == null 类似,都是先污染后治理的思想,很容易理解也不容易出问题

(2)访问邻接点

网格的邻接点有上下左右四个,通过下标±1就可以得到
在这里插入图片描述


需要注意的是,网格问题会存在重复访问问题

比如,对节点a

a.top.left.bottom == a.left, 发生了重复访问

做法是,使用一个同样大小的二维矩阵标标注已访问的点。

比如,对于岛屿问题,可以把0标注为海洋,1标注为未访问陆地,2标注已访问陆地。

!= 1也作为一个baseline, 这样就不会出现一再访问同一个点的问题了


到了这里,可以给出网格问题的通用模板:

int[][] grids, flag;//长为m,宽为n
int m, n;
void dfs (int i, int j) {
    if (!inErea(i, j) || flag[i][j] != 1) return;
    flag[i][j] = 2; //标注为已访问
    dfs(i - 1, j);
    dfs(i + 1, j);
    dfs(i, j - 1);
    dfs(i, j + 1); 
}
boolean inErea(int i, int j) {
    if (i < 0 || i >= len || j < 0 || j >= len) return false;
    else return true;
}

二、本题思路

本题需要求岛屿的数量,转化为上面的描述就是1区域的数量
对于本算法,访问过的节点flag位必定会被置为2,且其相邻的所有区域值都会被置为2.
因此,每次遇到1节点,都代表来到一片新大陆
因此,遍历一遍数组,没次遇到grid[i][j] == 1 && flag[i][j] == 1 的,都需要将count + 1
最终count即为结果

注:

  • 本题其实不需要额外的flag数组,可以使用原来的数组,甚至更简洁,但是不推荐这样【空间可以浪费,满足可读性即可】
  • java复制数组: int[] newArray = Arrays.copyOf(oldArray, oldArray.length);,注意二维数组需要一轮循环
  • 时间复杂度:O(n^2), 空间复杂度:O(N^2)【即使不使用flag数组,递归栈的深度也是O(N)】
class Solution {
	int[][] grid;
	int m, n;
    public int numIslands(char[][] grid) {
        this.grid = grid;
        m = grid.length;
        n = grid[0].length;
        int count = 0;
        for (int i = 0; i < m; i++) {
        	for (int j = 0; j < n; j++) {
        		if (grid[i][j] == '1') {
        			dfs(i, j);
        			count++;
        		}
        	}
        }
        return count;
    }

    public void dfs(int i, int j) {
    	if (!inArea(i, j) || grid[i][j] != '1') return;
    	flag[i][j] = '2';
    	dfs (i - 1, j);
    	dfs (i + 1, j);
    	dfs (i, j - 1);
    	dfs (i, j + 1);
    }

    public boolean inArea(int i, int j) {
    	return i >= 0 && i < len && j >= 0 && j < len;
    }
}

三、其他方法

1. 广度优先遍历

由于缺少递归的简单描述,bfs要考虑大量的下标问题,使用起来比dfs更为困难。

bfs一般依托于一个栈实现,我们首先要清楚栈中需要存储什么元素。
作为二维矩阵,可以只存储元素序号即可【元素序号 = 行号 * (列长度) + 列号】

第二个问题在于如何像使用递归方法那样方便的访问上下左右的相邻元素。

我看到的一种比较好的做法是:
设置一个常量池:{[-1, 0], [1, 0], [0, -1], [0, 1]}每个左右位置去轮流加x,y,每次x,y只会有一个加一或者减一

 private void bfs(int i, int j) {
        Queue<Integer> queue = new LinkedList<>();
        queue.offer(i * cols + j);
        // 注意:这里要标记上已经访问过
        visited[i][j] = true;
        while (!queue.isEmpty()) {
            int cur = queue.poll();
            int curX = cur / cols;
            int curY = cur % cols;
            for (int k = 0; k < 4; k++) {
                int newX = curX + DIRECTIONS[k][0];
                int newY = curY + DIRECTIONS[k][1];
                if (inArea(newX, newY) && grid[newX][newY] == '1' && !visited[newX][newY]) {
                    queue.offer(newX * cols + newY);
                    // 特别注意:在放入队列以后,要马上标记成已经访问过,语义也是十分清楚的:反正只要进入了队列,迟早都会遍历到它
                    // 而不是在出队列的时候再标记,如果是出队列的时候再标记,会造成很多重复的结点进入队列,造成重复的操作,这句话如果你没有写对地方,代码会严重超时的
                    visited[newX][newY] = true;
                }
            }
        }
    }

或者用简单一点的办法,直接将每个节点位置作为一个特殊的数据结构传入queue, 每次出队后,手动对其四个位置入队,再考虑是否满足要求,需要在处理

public void bfs(int i, int j) {
    	Queue<int[]> queue = new LinkedList();
    	queue.offer(new int[]{i, j});
    	while (!queue.isEmpty()) {
    		int[] cur = queue.poll();
    		int x = cur[0], y = cur[1];
    		if (x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == '1') {
                grid[x][y] = '2';
    			queue.offer(new int[] {x + 1, y});
    			queue.offer(new int[] {x - 1, y});
    			queue.offer(new int[] {x, y + 1});
    			queue.offer(new int[] {x, y - 1});
    		}
    	}
    }

这样做要创建大量的int[] 节点对象,资源耗费很大,远不及上面的计算下标

2. 并查集法

有点复杂,立个flag, 以后再看

本文参考至岛屿问题解法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值