一次探索 C++ 在二维网格中寻找和处理连通区域的算法之旅。
在二维网格(Grid)或图像中,连通块 (Connected Component) 指的是一组相邻且具有相同属性(例如,值都为 1)的单元格。在经典的“岛屿问题”中,我们通常将:
1
的格子视为 陆地 (Land)0
的格子视为 水域 (Water)一个“岛屿”就是由若干个相互连接的陆地格子组成的连通块。
连通定义: 如果两个陆地格子共享一条边(即上下左右相邻),则它们是连通的。通常情况下,对角线相邻不被视为连通。
核心任务通常包括:计算连通块的数量、找出最大(或最小)的连通块面积、标记或修改特定的连通块等。
深度优先搜索 (Depth-First Search, DFS) 是解决连通块问题的经典递归方法。其核心思想是“一条路走到黑,走不通再回头”:
// DFS 核心函数:淹没 (标记) 与 (x, y) 相连的整个岛屿
// grid: 二维网格引用
// x, y: 当前格子的行和列索引
void dfs(vector>& grid, int x, int y) {
int rows = grid.size();
int cols = grid[0].size();
// 1. 边界条件检查 + 是否为陆地('1')检查
if (x < 0 || x >= rows || y < 0 || y >= cols || grid[x][y] != '1') {
return; // 越界或是水域或是已访问过的陆地,直接返回
}
// 2. 标记当前陆地格子为已访问 (这里用 '2' 表示)
grid[x][y] = '2'; // '沉岛'操作,防止重复访问
// 3. 递归访问四个相邻方向
dfs(grid, x + 1, y); // 向下探索
dfs(grid, x - 1, y); // 向上探索
dfs(grid, x, y + 1); // 向右探索
dfs(grid, x, y - 1); // 向左探索
}
// 主函数 (计算岛屿数量)
int numIslands_dfs(vector>& grid) {
if (grid.empty() || grid[0].empty()) {
return 0; // 处理空网格情况
}
int rows = grid.size();
int cols = grid[0].size();
int islandCount = 0; // 岛屿数量计数器
// 遍历网格中的每个格子
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
// 如果发现一个未访问的陆地格子 ('1')
if (grid[i][j] == '1') {
islandCount++; // 发现新岛屿,计数加 1
dfs(grid, i, j); // 从此格子开始 DFS,淹没整个岛屿
}
}
}
return islandCount; // 返回最终岛屿数量
}
DFS 注意要点:
广度优先搜索 (Breadth-First Search, BFS) 是另一种常用的图遍历算法,特别适合查找最短路径。在岛屿问题中,它通过队列 (Queue) 来实现层序遍历:
// BFS 辅助函数:从 (startX, startY) 开始淹没岛屿
#include <vector>
#include <queue>
#include <utility> // for std::pair
using namespace std;
void bfs(vector>& grid, int startX, int startY) {
int rows = grid.size();
int cols = grid[0].size();
// 创建队列,存储待访问的陆地格子坐标 {行, 列}
queue> q;
// 将起点加入队列并标记为已访问
q.push({startX, startY});
grid[startX][startY] = '2'; // 标记为已访问
// 定义四个方向的偏移量: 上, 下, 左, 右
int dx[] = {-1, 1, 0, 0};
int dy[] = {0, 0, -1, 1};
// 当队列不为空时,持续进行 BFS
while (!q.empty()) {
// 取出队首元素
pair current = q.front();
q.pop();
int x = current.first;
int y = current.second;
// 探索当前格子的四个相邻方向
for (int i = 0; i < 4; ++i) {
int nextX = x + dx[i];
int nextY = y + dy[i];
// 检查新坐标是否有效 (在边界内) 且是未访问的陆地 ('1')
if (nextX >= 0 && nextX < rows && nextY >= 0 && nextY < cols && grid[nextX][nextY] == '1') {
// 将有效的相邻陆地格子加入队列
q.push({nextX, nextY});
// 并标记为已访问
grid[nextX][nextY] = '2';
}
}
}
}
// 主函数 (计算岛屿数量 - BFS 版本)
int numIslands_bfs(vector>& grid) {
if (grid.empty() || grid[0].empty()) {
return 0;
}
int rows = grid.size();
int cols = grid[0].size();
int islandCount = 0;
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
if (grid[i][j] == '1') {
islandCount++;
bfs(grid, i, j); // 使用 BFS 淹没岛屿
}
}
}
// 注意:如果不想修改原 grid,可以在 bfs/dfs 前复制一份,或者使用 visited 数组
// 如果修改了原 grid,在多次调用前需要恢复 grid
return islandCount;
}
当前 BFS 队列 (队首在左):
BFS vs DFS 对比:
这是一个常见变种:计算所有岛屿中,面积最大的那个岛屿包含了多少个 '1'。我们可以在 DFS 或 BFS 探索一个岛屿的同时,计数其包含的格子数量。
// DFS 计算岛屿面积
// 返回从 (x, y) 出发能访问到的陆地格子总数
int dfsArea(vector>& grid, int x, int y) {
int rows = grid.size();
int cols = grid[0].size();
// 边界检查 + 非陆地检查
if (x < 0 || x >= rows || y < 0 || y >= cols || grid[x][y] != '1') {
return 0; // 不贡献面积
}
// 标记为已访问
grid[x][y] = '2';
// 当前格子面积为 1,加上来自四个方向的递归面积
int currentArea = 1;
currentArea += dfsArea(grid, x + 1, y);
currentArea += dfsArea(grid, x - 1, y);
currentArea += dfsArea(grid, x, y + 1);
currentArea += dfsArea(grid, x, y - 1);
return currentArea;
}
// 主函数计算最大岛屿面积
int maxAreaOfIsland(vector>& grid) {
if (grid.empty() || grid[0].empty()) return 0;
int rows = grid.size();
int cols = grid[0].size();
int maxArea = 0;
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
if (grid[i][j] == '1') {
int currentIslandArea = dfsArea(grid, i, j); // 计算当前岛屿面积
maxArea = max(maxArea, currentIslandArea); // 更新最大面积
}
}
}
// 注意:同样需要恢复 grid 或使用 visited 数组
return maxArea;
}
📄 查看完整 C++ 实现 (最大面积)
BFS 同样可以修改,在每次将格子加入队列时计数,或者在出队时计数。
并查集是一种高效处理“集合合并”和“查找元素所属集合”问题的数据结构。它也可以用来解决岛屿数量问题,尤其在动态场景(如岛屿可能合并)或需要频繁查询连通性时很有优势。
核心思想:
union
操作将这两个格子所在的集合合并。#include <vector>
#include <numeric> // for std::iota
class UnionFind {
private:
vector parent; // 存储每个节点的父节点
vector rank; // 优化:按秩合并 (可选,也可以按大小合并)
int count; // 连通分量的数量
public:
// 构造函数:初始化 n 个独立的集合
UnionFind(int n) : count(n) {
parent.resize(n);
std::iota(parent.begin(), parent.end(), 0); // 每个元素的父节点初始是自己
rank.resize(n, 0); // 初始秩(树高)为 0
}
// 查找元素 x 所属集合的根节点 (带路径压缩优化)
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
// 合并元素 x 和 y 所在的集合
void unite(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) { // 如果它们不在同一个集合
// 按秩合并:将秩小的树合并到秩大的树上
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
parent[rootY] = rootX; // 秩相等,随便合并,但根节点秩要加 1
rank[rootX]++;
}
count--; // 合并成功,连通分量数量减 1
}
}
// 获取当前连通分量的数量
int getCount() const {
return count;
}
};
// 使用并查集计算岛屿数量
int numIslands_uf(vector>& grid) {
if (grid.empty() || grid[0].empty()) return 0;
int rows = grid.size();
int cols = grid[0].size();
int waterCount = 0; // 统计水域数量
// 初始化并查集,大小为 M*N,但只处理陆地
UnionFind uf(rows * cols);
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
if (grid[i][j] == '1') {
// 检查下方相邻格子
if (i + 1 < rows && grid[i + 1][j] == '1') {
uf.unite(i * cols + j, (i + 1) * cols + j); // 合并
}
// 检查右方相邻格子
if (j + 1 < cols && grid[i][j + 1] == '1') {
uf.unite(i * cols + j, i * cols + (j + 1)); // 合并
}
} else {
waterCount++; // 统计水域格子
}
}
}
// 初始连通分量是所有格子 M*N
// 减去水域格子,剩下的是初始陆地格子构成的独立集合数
// 再减去合并掉的集合数 (M*N - uf.getCount())
// 简化:最终连通分量数 uf.getCount() 包含了水域各自独立的集合
// 所以岛屿数 = 总连通分量数 - 水域格子数
return uf.getCount() - waterCount;
}
📄 查看完整 C++ 实现 (并查集)
并查集在处理二维网格时,通常需要将二维坐标 `(r, c)` 映射到一维索引 `r * cols + c`。
并查集优点:
缺点:
解析:
给定一个由 '1'(陆地)和 '0'(水域)组成的二维网格 `grid`,计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。你可以假设网格的四个边均被水包围。
输入: 一个 vector
类型的网格。
输出: 岛屿的数量 (int
)。
给定一个由 '1'(陆地)和 '0'(水域)组成的二维网格 `grid`,找到最大的岛屿面积。如果网格中没有岛屿,则返回 0。
面积是指岛屿中包含的 '1' 的个数。
输入: 一个 vector
类型的网格。
输出: 最大的岛屿面积 (int
)。
恭喜你!通过本次学习,我们掌握了处理 C++ 中二维网格连通块(岛屿)问题的核心方法:
0 <= x < rows && 0 <= y < cols
)。visited
数组/集合。理解并熟练运用 DFS 和 BFS 是解决各种图和网格问题的基础。多练习,多思考不同场景下的选择!
通过实战巩固理解:
尝试用 DFS、BFS 和并查集(如果适用)分别解决这些问题,体会不同方法的优劣。
算法学习是一个持续实践和深入理解的过程。祝你在 C++ 算法之路上越走越远!🎉