DAY50

77 阅读9分钟

第十一章:图论part02

99.  岛屿数量 深搜

注意深搜的两种写法,熟练掌握这两种写法 以及 知道区别在哪里,才算掌握的深搜。

www.programmercarl.com/kamacoder/0…

99.  岛屿数量 广搜

注意广搜的两种写法,第一种写法为什么会超时, 如果自己做的录友,题目通过了,也要仔细看第一种写法的超时版本,弄清楚为什么会超时,因为你第一次 幸运 没那么想,第二次可就不一定了。

www.programmercarl.com/kamacoder/0…

LeetCode 200: Number of Islands 中,你需要计算一个由 '1'(陆地)和 '0'(水)组成的二维网格中有多少个岛屿。岛屿是由水平或垂直相邻的 '1' 组成,并且岛屿的周围都是水域。

你可以通过深度优先搜索(DFS)或广度优先搜索(BFS)来解决这个问题,两者都是遍历图中连通分量的常用方法。

深度优先搜索 (DFS)

DFS 是一种 递归 实现的搜索方式。其特点是每次从当前节点延伸到最深处,然后再回溯。这种搜索方式适合在树或图中遍历所有相连的节点。

DFS 步骤:
  1. 遍历网格,找到第一个 '1'
  2. 当找到 '1' 时,将其标记为访问过(可以改为 '0' 或使用一个辅助的标记数组)。
  3. 使用递归(或栈)将与该 '1' 相邻的所有 '1' 全部访问,标记为 '0'
  4. 每找到一个新的 '1',计数器增加 1,表示发现了一个新岛屿。
  5. 继续遍历,直到网格的所有节点都被访问。
DFS 实现代码:
var numIslands = function(grid) {
    if (grid.length === 0) return 0;
    
    let count = 0;
    const rows = grid.length;
    const cols = grid[0].length;
    
    const dfs = (i, j) => {
        if (i < 0 || j < 0 || i >= rows || j >= cols || grid[i][j] === '0') {
            return;
        }
        // 标记为已访问
        grid[i][j] = '0';
        
        // 向四个方向扩展
        dfs(i - 1, j); // 上
        dfs(i + 1, j); // 下
        dfs(i, j - 1); // 左
        dfs(i, j + 1); // 右
    };
    
    for (let i = 0; i < rows; i++) {
        for (let j = 0; j < cols; j++) {
            if (grid[i][j] === '1') {
                count++;
                dfs(i, j); // 从找到的第一个 '1' 开始扩展
            }
        }
    }
    
    return count;
};
DFS 详解:
  • 递归从当前节点延伸至四个方向(上、下、左、右),并标记已经访问过的节点为 '0'
  • 每次进入一个新的连通块(即找到一个新的 '1'),计数器增加,表示新的岛屿。

回溯

DFS 模板中,"回溯" 的概念指的是:在递归过程中,处理某个节点并继续深入递归,之后在递归完成时,撤销对当前节点的处理,回到上一步状态,以便在其他路径上继续探索。

来看你提到的岛屿问题 (LeetCode 200: Number of Islands),DFS 模板如下:

function dfs(grid, i, j) {
    // 终止条件:超出边界或遇到水
    if (i < 0 || j < 0 || i >= grid.length || j >= grid[0].length || grid[i][j] === '0') {
        return;
    }

    // 处理当前节点,将陆地标记为已访问
    grid[i][j] = '0';

    // 对四个方向的相邻节点进行递归
    dfs(grid, i - 1, j); // 上
    dfs(grid, i + 1, j); // 下
    dfs(grid, i, j - 1); // 左
    dfs(grid, i, j + 1); // 右
}
回溯的部分

在这个 DFS 模板中,回溯的部分体现为:

  1. 每次在递归时,标记当前节点为已访问,比如这里将陆地 '1' 标记为 '0',表示该节点已经被处理。
  2. 递归调用完后,没有显式的“撤销”操作。这是因为一旦一个岛屿的某个部分被标记为 '0'(表示处理过),就不需要恢复原状态。换句话说,这道题的回溯是隐式的——我们不需要将处理后的 '0' 恢复为 '1',因为岛屿只需要遍历一次,后续不需要再次访问这个节点。

