1. 图论算法实战:岛屿问题全解析
作为一名算法工程师,我在处理二维矩阵类问题时最常遇到的就是岛屿相关题目。这类问题看似简单,却能考察对图遍历、连通性分析等核心概念的掌握程度。今天我将通过4道经典题目,手把手带你掌握岛屿问题的解题套路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 101孤岛的总面积计算
2.1 问题重述与分析
给定一个由0(水)和1(陆地)组成的二维矩阵,我们需要找出所有"孤岛"的总面积。这里的孤岛定义为完全不接触矩阵边缘的陆地区域。例如在3x3矩阵中:
code复制0 0 0
0 1 0
0 0 0
中间单独的1就是一个孤岛,面积为1。
关键点在于:
- 孤岛必须完全被水包围
- 不能有任何部分接触矩阵边缘
- 相邻指上下左右四个方向
2.2 解题思路与算法选择
这类连通性问题通常采用DFS或BFS。我的解题思路是:
- 先处理边缘陆地:遍历矩阵四边,遇到陆地就用DFS/BFS将其全部淹没(标记为0)
- 再统计剩余陆地:此时矩阵中剩余的陆地就都是孤岛了
- 二次遍历统计面积
这种方法避免了维护额外数据结构,直接在原矩阵上操作,空间复杂度O(1)。
2.3 代码实现与细节处理
java复制public class Main {
static int count = 0;
static int[][] dirs = {{0,1}, {1,0}, {-1,0}, {0,-1}};
static void dfs(int[][] grid, int x, int y) {
for(int[] dir : dirs) {
int nx = x + dir[0];
int ny = y + dir[1];
if(nx<0 || nx>=grid.length || ny<0 || ny>=grid[0].length)
continue;
if(grid[nx][ny] == 1) {
grid[nx][ny] = 0;
count++;
dfs(grid, nx, ny);
}
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int m = sc.nextInt(), n = sc.nextInt();
int[][] grid = new int[m][n];
// 输入处理...
// 处理边缘陆地
for(int i=0; i<m; i++) {
if(grid[i][0] == 1) {
grid[i][0] = 0;
count++;
dfs(grid, i, 0);
}
if(grid[i][n-1] == 1) {
grid[i][n-1] = 0;
count++;
dfs(grid, i, n-1);
}
}
// 上下边缘处理...
// 统计孤岛面积
count = 0;
for(int i=0; i<m; i++) {
for(int j=0; j<n; j++) {
if(grid[i][j] == 1) {
grid[i][j] = 0;
count++;
dfs(grid, i, j);
}
}
}
System.out.println(count);
}
}
注意事项:DFS递归深度可能造成栈溢出,对于极大矩阵建议改用BFS迭代实现。同时要注意Java中数组访问的边界检查。
3. 102沉没孤岛问题
3.1 问题变形与区别
与101题不同,本题要求:
- 找出所有孤岛(不接触边缘的陆地)
- 将这些孤岛全部沉没(1变0)
- 输出处理后的矩阵
关键区别在于需要保留边缘连接的陆地。
3.2 标记法的应用
我采用三阶段标记法:
- 标记边缘连通陆地为2
- 将剩余的1(孤岛)沉没为0
- 将标记2恢复为1
java复制public class Main {
static int[][] dirs = {{-1,0}, {0,-1}, {1,0}, {0,1}};
static void dfs(int[][] grid, int x, int y) {
grid[x][y] = 2; // 特殊标记
for(int[] dir : dirs) {
int nx = x + dir[0];
int ny = y + dir[1];
if(nx<0||ny<0||nx>=grid.length||ny>=grid[0].length)
continue;
if(grid[nx][ny] != 1) continue;
dfs(grid, nx, ny);
}
}
public static void main(String[] args) {
// 输入处理...
// 标记边缘连通区域
for(int i=0; i<m; i++) {
if(grid[i][0] == 1) dfs(grid, i, 0);
if(grid[i][n-1] == 1) dfs(grid, i, n-1);
}
// 其他边缘处理...
// 沉没孤岛
for(int i=0; i<m; i++) {
for(int j=0; j<n; j++) {
if(grid[i][j] == 1) grid[i][j] = 0;
else if(grid[i][j] == 2) grid[i][j] = 1;
}
}
// 输出结果...
}
}
3.3 性能优化思考
当矩阵非常大时(如1000x1000),递归DFS可能导致栈溢出。这时可以:
- 改用BFS迭代实现
- 使用并查集数据结构
- 分块处理大矩阵
实测在常规OJ题目范围内(100x100左右),DFS完全够用且代码更简洁。
4. 103水流问题
4.1 问题建模
这是一个典型的多源BFS/DFS问题。我们需要找出哪些位置的水可以同时流向:
- 第一边界(左+上)
- 第二边界(右+下)
直接思路是对每个点做两次DFS,但这样时间复杂度O(n²m²)会超时。
4.2 逆向思维优化
更聪明的做法是从边界反推:
- 从第一边界出发,逆向DFS标记可达点
- 从第二边界出发,同样逆向DFS标记
- 两次都被标记的点就是解
java复制public class Main {
static void dfs(int[][] grid, int x, int y, boolean[][] visited, int height) {
if(x<0||y<0||x>=grid.length||y>=grid[0].length||visited[x][y])
return;
if(grid[x][y] < height) return;
visited[x][y] = true;
dfs(grid, x+1, y, visited, grid[x][y]);
// 其他方向...
}
public static void main(String[] args) {
// 输入处理...
boolean[][] visited1 = new boolean[m][n];
boolean[][] visited2 = new boolean[m][n];
// 从第一边界逆向搜索
for(int i=0; i<m; i++) {
dfs(grid, i, 0, visited1, Integer.MIN_VALUE);
}
for(int j=0; j<n; j++) {
dfs(grid, 0, j, visited1, Integer.MIN_VALUE);
}
// 从第二边界逆向搜索
for(int i=0; i<m; i++) {
dfs(grid, i, n-1, visited2, Integer.MIN_VALUE);
}
// 其他边界处理...
// 收集结果
List<int[]> res = new ArrayList<>();
for(int i=0; i<m; i++) {
for(int j=0; j<n; j++) {
if(visited1[i][j] && visited2[i][j]) {
res.add(new int[]{i, j});
}
}
}
// 输出结果...
}
}
4.3 边界条件处理
特别注意:
- 高度相等时水也可以流动(grid[x][y] < height 判断)
- 初始高度设为Integer.MIN_VALUE确保边界点能被处理
- 使用两个独立的visited数组分别记录
5. 104建造最大岛屿
5.1 问题分析
这是岛屿问题的进阶版,允许将一个0变为1,求能形成的最大岛屿面积。需要考虑:
- 原有岛屿的连通性和面积
- 连接不同岛屿的可能性
5.2 多阶段解法
我的解决方案分为三个阶段:
- 标记阶段:给每个岛屿编号并记录面积
- 探索阶段:检查每个0,看它能连接哪些不同岛屿
- 计算阶段:找出最佳连接方案
java复制public class Main {
static int count, mark;
static int[][] dirs = {{0,1}, {0,-1}, {1,0}, {-1,0}};
static void dfs(int[][] grid, int x, int y, boolean[][] visited) {
if(x<0||x>=grid.length||y<0||y>=grid[0].length) return;
if(visited[x][y] || grid[x][y] != 1) return;
visited[x][y] = true;
count++;
grid[x][y] = mark;
for(int[] dir : dirs) {
dfs(grid, x+dir[0], y+dir[1], visited);
}
}
public static void main(String[] args) {
// 输入处理...
boolean[][] visited = new boolean[m][n];
mark = 2;
Map<Integer, Integer> areaMap = new HashMap<>();
Set<Integer> neighborIslands = new HashSet<>();
// 标记岛屿并记录面积
for(int i=0; i<m; i++) {
for(int j=0; j<n; j++) {
if(grid[i][j] == 1) {
count = 0;
dfs(grid, i, j, visited);
areaMap.put(mark++, count);
}
}
}
int maxArea = 0;
// 检查每个0
for(int i=0; i<m; i++) {
for(int j=0; j<n; j++) {
if(grid[i][j] == 0) {
neighborIslands.clear();
int total = 1; // 当前0变1
for(int[] dir : dirs) {
int ni = i + dir[0];
int nj = j + dir[1];
if(ni<0||ni>=m||nj<0||nj>=n) continue;
int islandMark = grid[ni][nj];
if(islandMark >= 2 && !neighborIslands.contains(islandMark)) {
neighborIslands.add(islandMark);
total += areaMap.get(islandMark);
}
}
maxArea = Math.max(maxArea, total);
}
}
}
System.out.println(maxArea);
}
}
5.3 实现细节与优化
- 使用mark从2开始编号,避免与0/1冲突
- 使用HashSet记录相邻岛屿避免重复计算
- 全为陆地的情况需要特殊处理
- 时间复杂度O(mn),空间复杂度O(mn)
6. 总结与经验分享
通过这四道题目,我们可以总结出岛屿类问题的通用解法框架:
-
遍历方式选择:
- 矩阵较小:DFS(代码简洁)
- 矩阵较大:BFS(避免栈溢出)
- 特殊需求:并查集(动态连通性)
-
常见处理技巧:
- 方向数组简化代码
- 原地标记减少空间使用
- 逆向思维优化性能
-
易错点警示:
- 边界条件检查(矩阵边缘)
- 访问标记防止重复处理
- 方向定义一致性(上下左右)
在实际面试中,建议先从暴力解法入手,再逐步优化。例如水流问题,先实现O(n²m²)的解法,再引导出O(nm)的优化方案,展示思维过程比直接给出最优解更重要。
