图论专题(六):“隐式图”的登场!DFS/BFS 攻克「岛屿数量」

哈喽各位,我是前端L。

欢迎来到我们的图论专题第六篇!我们已经学会了如何在"显式"的图(由节点和边列表定义)上进行探险。但如果,地图本身就是一张"网格"呢?

今天,我们要解决的"岛屿数量"问题,是算法面试中最经典、最基础 的网格遍历题。它将完美地向我们展示,如何将一个m x n的矩阵,视作 一个拥有 m * n 个节点、由"上下左右"关系连接的"隐式图"。而我们的DFS/BFS,就是在这张新地图上"航行"的完美工具。

力扣 200. 岛屿数量

https://leetcode.cn/problems/number-of-islands/

题目分析:

  • 输入 :一个 m x n 的二维网格 grid,由 '1' (陆地) 和 '0' (水) 组成。

  • 目标:计算图中"岛屿"的数量。

  • 岛屿定义 :由水平或竖直 相邻的 '1'(陆地)连接而成的区域,且四周被水('0')环绕。

"Aha!"时刻:将"网格"翻译成"图"

  • 节点 (Vertex) :每一个单元格 (r, c) 都是图中的一个节点。

  • 边 (Edge) :每个单元格 (r, c) 与它的上 (r-1, c)、下 (r+1, c)、左 (r, c-1)、右 (r, c+1) 邻居之间,都存在一条"隐式"的边。

  • 我们要找什么? :我们只关心由 '1'(陆地)构成的"连通区域"。

  • 问题被完美转化: 计算这个"隐式图"中,由 '1' 构成的"连通分量 (Connected Components)"的个数。

解决方案:"淹没"岛屿 (Flood Fill)

如何计算"连通分量"的个数? 我们需要一个"侦察兵 "(主循环)和一个"作战部队"(DFS/BFS)。

算法流程:

  1. 初始化岛屿计数 islandCount = 0

  2. "侦察兵"出动 :用两层 for 循环 ,遍历矩阵中的每一个 单元格 (r, c)

  3. 发现新目标 :在遍历时,if (grid[r][c] == '1')

    • "Aha!" 我们发现了一块"陆地"!

    • 由于我们的"作战部队"会把访问过的陆地都"淹没"(标记掉),所以,任何时候我们遇到的 '1',都必定是一个全新 的、未被发现的岛屿的"登陆点"。

    • islandCount++

  4. "作战部队"出动

    • (r, c) 这个"登陆点"开始,启动一次 DFS 或 BFS

    • 这个 DFS/BFS 的任务,就是"淹没 (Flood Fill) ":将所有与 (r, c) 连通 的、同属于这个岛屿的 '1' ,全部标记为已访问 (比如,直接改成 '0''2'),防止它们被"侦察兵"重复发现。

  5. for 循环结束后,islandCount 就是最终答案。

代码实现 (O(V+E) -> O(m*n))

解法一:DFS (递归"淹没")

C++

复制代码
#include <vector>

using namespace std;

class Solution {
private:
    // "作战部队":DFS 函数
    // 任务:从 (r, c) 出发,淹没所有相连的 '1'
    void dfs_sink(vector<vector<char>>& grid, int r, int c) {
        int m = grid.size();
        int n = grid[0].size();

        // 1. Base Case (越界或遇到水)
        if (r < 0 || r >= m || c < 0 || c >= n || grid[r][c] == '0') {
            return;
        }

        // 2. "淹没" (标记为已访问)
        grid[r][c] = '0';

        // 3. 递归探索邻居
        dfs_sink(grid, r + 1, c); // 下
        dfs_sink(grid, r - 1, c); // 上
        dfs_sink(grid, r, c + 1); // 右
        dfs_sink(grid, r, c - 1); // 左
    }

public:
    int numIslands(vector<vector<char>>& grid) {
        if (grid.empty() || grid[0].empty()) {
            return 0;
        }

        int m = grid.size();
        int n = grid[0].size();
        int islandCount = 0;

        // "侦察兵":遍历所有单元格
        for (int r = 0; r < m; ++r) {
            for (int c = 0; c < n; ++c) {
                if (grid[r][c] == '1') {
                    // 发现了新岛屿!
                    islandCount++;
                    // "作战部队"出动,淹没它
                    dfs_sink(grid, r, c);
                }
            }
        }
        
        return islandCount;
    }
};