这就是问题中回溯的表现:我们对每个节点处理后不会再重复访问它,因此也无需撤销。回溯操作通常会在我们需要探索所有可能的路径或组合时显式存在,而在岛屿问题中,每个节点只能访问一次,因此回溯的“撤销”操作不需要显式地进行。

举例说明:

例如,考虑下面的矩阵:

1 1 0
1 0 0
0 0 1

DFS 从 (0,0) 开始,将其标记为 '0',然后递归处理 (0,1),再处理 (1,0),这些相邻的 '1' 都会被标记为 '0'。在遍历完第一个岛屿后,我们不需要再撤销之前的修改,继续遍历网格,直到找到新的未访问的岛屿。

广度优先搜索 (BFS)

BFS 是使用 队列 实现的遍历方式,其特点是按层级扩展,先访问当前节点的所有相邻节点,然后再访问这些相邻节点的相邻节点。适合用于寻找最短路径等场景。

BFS 步骤:
  1. 遍历网格,找到第一个 '1'
  2. 当找到 '1' 时,将其标记为访问过(可以改为 '0' 或使用一个辅助的标记数组)。
  3. 将其放入队列,并不断扩展其四个方向(上、下、左、右),将相邻的 '1' 加入队列。
  4. 每找到一个新的 '1',计数器增加 1,表示发现了一个新岛屿。
  5. 继续遍历,直到网格的所有节点都被访问。
BFS 实现代码:
var numIslands = function(grid) {
    if (grid.length === 0) return 0;
    
    let count = 0;
    const rows = grid.length;
    const cols = grid[0].length;
    
    const bfs = (i, j) => {
        const queue = [];
        queue.push([i, j]);
        grid[i][j] = '0'; // 标记为已访问
        
        while (queue.length) {
            const [x, y] = queue.shift();
            
            // 向四个方向扩展
            if (x - 1 >= 0 && grid[x - 1][y] === '1') {
                queue.push([x - 1, y]);
                grid[x - 1][y] = '0'; // 标记为已访问
            }
            if (x + 1 < rows && grid[x + 1][y] === '1') {
                queue.push([x + 1, y]);
                grid[x + 1][y] = '0'; // 标记为已访问
            }
            if (y - 1 >= 0 && grid[x][y - 1] === '1') {
                queue.push([x, y - 1]);
                grid[x][y - 1] = '0'; // 标记为已访问
            }
            if (y + 1 < cols && grid[x][y + 1] === '1') {
                queue.push([x, y + 1]);
                grid[x][y + 1] = '0'; // 标记为已访问
            }
        }
    };
    
    for (let i = 0; i < rows; i++) {
        for (let j = 0; j < cols; j++) {
            if (grid[i][j] === '1') {
                count++;
                bfs(i, j); // 从找到的第一个 '1' 开始扩展
            }
        }
    }
    
    return count;
};
BFS 详解:
  • 使用队列存储当前节点,然后扩展它的四个方向。
  • 每次进入一个新的连通块(即找到一个新的 '1'),将其所有相邻 '1' 节点标记为已访问。

DFS 与 BFS 的比较

  1. DFS:
    • 递归实现,适合探索最深层的节点。
    • 在网格遍历中每次找到一个 '1',立即将其所有相邻节点(上下左右)全部递归访问。
    • 使用栈(或隐式的递归栈)实现。
  2. BFS:
    • 队列实现,适合层级遍历所有相邻节点。
    • 在网格遍历中每次找到一个 '1',层级地逐步扩展相邻的 '1'
    • 更适合用于求最短路径问题。
  3. 时间复杂度
    • DFS 和 BFS 的时间复杂度都是 O(m * n),其中 m 是网格的行数,n 是列数。
  4. 空间复杂度
    • DFS 的空间复杂度在最坏情况下为 O(m * n)(递归栈深度)。
    • BFS 的空间复杂度也是 O(m * n)(队列存储所有节点)。

100.  岛屿的最大面积

本题就是基础题了,做过上面的题目,本题很快。

