1. 题目解析与解题思路
LeetCode 749题"隔离病毒"是一个典型的图论与模拟类题目,要求我们在二维网格上模拟病毒传播和隔离的过程。题目给定一个表示感染状态的矩阵isInfected,其中1表示被感染的区域,0表示健康区域。我们需要计算隔离所有病毒区域所需的最少防火墙数量。
1.1 问题建模
这道题本质上是在考察:
- 网格中的连通区域识别(类似岛屿问题)
- 模拟扩散过程(类似游戏生命问题)
- 贪心算法策略(每次处理威胁最大的区域)
关键观察点:
- 病毒每晚向相邻的四个方向(上下左右)扩散
- 防火墙只能建在相邻的健康和感染区域之间
- 每天可以选择一个区域进行完全隔离
- 需要最小化防火墙总数
1.2 核心算法选择
经过分析,这个问题适合采用以下算法组合:
- BFS/DFS进行区域感染识别
- 优先队列选择威胁最大的区域
- 模拟多轮扩散和隔离过程
// 伪代码框架 public int containVirus(int[][] isInfected) { int walls = 0; while (存在活跃感染区) { 1. 识别所有感染区域及其威胁程度 2. 选择威胁最大的区域进行隔离 3. 计算该区域需要的防火墙数 4. 模拟其他区域的扩散 5. 更新感染状态 } return walls; }2. 详细实现步骤
2.1 感染区域识别
我们需要为每个感染区域计算两个关键指标:
- 该区域的边界长度(决定防火墙数量)
- 该区域可能感染的健康细胞数量(决定威胁程度)
// 区域识别实现 private Region findMostThreateningRegion(int[][] grid) { int m = grid.length, n = grid[0].length; boolean[][] visited = new boolean[m][n]; Region maxRegion = null; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (grid[i][j] == 1 && !visited[i][j]) { Region region = new Region(); dfs(grid, i, j, visited, region); if (maxRegion == null || region.threat > maxRegion.threat) { maxRegion = region; } } } } return maxRegion; }2.2 防火墙计算与隔离
隔离一个区域需要:
- 标记该区域为已隔离(设为-1)
- 统计其与健康区域的相邻边数
private int isolateRegion(int[][] grid, Region region) { int walls = 0; for (int[] cell : region.infectedCells) { int i = cell[0], j = cell[1]; grid[i][j] = -1; // 标记为已隔离 // 检查四个方向 for (int[] dir : DIRECTIONS) { int x = i + dir[0], y = j + dir[1]; if (x >= 0 && x < grid.length && y >= 0 && y < grid[0].length && grid[x][y] == 0) { walls++; } } } return walls; }2.3 病毒扩散模拟
每晚未被隔离的感染区域会向四周扩散:
private void spreadVirus(int[][] grid) { int m = grid.length, n = grid[0].length; List<int[]> newInfections = new ArrayList<>(); for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (grid[i][j] == 1) { for (int[] dir : DIRECTIONS) { int x = i + dir[0], y = j + dir[1]; if (x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == 0) { newInfections.add(new int[]{x, y}); } } } } } for (int[] cell : newInfections) { grid[cell[0]][cell[1]] = 1; } }3. 完整算法实现
将上述模块组合起来,得到完整解决方案:
class Solution { private static final int[][] DIRECTIONS = {{-1,0}, {1,0}, {0,-1}, {0,1}}; class Region { List<int[]> infectedCells = new ArrayList<>(); Set<String> threatenedCells = new HashSet<>(); int threat = 0; int wallsRequired = 0; } public int containVirus(int[][] isInfected) { int walls = 0; while (true) { Region region = findMostThreateningRegion(isInfected); if (region == null) break; walls += isolateRegion(isInfected, region); spreadVirus(isInfected); } return walls; } // 前面定义的各个方法... }4. 复杂度分析与优化
4.1 时间复杂度
设网格大小为m×n:
- 每轮需要O(mn)时间扫描整个网格
- 最多可能进行O(mn)轮(每次隔离至少一个单元格)
- 因此最坏时间复杂度为O((mn)^2)
4.2 空间复杂度
主要消耗在:
- 访问标记数组O(mn)
- 区域存储O(mn)
- 总体O(mn)
4.3 优化方向
- 并行处理:可以同时识别所有区域而非每次找最大
- 增量更新:只处理边界变化而非全图扫描
- 更高效的数据结构:如并查集管理感染区域
5. 测试用例与边界情况
5.1 典型测试用例
@Test public void testContainVirus() { Solution solution = new Solution(); // 基础用例 int[][] grid1 = { {0,1,0,0,0,0,0,1}, {0,1,0,0,0,0,0,1}, {0,0,0,0,0,0,0,1}, {0,0,0,0,0,0,0,0} }; assertEquals(10, solution.containVirus(grid1)); // 完全隔离 int[][] grid2 = {{1,1,1},{1,0,1},{1,1,1}}; assertEquals(4, solution.containVirus(grid2)); // 无感染 int[][] grid3 = {{0,0,0},{0,0,0},{0,0,0}}; assertEquals(0, solution.containVirus(grid3)); }5.2 边界情况处理
- 网格为空或单元素
- 全部感染或全部健康
- 感染区域形成特殊形状(L型、环形等)
- 大规模网格(性能测试)
6. 常见错误与调试技巧
6.1 典型错误
- 无限循环:未正确判断终止条件
- 防火墙计数错误:重复计算或遗漏边界
- 扩散控制不当:新感染区域不应参与当晚扩散
6.2 调试建议
- 可视化中间状态:打印每轮后的网格
- 添加详细日志:记录每个区域的威胁值和防火墙数
- 单元测试:先确保各子模块正确性
关键提示:在扩散阶段,一定要先收集所有新感染位置再统一更新,避免同一轮中多次感染同一单元格。
7. 算法变种与扩展
7.1 问题变种
- 不同隔离成本:不同方向的防火墙成本不同
- 动态传播速率:不同区域传播速度不同
- 多类型病毒:需要区分不同病毒类型
7.2 实际应用场景
- 疫情传播控制模拟
- 网络安全中的威胁隔离
- 生态系统中物种扩散控制
8. 个人实现心得
在实际编码中,我发现以下几点特别重要:
- 使用单独的类管理区域信息比维护多个数据结构更清晰
- 在扩散阶段使用批量更新可以避免很多边界问题
- 威胁值的计算方式直接影响算法效率,需要仔细设计
一个容易忽略的细节是:已隔离区域(标记为-1)不应再参与后续处理。我在最初实现时因为这个错误导致结果偏大。