蓝桥杯真题解析:用BFS与Flood Fill解决岛屿淹没问题

📅 2026/8/26 8:37:40
蓝桥杯真题解析:用BFS与Flood Fill解决岛屿淹没问题
1. 这道题不是考“天气”是考你能不能把地图“烧穿”“全球变暖”这四个字一出来很多人下意识以为要算碳排放、看温度曲线、画气候模型——结果点开蓝桥杯国赛真题一看题目里连一滴水、一片冰都没有只有一张由#陆地和.海洋组成的二维网格。你得写代码模拟海平面上升后哪些岛屿会被完全淹没哪些还能露出水面。这题的官方编号是题目 1459出自2013年第四届蓝桥杯国赛但直到今天它仍是BFS与Flood Fill类题目的“教科书级范本”。核心关键词就两个BFS和Flood Fill。前者是广度优先搜索一种按层“推开”的遍历策略后者是“洪水填充”本质是用BFS或DFS把连通区域一次性染色或标记。这道题不考数学公式不考物理建模考的是你对“连通性”这个底层概念的直觉——哪片陆地是孤岛哪片陆地和海岸线接壤接壤的陆地涨潮时必然被冲垮不接壤的才叫“真正存活下来的岛屿”。换句话说判断一个陆地区域是否会被淹没等价于判断它是否与边界相连。适合谁来啃这道题如果你正在准备蓝桥杯单片机、嵌入式或Python组别尤其在刷真题阶段这题必须吃透。它短小输入最多100×100逻辑干净但陷阱密集——比如容易忽略“岛屿内部有海洞”这种结构或者误判“角落陆地是否临海”。我带过三届蓝桥杯集训队每年都有学生卡在这题的第3个测试用例上不是算法不会而是没想清楚“临海”的定义到底是什么。这篇文章不讲PPT式原理直接从读题、建模、编码、调试到优化全程复现我当年手敲AC的全过程包括编译器报错时怎么快速定位、本地测不出边界问题时怎么加日志、甚至考场环境下如何用纸笔模拟BFS队列。所有内容都是我在真实训练场景中反复验证过的硬核经验。2. 题目拆解为什么非得用BFSDFS不行吗2.1 题干还原与关键约束提炼先还原原始题干已做语义精简保留全部约束你有一张 N×M 的地图每个格子是#陆地或.海洋。全球变暖导致海平面上升所有与海洋直接相邻上下左右的陆地都会被淹没变成海洋。被淹没的陆地又可能使原本不临海的陆地变成新临海的陆地从而继续被淹没……这个过程持续进行直到没有新的陆地被淹没为止。问最终还剩下多少块“孤立”的陆地区域即不与任何海洋格子相邻的#区域注意三个关键点淹没是传播式的不是只淹第一层而是像多米诺骨牌一样层层推进判定标准是“是否与海洋相邻”这里的“海洋”包括原始海洋.也包括已被淹没的陆地即新生成的.最终统计的是“剩余陆地区域数量”不是剩余格子数——一块由10个#连成的岛只要没被全淹就算1块。这就排除了简单遍历的可能。你不能只扫一遍就完事因为A格子被淹后可能让B格子突然变成临海B再被淹又影响C……这个链式反应必须被完整模拟。2.2 BFS vs DFS选型背后的工程权衡很多初学者会问“DFS递归也能一层层走啊为啥非得BFS”答案藏在“传播顺序”和“内存安全”里。BFS天然匹配“层序淹没”逻辑第1轮所有初始临海的#变成.第2轮所有与第1轮新.相邻的#变成.第3轮所有与第2轮新.相邻的#变成.……这种“按轮次推进”的过程BFS的队列结构先进先出完美对应。你把所有第i轮要处理的格子塞进队列处理完这一批再把它们产生的新待处理格子塞进去——逻辑清晰无歧义。DFS在此场景下极易栈溢出或逻辑错乱假设你用DFS从某个临海#开始递归它会一路深挖到岛屿最中心中途可能把本该属于第2轮处理的格子提前标为已访问导致后续轮次漏判。更致命的是N100时DFS最坏递归深度可达10000远超多数OJ平台的栈空间限制通常1MB直接RERuntime Error。而BFS用显式队列内存可控且可随时中断调试。提示蓝桥杯单片机组别考生尤其要注意——嵌入式环境栈空间极小常仅几KBDFS在这种传播类问题中是明确的反模式。哪怕你用循环模拟DFS逻辑复杂度也远高于BFS。2.3 Flood Fill的本质不是“填色”是“连通域隔离”Flood Fill常被误解为“Photoshop里的油漆桶工具”但在这道题里它的作用恰恰相反不是把一片区域染成同色而是把“注定要消失”的区域精准剥离出来。具体来说我们要做的Flood Fill分两步反向标记“必淹区”从所有边界上的海洋格子即地图四边的.出发用BFS向外扩展把所有能到达的陆地格子标记为“将被淹没”。为什么从海洋出发因为“临海”是淹没的充要条件而边界海洋是所有淹没路径的源头。正向统计“幸存区”遍历全图对未被标记的#格子用另一次BFS/DFS找出其所在连通域并计数。这个“反向标记正向统计”的双阶段设计是本题最优解的核心。它避免了模拟多轮淹没的繁琐迭代把动态过程转化为静态连通性分析——这才是Flood Fill在此类问题中的高阶用法。3. 实操细节从读题到AC的完整链路3.1 输入解析与地图建模别在第一步就翻车蓝桥杯真题输入格式非常“朴实”第一行是N和M接下来N行每行M个字符#或.。但实际编码时有三个易错点数组索引与坐标系混淆题目说“第i行第j列”但C/C/Python中二维列表是grid[i][j]i是行号j是列号。而人类习惯的“坐标(x,y)”通常是x为列、y为行。我建议统一用grid[r][c]rrow, ccol并在注释里写明“r0是顶行c0是左列”避免后期方向判断出错。边界格子的预处理所有位于第0行、第N-1行、第0列、第M-1列的格子如果值为.就是“初始海洋”必须加入BFS起点队列。但注意这些格子本身是海洋不参与淹没只是传播源。代码里要严格区分“源点”和“被淹没点”。内存布局优化针对单片机组如果你用蓝桥杯单片机开发板如IAP15F2K61S2RAM只有2KB。此时不能开int visited[100][100]占10KB而要用位图压缩uint8_t visited_map[100*100/8 1]用位运算查/设状态。Python组虽无此忧但养成这种思维对理解底层很有帮助。# Python参考实现清晰版非最优内存 import sys from collections import deque def main(): data sys.stdin.read().splitlines() if not data: return n, m map(int, data[0].split()) grid [] for i in range(1, 1n): grid.append(list(data[i].strip())) # 步骤1初始化visited数组False表示未访问 visited [[False] * m for _ in range(n)] # 步骤2收集所有边界海洋作为BFS起点 queue deque() for r in [0, n-1]: for c in range(m): if grid[r][c] .: visited[r][c] True queue.append((r, c)) for c in [0, m-1]: for r in range(1, n-1): # 避免角点重复加入 if grid[r][c] .: visited[r][c] True queue.append((r, c))这段代码里queue.append((r,c))后立刻visited[r][c]True是关键——防止同一格子被多次加入队列这是BFS效率的基石。3.2 BFS传播四方向移动与状态更新的精确控制BFS的核心是“从当前格子出发检查上下左右四个邻居”。但这里有个隐藏陷阱邻居必须是陆地#且未被访问过。因为只有陆地才会被淹没海洋不用处理且已访问过的格子说明已被标记为“必淹”无需重复操作。方向数组是标配但写法有讲究# 推荐写法用元组列表语义清晰 directions [(0, 1), (1, 0), (0, -1), (-1, 0)] # 右、下、左、上 # 不推荐用二维数组或魔法数字易错 # directions [[0,1],[1,0],[0,-1],[-1,0]] # 功能相同但可读性差BFS主循环的骨架如下while queue: r, c queue.popleft() for dr, dc in directions: nr, nc r dr, c dc # 检查新坐标是否越界 if 0 nr n and 0 nc m: # 检查是否为陆地且未访问 if grid[nr][nc] # and not visited[nr][nc]: visited[nr][nc] True queue.append((nr, nc))注意visited[nr][nc] True必须在queue.append之前执行否则同一格子可能被多个父节点同时发现重复入队导致BFS退化为O(N²)甚至死循环。这是我在调试时踩过最痛的坑——某次本地测试数据小没暴露一交OJ就TLETime Limit Exceeded。3.3 幸存岛屿计数二次BFS的启动时机与终止条件第一轮BFS结束后visited数组里True的位置就是所有“将被淹没”的陆地包括原始临海陆地和被传播淹没的陆地。剩下的#格子就是潜在幸存者。此时启动第二轮BFS遍历全图遇到grid[r][c]#且not visited[r][c]就从此点开始一次新BFS把整个连通域的visited全部标为True这次是“已统计”和第一轮的“将淹没”含义不同并计数器ans 1。关键细节必须重用visited数组不要新开数组。第一轮标记“必淹”第二轮标记“已统计”用同一个布尔数组即可。这样内存省一半且避免同步错误。第二轮BFS不修改grid只改visited因为grid是只读输入。统计完直接输出ans。边界检查仍需严谨第二轮BFS的邻居检查同样要0nrn and 0ncm且grid[nr][nc]# and not visited[nr][nc]。ans 0 for r in range(n): for c in range(m): if grid[r][c] # and not visited[r][c]: ans 1 # 启动新BFS标记整个连通域 queue2 deque([(r, c)]) visited[r][c] True while queue2: cr, cc queue2.popleft() for dr, dc in directions: nr, nc cr dr, cc dc if 0 nr n and 0 nc m: if grid[nr][nc] # and not visited[nr][nc]: visited[nr][nc] True queue2.append((nr, nc)) print(ans)这段代码里queue2是局部变量每次新岛屿都新建避免残留状态。visited[r][c] True在ans 1后立即执行确保该点不会被后续外层循环再次触发。3.4 完整可运行代码与本地测试技巧以下是整合后的完整Python代码已通过蓝桥杯OJ验证import sys from collections import deque def solve(): data sys.stdin.read().splitlines() if not data: print(0) return n, m map(int, data[0].split()) grid [] for i in range(1, 1n): grid.append(list(data[i].strip())) if n 0 or m 0: print(0) return visited [[False] * m for _ in range(n)] directions [(0, 1), (1, 0), (0, -1), (-1, 0)] queue deque() # 第一步将所有边界海洋加入队列 for r in [0, n-1]: for c in range(m): if grid[r][c] .: visited[r][c] True queue.append((r, c)) for c in [0, m-1]: for r in range(1, n-1): if grid[r][c] .: visited[r][c] True queue.append((r, c)) # 第二步BFS传播标记所有将被淹没的陆地 while queue: r, c queue.popleft() for dr, dc in directions: nr, nc r dr, c dc if 0 nr n and 0 nc m: if grid[nr][nc] # and not visited[nr][nc]: visited[nr][nc] True queue.append((nr, nc)) # 第三步统计幸存岛屿数量 ans 0 for r in range(n): for c in range(m): if grid[r][c] # and not visited[r][c]: ans 1 # BFS标记整个连通域 queue2 deque([(r, c)]) visited[r][c] True while queue2: cr, cc queue2.popleft() for dr, dc in directions: nr, nc cr dr, cc dc if 0 nr n and 0 nc m: if grid[nr][nc] # and not visited[nr][nc]: visited[nr][nc] True queue2.append((nr, nc)) print(ans) if __name__ __main__: solve()本地测试技巧蓝桥杯OJ不提供详细错误信息所以本地测准至关重要。我习惯用以下三组数据验证最小Case1 1\n#→ 输出1单格陆地不临海幸存边界Case3 3\n###\n#.#\n###→ 输出1中间是海洞四周陆地形成环全部临海但环内陆地不临海应幸存1块传播Case4 4\n....\n#.#.\n#.#.\n....→ 输出0四个角的#全部临海被淹没用Python的sys.stdin模拟输入时可写# 临时替换stdin import io sys.stdin io.StringIO(3 3\n###\n#.#\n###) solve()4. 常见问题与避坑指南那些年我们交过的学费4.1 典型错误模式速查表错误现象可能原因快速定位方法修复方案样例通过OJ WAWrong Answer忽略了“角点重复加入”导致边界海洋被多次处理在第一轮BFS开头加print(len(queue))看是否等于实际边界海洋数按3.1节代码对四边分别处理角点只计入一次OJ TLE超时BFS入队前未标记visited导致同一格子反复入队在queue.append前加print(fenqueue {nr},{nc})观察是否大量重复严格遵守visited[nr][nc]True在append之前OJ RE栈溢出用了DFS且未限制深度或递归太深本地用sys.setrecursionlimit(10000)测试看是否崩溃改用BFS或DFS加深度剪枝不推荐输出为0但预期0第二轮BFS的条件写成grid[nr][nc].而非#检查第二轮BFS的if判断打印grid[nr][nc]值仔细核对字符#是陆地.是海洋4.2 真实考场应急策略蓝桥杯国赛是封闭环境不能联网查文档。我给学生的三条铁律纸笔模拟BFS队列考前准备一张方格纸把样例地图画出来手动模拟第一轮BFS的队列变化记录(r,c)入队/出队顺序。这比背代码管用十倍能瞬间发现逻辑漏洞。变量命名即注释不要写a,b,c而用queue_flood,visited_sunk,ans_islands。阅卷系统不看变量名但你自己调试时能省5分钟。边界检查写死if r 0 or r n or c 0 or c m:比if not (0rn and 0cm):更不易出错因为短路逻辑在极端情况下可能失效虽然Python里很少但保险起见。4.3 单片机/嵌入式组特别注意事项如果你用蓝桥杯单片机开发板基于8051或STM32这段代码需大幅改造队列用静态数组模拟int queue_r[MAX_SIZE], queue_c[MAX_SIZE]; int head0, tail0;方向数组用宏定义#define DIR_NUM 4const int dr[DIR_NUM] {0,1,0,-1}; const int dc[DIR_NUM] {1,0,-1,0};内存极致压缩visited用bit数组#define GET_BIT(arr,i) ((arr[(i)/8] ((i)%8)) 1)避免浮点与除法所有坐标计算用位运算如r*100c当作唯一ID因M≤100我曾帮学生把这题移植到IAP15F2K61S2上最终ROM占用8KBRAM512B完全满足竞赛要求。关键不是功能而是确定性——嵌入式环境里任何不确定行为如动态内存分配都是灾难。4.4 进阶思考这道题还能怎么变掌握本题后可以自然延伸到三类高频变体带权重的淹没每个#有海拔高度海平面以固定速率上升问t时刻剩余岛屿数。解法把BFS改为优先队列Dijkstra按海拔从小到大处理。多源最短距离求每个#格子到最近海洋的距离。解法所有海洋格子同时入队BFS过程中记录dist[r][c]。动态岛屿合并海平面下降海洋变陆地问新增连通域数。解法逆向思维用并查集Union-Find从全海开始逐步加陆地。这些变体在近年蓝桥杯EDA组、Python组真题中已陆续出现。吃透“全球变暖”相当于拿到了连通性问题的万能钥匙。5. 我的实战心得为什么这道题值得你反复刷这道题我带学生刷过至少五轮。第一轮大家盯着代码调语法第二轮开始讨论BFS和DFS的取舍第三轮有人提出用并查集替代BFS第四轮我们对比了Python、C、汇编三种实现的性能差异第五轮学生自己出了个“全球变冷”版本——冰川扩张陆地变冰问最终冰盖连通数。为什么值得反复刷因为它把抽象算法、具象建模、工程落地、边界思维全揉在了一张100×100的网格里。你写下的每一行if判断都在回答一个现实问题“这块地到底算不算岛”——而答案永远取决于你如何定义“临海”。最后分享一个小技巧下次看到类似题先问自己三个问题“传播源在哪里”本题是边界海洋“传播的条件是什么”本题是相邻且为陆地“最终要统计什么”本题是剩余连通域数把这三个问题的答案写在草稿纸上代码框架就出来了。算法不是背出来的是在一次次“定义问题”的过程中长出来的。这道题教会我的从来不是BFS怎么写而是——所有复杂问题拆到最后不过是一次清晰的定义。