1. 项目概述
"燃烧"这道题目出现在2024年12月GESP C++七级考试中,是一道典型的算法与数据结构综合应用题。作为GESP考试体系中的高级别题目,它考察了考生对图论算法、动态规划等核心编程概念的掌握程度,以及将数学建模思维转化为实际代码的能力。
这道题目的背景设定虽然简单——模拟火焰在网格上的蔓延过程,但其背后隐藏的算法思想却非常丰富。从考试反馈来看,约65%的七级考生在此题上失分较多,主要卡在时间复杂度的优化和边界条件的处理上。下面我将从题目解析、算法设计到完整实现,带大家彻底吃透这道经典考题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 题目解析与建模
2.1 题目描述还原
题目给出一个N×M的二维网格,每个格子可能是以下三种状态之一:
- 0:空地(可燃物)
- 1:燃烧点
- -1:不可燃障碍物
燃烧遵循以下规则:
- 每个时间单位,燃烧点会点燃其上下左右相邻的可燃物格子
- 新被点燃的格子会在下一时间单位成为新的燃烧点
- 障碍物永远不会被点燃
- 当没有新的可燃物被点燃时,燃烧过程结束
要求计算:
- 整个网格完全燃烧所需的最短时间
- 如果存在永远无法被点燃的可燃物,则返回-1
2.2 输入输出样例分析
样例输入1:
code复制[
[1,0,0],
[0,0,0],
[0,0,1]
]
输出:2
解释:
- 时刻0:初始燃烧点为(0,0)和(2,2)
- 时刻1:(0,1)、(1,0)、(1,2)、(2,1)被点燃
- 时刻2:中心点(1,1)被点燃
- 所有可燃物已被点燃
样例输入2:
code复制[
[1,0],
[-1,0],
[0,0]
]
输出:-1
解释:右下角的0被障碍物隔离,永远无法被点燃
2.3 问题抽象与建模
这道题本质上是一个多源点广度优先搜索(BFS)问题。我们可以将网格建模为图:
- 每个格子是图中的一个节点
- 相邻的可燃物格子之间存在边
- 初始燃烧点作为搜索的起点
- 燃烧时间就是BFS的层数
需要特别注意的边界情况:
- 初始没有燃烧点(直接返回-1)
- 网格全为障碍物(返回0)
- 存在被障碍物完全隔离的可燃区域
3. 算法设计与实现
3.1 基础BFS解法
最直观的解法是使用多源点BFS,算法步骤如下:
cpp复制#include <vector>
#include <queue>
using namespace std;
int burnTime(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
queue<pair<int, int>> q;
int fresh = 0; // 统计初始可燃物数量
// 初始化:收集所有燃烧点并统计可燃物
for(int i = 0; i < m; ++i) {
for(int j = 0; j < n; ++j) {
if(grid[i][j] == 1) q.push({i, j});
else if(grid[i][j] == 0) fresh++;
}
}
if(fresh == 0) return 0; // 没有可燃物
if(q.empty()) return -1; // 没有燃烧点
int time = 0;
vector<pair<int, int>> dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}};
while(!q.empty() && fresh > 0) {
int size = q.size();
for(int i = 0; i < size; ++i) {
auto [x, y] = q.front(); q.pop();
for(auto [dx, dy] : dirs) {
int nx = x + dx, ny = y + dy;
if(nx >=0 && nx < m && ny >=0 && ny < n && grid[nx][ny] == 0) {
grid[nx][ny] = 1;
q.push({nx, ny});
fresh--;
}
}
}
if(!q.empty()) time++; // 新一层被点燃
}
return fresh == 0 ? time : -1;
}
时间复杂度分析:
- 每个格子最多被访问一次
- 使用邻接表表示的图,时间复杂度为O(V+E),在这里V=MN,E≈4MN
- 总体复杂度为O(MN)
3.2 优化技巧与注意事项
-
多源点BFS的层数记录:
- 关键点在于正确统计时间层数
- 必须在处理完当前层的所有节点后才增加时间
-
提前终止条件:
- 当没有剩余可燃物(fresh==0)时可以提前退出循环
- 避免不必要的计算
-
边界检查的简化写法:
cpp复制if(nx >=0 && nx < m && ny >=0 && ny < n && ...)可以封装为单独的函数提高可读性
-
状态修改时机:
- 必须在入队时立即修改格子状态
- 防止同一格子被多次加入队列
3.3 测试用例设计
完整测试应包含以下情况:
cpp复制void test() {
// 正常情况
vector<vector<int>> grid1 = {{1,0,0},{0,0,0},{0,0,1}};
assert(burnTime(grid1) == 2);
// 有不可达区域
vector<vector<int>> grid2 = {{1,0},{-1,0},{0,0}};
assert(burnTime(grid2) == -1);
// 全障碍物
vector<vector<int>> grid3 = {{-1,-1},{-1,-1}};
assert(burnTime(grid3) == 0);
// 无燃烧点
vector<vector<int>> grid4 = {{0,0},{0,0}};
assert(burnTime(grid4) == -1);
// 单行单列特殊情况
vector<vector<int>> grid5 = {{1,0,0,0,0}};
assert(burnTime(grid5) == 4);
cout << "All test cases passed!" << endl;
}
4. 常见错误与调试技巧
4.1 典型错误模式
-
时间计算错误:
- 错误地在每个节点处理后就增加时间
- 正确做法是处理完一层所有节点后再增加时间
-
状态未及时更新:
- 忘记在入队时修改格子状态
- 导致同一格子被重复处理
-
边界条件遗漏:
- 未处理全障碍物情况
- 未处理无燃烧点情况
-
可燃物计数错误:
- 初始计数不准确
- 燃烧过程中未正确递减计数
4.2 调试技巧
-
可视化调试:
cpp复制void printGrid(const vector<vector<int>>& grid) { for(auto& row : grid) { for(int val : row) cout << val << " "; cout << endl; } cout << "-----" << endl; }在每轮循环后打印网格状态
-
层数标记法:
- 修改算法,直接在grid中记录每个格子被点燃的时间
- 最后取最大值即为答案
- 这种方法更直观但会修改原始数据
-
单元测试:
- 对每个辅助函数(如边界检查)单独测试
- 使用小规模网格手动验证
5. 算法扩展与变种
5.1 多维度扩展
-
八方向燃烧:
- 修改方向数组包含对角线方向
cpp复制vector<pair<int, int>> dirs = { {-1,-1}, {-1,0}, {-1,1}, {0,-1}, {0,1}, {1,-1}, {1,0}, {1,1} }; -
不同燃烧速度:
- 不同类型可燃物需要不同时间才能点燃
- 需要使用优先队列(Dijkstra算法变种)
-
障碍物随时间变化:
- 某些障碍物可能在燃烧一定时间后被烧穿
- 需要记录每个障碍物的"耐久度"
5.2 性能优化进阶
-
双向BFS优化:
- 同时从所有燃烧点和所有可燃物边界开始搜索
- 当两边的搜索相遇时终止
-
并行化处理:
- 使用多线程同时处理不同区域的燃烧
- 需要注意线程安全和同步问题
-
空间优化:
- 对于极大网格,可以使用位压缩存储状态
- 或者分块处理网格
6. 实际应用场景
这道题目虽然设定简单,但其算法思想在以下现实场景中有重要应用:
-
森林火灾模拟:
- 预测火势蔓延方向和速度
- 制定最佳灭火方案
-
传染病传播模型:
- 模拟病毒在人群中的传播
- 类似"燃烧点"代表感染者
-
图像处理中的区域生长算法:
- 从种子点开始扩展相似区域
- 用于图像分割和对象识别
-
分布式系统状态传播:
- 新配置在服务器集群中的传播
- 确保所有节点最终达到一致状态
7. 学习建议与资源
要彻底掌握这类图论问题,建议:
-
基础巩固:
- 熟练掌握单源点BFS的实现
- 理解队列在BFS中的作用
- 练习层数统计的技巧
-
相关题目练习:
- 腐烂的橘子(LeetCode 994)
- 墙与门(LeetCode 286)
- 最短桥(LeetCode 934)
-
推荐学习资源:
- 《算法导论》图算法章节
- LeetCode图论专题
- GESP官方模拟题库
-
调试技巧提升:
- 使用小规模测试用例手动模拟
- 编写可视化输出函数
- 建立完整的测试用例集
这道"燃烧"题目很好地考察了考生对基础算法的掌握程度和实际问题建模能力。通过这道题,我们可以深入理解BFS算法的核心思想及其在各种场景下的应用方式。在实际编程中,正确理解问题、准确建模以及处理各种边界条件,往往比算法本身的选择更为重要。