解法二:BFS (队列"淹没")

C++

复制代码
#include <vector>
#include <queue>

using namespace std;

class Solution_BFS {
public:
    int numIslands(vector<vector<char>>& grid) {
        if (grid.empty() || grid[0].empty()) {
            return 0;
        }

        int m = grid.size();
        int n = grid[0].size();
        int islandCount = 0;
        
        // 邻居的方向数组
        int dr[] = {0, 0, 1, -1};
        int dc[] = {1, -1, 0, 0};

        for (int r = 0; r < m; ++r) {
            for (int c = 0; c < n; ++c) {
                if (grid[r][c] == '1') {
                    islandCount++;
                    grid[r][c] = '0'; // 标记为已访问
                    
                    queue<pair<int, int>> q;
                    q.push({r, c});

                    while (!q.empty()) {
                        pair<int, int> curr = q.front();
                        q.pop();

                        // 探索4个邻居
                        for (int i = 0; i < 4; ++i) {
                            int nr = curr.first + dr[i];
                            int nc = curr.second + dc[i];

                            // 检查邻居是否合法且是 '1'
                            if (nr >= 0 && nr < m && nc >= 0 && nc < n && grid[nr][nc] == '1') {
                                grid[nr][nc] = '0'; // 淹没
                                q.push({nr, nc});
                            }
                        }
                    }
                }
            }
        }
        return islandCount;
    }
};

深度复杂度分析

  • V (Vertices) :顶点数,即 m * n

  • E (Edges) :边数,每个顶点最多4条边,所以 E 最多是 4 * m * n 的级别。

  • 时间复杂度 O(m * n)

    • 我们的"侦察兵" for 循环,会访问 m * n 个单元格。

    • "作战部队" (DFS/BFS) 会在 grid[r][c] == '1' 时启动。由于启动后它会"淹没"所有它能到达的 1确保了每个 '1' 单元格,只会被 DFS/BFS 核心逻辑访问一次

    • 总的来看,每个单元格 (r, c)(无论是0还是1)都被主循环和遍历逻辑,常数次地访问。

    • 总时间复杂度 O(V + E) -> O(mn + 4 m*n) -> O(m * n)

  • 空间复杂度

    • DFSO(m * n)。在最坏情况下(一个"蛇形"岛屿占满了整个网格),递归栈 的深度可能是 m * n

    • BFSO(min(m, n))。在最坏情况下(比如一个"棋盘格"),队列的大小最多是 min(m, n) 级别。(修正:一个"圆形"岛屿,队列大小可能达到 O(m n))*。(再修正:BFS的最坏空间是O(V),即 O(m*n),例如一个从 (0,0) 开始的巨大岛屿)。

    • (注:如果我们不使用"原地修改" grid[r][c]='0',而是用一个 visited[m][n] 数组,那么空间复杂度会额外增加 O(mn))*

总结

今天,我们打响了"图论"专题的"隐式图"第一枪!

  • "二维网格" = "隐式图"

  • "岛屿数量" = "连通分量个数"

  • DFS/BFS + visited (或原地修改) = "淹没算法 (Flood Fill)"

这个"网格即图"的思维模型,是图论应用中最重要、最常见的模式。

在下一篇中,我们将继续使用这个模型,但我们的任务不再是"计数",而是要计算"岛屿的最大面积"!

下期见!

相关推荐
-大头.2 小时前
Python数据结构之旅:09-图论基础——连接万物的网络
数据结构·图论
sin_hielo2 小时前
leetcode 2654
算法·leetcode
zz0723202 小时前
数据结构 —— 队列
数据结构
Bear on Toilet3 小时前
C++_Bug:现代写法拷贝构造中 swap 写法之小坑
数据结构·c++·bug
智者知已应修善业3 小时前
【给定英文字符串统计最多小写最前输出】2023-2-27
c语言·开发语言·c++·经验分享·笔记·算法
RWKV元始智能3 小时前
体验RWKV-7训练全过程,只需400行代码训练3分钟
人工智能·算法·机器学习
点云SLAM3 小时前
四元数 (Quaternion)微分-四元数导数的矩阵表示推导(8)
线性代数·算法·计算机视觉·矩阵·机器人·slam·四元数
潼心1412o3 小时前
数据结构(长期更新)第8讲:队列
数据结构
kyle~4 小时前
算法---贪心算法(Greedy Algorithm)
算法·贪心算法