www.programmercarl.com/kamacoder/0… 在 LeetCode 第 695 题「岛屿的最大面积」中,问题可以通过深度优先搜索(DFS)或广度优先搜索(BFS)来解决。两种搜索方式的本质是一样的,都在查找连通的陆地区域,计算连通块的面积,找到最大的连通块。下面分别说明 DFS 和 BFS 的实现。

1. 深度优先搜索(DFS)

深度优先搜索通过递归的方式,在找到某个陆地后,优先向四周继续深入搜索,直到到达边界或遇到水。

DFS 代码实现

/**
 * @param {number[][]} grid
 * @return {number}
 */
var maxAreaOfIsland = function(grid) {
    if (!grid.length) return 0;

    let maxArea = 0;
    let rows = grid.length;
    let cols = grid[0].length;

    // 深度优先搜索函数
    function dfs(i, j) {
        if (i < 0 || j < 0 || i >= rows || j >= cols || grid[i][j] === 0) {
            return 0;
        }

        grid[i][j] = 0; // 标记为已访问
        let area = 1;

        // 四个方向递归搜索
        area += dfs(i - 1, j); // 上
        area += dfs(i + 1, j); // 下
        area += dfs(i, j - 1); // 左
        area += dfs(i, j + 1); // 右

        return area;
    }

    // 遍历网格
    for (let i = 0; i < rows; i++) {
        for (let j = 0; j < cols; j++) {
            if (grid[i][j] === 1) { // 如果是陆地
                maxArea = Math.max(maxArea, dfs(i, j)); // 更新最大岛屿面积
            }
        }
    }

    return maxArea;
};
深搜 DFS 思路:
  1. 遍历网格,当遇到 1(陆地)时,开始进行深度优先搜索。
  2. 每次搜索时,将当前的陆地面积累加,并向四个方向递归搜索。
  3. 将访问过的节点标记为 0,以避免重复访问。
  4. 记录搜索过程中最大的岛屿面积。

2. 广度优先搜索(BFS)

广度优先搜索通过使用队列,逐层扩展查找每个连通的陆地区域。每次从队列中取出一个节点,访问其四周未访问的陆地,直到搜索完所有的陆地。

BFS 代码实现

/**
 * @param {number[][]} grid
 * @return {number}
 */
var maxAreaOfIsland = function(grid) {
    if (!grid.length) return 0;

    let maxArea = 0;
    let rows = grid.length;
    let cols = grid[0].length;

    // 广度优先搜索函数
    function bfs(i, j) {
        let queue = [[i, j]];
        grid[i][j] = 0; // 标记为已访问
        let area = 0;

        // 方向数组,上下左右
        const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]];

        while (queue.length) {
            const [x, y] = queue.shift();
            area++;

            // 遍历四个方向
            for (let [dx, dy] of directions) {
                const newX = x + dx;
                const newY = y + dy;

                // 如果新坐标在网格内,且为陆地
                if (newX >= 0 && newX < rows && newY >= 0 && newY < cols && grid[newX][newY] === 1) {
                    queue.push([newX, newY]);
                    grid[newX][newY] = 0; // 标记为已访问
                }
            }
        }

        return area;
    }

    // 遍历网格
    for (let i = 0; i < rows; i++) {
        for (let j = 0; j < cols; j++) {
            if (grid[i][j] === 1) { // 如果是陆地
                maxArea = Math.max(maxArea, bfs(i, j)); // 更新最大岛屿面积
            }
        }
    }

    return maxArea;
};
广搜 BFS 思路:
  1. 每次遇到 1(陆地)时,开始进行广度优先搜索。
  2. 将当前节点入队列,并依次访问四周未访问的陆地。
  3. 使用队列逐层扩展,直到没有可以访问的陆地为止。
  4. 记录搜索过程中最大的岛屿面积。

深搜(DFS)与广搜(BFS)区别:

  • DFS:通过递归函数向四周扩展,深入到某个方向的尽头后再回溯。这种方式适合用递归实现,栈的深度取决于岛屿的形状。
  • BFS:使用队列,一层一层向四周扩展。这种方式适合用迭代实现,适合逐层遍历,队列的长度取决于扩展过程中某一层的节点数量。

选择 DFS 或 BFS 取决于具体需求,通常两者性能相近,只是实现方式和递归栈/队列处理不同。