FEATURED · 精选文章

蓝桥杯国赛真题解析:多源BFS模拟网格扩散问题

发布时间 / 2026/8/29 9:05:05
来源 / 创域科博编辑部
栏目 / 资讯中心
蓝桥杯国赛真题解析:多源BFS模拟网格扩散问题 1. 项目概述从一道国赛真题看“扩散”问题的本质最近在整理历年蓝桥杯国赛的真题翻到了第十一届C语言大学B组的B题“扩散”。这道题挺有意思的它不像传统的算法题那样直接让你排序、搜索或者动态规划而是构建了一个生动的“模拟”场景。题目描述了一个在无限大网格中有若干个初始点会向上下左右四个方向同时扩散问你经过多少时间后会有多少个格子被覆盖。这听起来是不是有点像墨水在纸上晕开或者病毒在人群中传播的简化模型没错这道题的核心就是模拟一个多源点的同步扩散过程并计算最终被“感染”的格子总数。对于刚接触这类模拟题的同学来说可能会有点懵。直接暴力模拟网格是无限的时间也可能很长这显然行不通。但如果你对广度优先搜索BFS和曼哈顿距离有比较深的理解就会发现这道题其实是一个经典的“多源点最短路径”问题或者说是计算每个格子被离它最近的初始点“扩散”到所需的时间。理解了这一点解题的钥匙就找到了。这篇文章我就来详细拆解这道“扩散”题不仅讲清楚怎么做更重点分析为什么这么做以及在实际编码中会遇到哪些坑怎么避开。无论你是正在备赛蓝桥杯还是对算法模拟感兴趣相信都能从中获得启发。2. 问题核心与数学模型抽象2.1 题目场景还原与关键约束我们先来把题目场景具象化。想象一张无限大的方格纸二维平面网格上面有若干个已经涂黑的点我们称之为“初始源点”。从时刻0开始每一个黑点在每一个单位时间内都会把自己上下左右四个相邻的格子也染黑。新被染黑的格子在下一个单位时间也会拥有同样的扩散能力。这个过程会一直持续下去。题目最终问的是在经过指定的时间 t 后整个无限大的网格中有多少个格子被染黑了这里有几个关键约束需要立刻明确无限大网格我们不能真的在程序里开辟一个无限大的数组。这提示我们必须找到一种不依赖于显式模拟整个网格空间的计算方法。同步扩散所有源点同时开始且扩散速度恒定一个单位时间移动一格。这意味着一个格子可能被多个源点同时扩散到但它只需要被扩散到一次就会被染黑。扩散规则仅限于上下左右四个方向曼哈顿距离意义上的相邻不能走斜线。看到“无限大”、“同时扩散”、“四个方向”你的脑海里应该立刻浮现出一个概念曼哈顿距离。对于一个坐标为 (x, y) 的格子它到某个源点 (sx, sy) 的曼哈顿距离是 |x - sx| |y - sy|。这个距离的物理意义就是从源点出发只走上下左右到达这个格子所需要的最少步数时间。那么这个格子被染黑的时间是不是就是它到所有源点中最近的那个源点的曼哈顿距离呢是的因为扩散是同步的离它最近的源点会最先“到达”它。一旦到达格子即被染色其他更远的源点后续再“到达”时已经不影响结果了。因此原问题被完美地转化为了一个数学模型计算在无限网格中所有到任意一个初始源点的曼哈顿距离不超过时间 t 的格子的数量。2.2 从暴力模拟到思维跃迁最朴素的想法是模拟。设定一个足够大的二维数组标记初始点然后每分钟遍历所有黑点将其四周变黑。但“足够大”是多大时间t可能很大扩散范围是以源点为中心t为边长的菱形区域。多个源点区域还会重叠。数组大小很难预估且模拟过程的时间复杂度是 O(k * t^2) 级别的k是源点数效率极低对于较大的t根本无法计算。我们必须摒弃“模拟过程”的思维转向“计算状态”的思维。我们不再关心格子是如何一步步被染黑的我们只关心最终状态一个格子是否被染黑。而根据上面的分析判断依据非常简单是否存在一个初始源点使得该格子到该源点的曼哈顿距离 t。所以解题的核心就变成了如何高效地枚举或计算满足上述条件的格子数这里通常有两种思路离散化区域扫描由于源点很少题目一般是4个我们可以计算出每个源点的影响范围一个菱形然后求这些菱形的并集面积。这涉及到计算几何中多边形的并集实现起来较为复杂。BFS搜索边界既然我们关心的是距离源点t范围内的点那么我们可以从所有源点同时开始做BFS只搜索到第t层为止。BFS天然保证了搜索的层数就是曼哈顿距离。这种方法更直观也更容易处理多个源点区域的重复计数问题通过一个访问标记数组去重即可。显然第二种BFS的思路更贴近编程竞赛的常规解法也更易于实现和调试。接下来我们就深入探讨这种解法的具体实现和优化细节。3. 基于BFS的算法实现与细节剖析3.1 算法框架设计与数据结构选择我们选择BFS广度优先搜索作为核心算法。可以将每个网格点看作图的一个节点上下左右相邻的点之间有边。我们从所有的初始源点同时开始BFS。数据结构选择队列 (Queue)用于BFS的标准数据结构。我们需要将初始的源点全部放入队列。访问标记与距离记录我们需要一个高效的方式来记录一个坐标是否被访问过以及它是在第几轮距离被访问的。由于坐标范围可能很大向四个方向扩散t使用标准的二维数组visited[x][y]可能内存过大。这里有两个常用技巧使用std::pair或结构体结合std::set或std::unordered_set来存储已访问的点。查找效率是O(log n)或平均O(1)。使用std::map或std::unordered_map将坐标(x, y)映射到距离d。map基于红黑树unordered_map基于哈希表后者平均效率更高。对于蓝桥杯的C语言组虽然标题是C语言组但实际参赛多用C因为STL方便我们通常用C的queue和unordered_map或map来实现。算法核心步骤初始化将所有初始源点(sx_i, sy_i)放入队列并在visited_map中标记这些点的距离为0。BFS循环当队列非空且当前扩展的轮次距离 t时进行循环。 a. 取出队首节点(x, y)获取其距离d。 b. 如果d t说明这个点正好在边界上从它出发的扩散不会增加新的在t时间内的点因为新点距离会是t1。所以对于d t的点我们只计数不继续扩展其邻居。 c. 如果d t则遍历其上下左右四个邻居(nx, ny)。 d. 如果(nx, ny)未被访问过不在visited_map中则将其距离标记为d1并放入队列。统计结果BFS结束后visited_map中存储的所有点的数量就是被染黑的格子总数。因为BFS保证了我们访问了所有距离源点 t的点且每个点只访问一次。3.2 坐标映射与去重关键这里有一个至关重要的细节如何表示一个点以确保去重正确在BFS中从不同源点扩散可能会到达同一个格子。我们必须确保这个格子只被计数一次。使用visited_map正好解决了这个问题。无论从哪个方向、哪个源点第一次到达这个格子我们将其标记后其他路径再次到达时就会因为已访问而跳过。坐标处理技巧由于网格是无限的坐标可能是负数。使用std::pairint, int作为键是直接的方式。为了提升unordered_map的性能我们可以自定义哈希函数或者直接使用map虽然查找是O(log n)但对于本题规模通常足够。#include iostream #include queue #include unordered_map #include utility using namespace std; typedef pairint, int Point; // 简单的哈希函数可选如果使用unordered_map struct pair_hash { template class T1, class T2 std::size_t operator () (const std::pairT1, T2 p) const { auto h1 std::hashT1{}(p.first); auto h2 std::hashT2{}(p.second); // 一个简单的组合方式注意哈希碰撞概率 return h1 ^ (h2 1); } }; // 方向数组表示上下左右四个方向 int dirs[4][2] {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; int main() { // 假设初始源点坐标和时间t vectorPoint sources {{0,0}, {2020,11}, {11,14}, {2000,2000}}; // 示例坐标 int t 2020; // 示例时间 queuepairPoint, int q; // 队列存储 (点 距离) unordered_mapPoint, int, pair_hash visited; // 记录点及其距离 // 1. 初始化所有源点入队 for (auto src : sources) { q.push({src, 0}); visited[src] 0; } long long ans 0; // 使用long long防止溢出 // 2. BFS循环 while (!q.empty()) { auto [curPos, dist] q.front(); q.pop(); ans; // 每处理一个点就计数一个注意这是在出队时计数 // 如果已经达到时间t则不再从该点向外扩展 if (dist t) { continue; } // 遍历四个邻居 for (auto d : dirs) { int nx curPos.first d[0]; int ny curPos.second d[1]; Point nextPos {nx, ny}; // 如果邻居未被访问过 if (visited.find(nextPos) visited.end()) { visited[nextPos] dist 1; q.push({nextPos, dist 1}); } } } cout 在时间 t 后被覆盖的格子数量为: ans endl; return 0; }注意上面的代码将计数ans放在了出队处理时。这是正确的因为BFS队列保证了每个点只会入队一次得益于visited map的检查所以出队时计数恰好每个被访问的点计一次数。也有写法是在入队时计数但需要更小心的初始化。3.3 边界判断与循环终止条件在无限网格中BFS理论上会一直进行下去。我们必须明确循环终止条件只扩散t个单位时间。在代码中我们通过两个机制来控制入队/出队时的距离判断当从队列中取出的点其距离dist已经等于t时就不再从这个点向外扩展邻居。因为扩展出的邻居距离将是t1超出了时间限制。队列的自然消耗所有距离 t的点都会被访问并入队。当所有这样的点都被处理完毕后队列变空BFS循环结束。这里有一个易错点为什么是dist t时停止扩展而不是dist t因为距离为t的点本身是在时间t被覆盖的是有效的应该被计数。但从它出发需要再花1个单位时间才能覆盖邻居那时已经是时间t1了超过了题目要求的时间范围所以不应该再扩展。因此判断条件是if (dist t) continue;。4. 性能优化与潜在问题深究4.1 算法复杂度与可行性分析假设初始源点数量为k扩散时间为t。那么最终被覆盖的格子数大约在O(k * t^2)量级每个源点影响一个菱形区域面积与t^2成正比。我们的BFS会访问其中的每一个格子。时间复杂度每个格子访问一次每次访问需要常数时间进行队列操作和map查找/插入。因此总时间复杂度约为O(N)其中N是最终被覆盖的格子数即O(k * t^2)。对于t2020,k4N大约是4 * 2020^2 ≈ 1600万这个数量级。BFS配合哈希表是完全可以接受的。空间复杂度主要消耗在visited_map和队列q上它们存储的数量也都是O(N)。对于1600万级别的点每个点存储一对int和距离内存占用大约在几百MB量级这在竞赛环境通常内存限制256MB或512MB下是有风险的可能接近或超出限制。4.2 内存优化策略与取舍这是本题实现中的一个核心挑战。直接使用unordered_mappairint,int, int存储所有点内存开销较大。我们可以考虑以下优化使用std::map替代unordered_mapmap基于红黑树每个节点内存开销通常比unordered_map的桶结构加链表/红黑树节点要小一些但查找效率是O(log N)。在数据量极大时map的内存优势可能体现出来但时间会变慢。需要测试。坐标压缩离散化这是解决内存问题的根本性方法。我们发现所有有效的点都位于初始源点坐标± t的范围内。我们可以将所有可能出现的x坐标和y坐标分别收集起来排序去重然后用它们在排序后数组中的索引一个较小的整数来代表原始坐标。这样我们就可以用一个大小为(2t * k)^2的二维布尔数组来标记访问但经过压缩后这个数组会小得多。不过离散化本身需要扫描所有潜在点实现起来比直接BFS复杂。使用更紧凑的数据结构例如将坐标(x, y)编码成一个64位整数((long long)x 32) | (y 0xffffffff)然后使用unordered_setlong long只存储是否访问而不存距离。距离信息可以通过BFS的层数来控制需要将层数信息与坐标一起存入队列。这能节省一些内存。对称性剪枝如果适用如果初始源点分布具有对称性理论上可以只计算一部分区域然后乘以倍数。但本题的源点坐标是任意的一般不具备这种对称性。对于蓝桥杯赛场上的策略如果时间t不大比如几百直接BFSunordered_map是最快最稳的。如果t很大比如2020就需要谨慎评估。通常国赛题目的数据是经过设计的t2020和给定的四个点用优化的BFS如编码为long long是可以在内存限制内通过的。但必须意识到内存是瓶颈。4.3 数值溢出与精度问题本题结果可能是一个很大的数。k4, t2020每个源点影响的菱形区域格子数约为2*t*(t1)1四个区域并集可能接近这个数值的4倍减去重叠部分最终结果在千万级别。使用int类型通常32位最大约21亿存储计数是足够的。但为了安全起见尤其是在中间计算或更大规模时使用long long(C) 或int64_t是更稳妥的做法。5. 代码实现全解析与调试技巧5.1 完整代码实现C带注释下面给出一个考虑了内存优化64位编码和稳健性的实现版本。#include bits/stdc.h using namespace std; // 方向数组上、下、右、左 const int dx[4] {0, 0, 1, -1}; const int dy[4] {1, -1, 0, 0}; // 将坐标(x,y)编码成一个64位整数 inline long long encode(int x, int y) { // 注意这里需要先将int转换为long long再移位防止溢出 return ((long long)x 32) | (y 0xffffffffLL); } int main() { // 第十一届国赛B组B题数据 vectorpairint, int sources {{0, 0}, {2020, 11}, {11, 14}, {2000, 2000}}; int t 2020; // 扩散时间 queuepairlong long, int q; // 队列存储(编码后的坐标, 距离) unordered_setlong long visited; // 只记录是否访问不记录距离 // 初始化队列和已访问集合 for (auto [sx, sy] : sources) { long long code encode(sx, sy); q.push({code, 0}); visited.insert(code); } long long ans 0; // 最终结果使用long long while (!q.empty()) { auto [code, dist] q.front(); q.pop(); ans; // 出队时计数每个点只计一次 // 如果已达到最大时间不再扩展 if (dist t) { continue; } // 解码出坐标x, y int x (int)(code 32); int y (int)(code 0xffffffff); // 遍历四个方向 for (int i 0; i 4; i) { int nx x dx[i]; int ny y dy[i]; long long ncode encode(nx, ny); // 如果新点未被访问过 if (visited.find(ncode) visited.end()) { visited.insert(ncode); q.push({ncode, dist 1}); } } } cout ans endl; return 0; }5.2 关键代码段解读与避坑指南编码与解码函数encode将两个32位int合成一个64位long long。高位放x低位放y。关键点(long long)x 32中的(long long)强制转换至关重要。如果x是int直接x 32会发生移位溢出因为int只有32位结果是未定义的。转换为long long后移位在64位环境下进行。解码时(int)(code 32)取回高位x(int)(code 0xffffffff)取回低位y。注意0xffffffff需要写为0xffffffffLL以确保是64位常量避免符号扩展问题。使用unordered_setlong long替代unordered_map我们只关心点是否被访问过距离信息由BFS队列维护每个队列元素都存储了该点的距离。这节省了大约一半的内存因为map需要存储键值对。队列元素类型为pairlong long, int包含了编码后的坐标和距离。循环终止与计数时机ans在出队时执行。这保证了每个从队列中出来的点都被计数一次。由于我们通过visited集合保证了每个点只入队一次所以这个计数是准确的。扩展邻居的条件是dist t通过if (dist t) continue;实现。距离等于t的点是有效的但不扩展。5.3 调试与验证方法对于这类模拟/搜索题调试至关重要。小数据验证用小的t如12和简单的源点如{(0,0)}手动计算结果与程序输出对比。t0 只有源点本身结果应为1。t1 源点(0,0)覆盖点 (0,0), (1,0), (-1,0), (0,1), (0,-1) 结果应为5。编写一个简单的函数打印出被访问的点坐标直观检查。验证去重设置两个很近的源点比如{(0,0), (1,0)}t1。手动计算重叠区域确保程序计数正确。内存与时间监控对于t较大的情况在本地运行时可粗略监控。如果程序运行异常缓慢或内存激增可能是算法有误如死循环或数据结构选择不当。利用对称性检验如果所有源点关于原点对称那么结果可能具有规律。但这道题的源点坐标是特定的不具备明显对称性。6. 举一反三问题变体与拓展思考“扩散”模型是算法竞赛中的一个经典问题原型。理解了这个基础版本我们可以探讨其变体和拓展方向。6.1 变体一扩散速度不同或规则变化不同速度如果每个源点的扩散速度不同例如有的点一次扩散一格有的点一次扩散两格。这就不再是简单的曼哈顿距离了。解决方案可以修改BFS队列使用优先队列最小堆即Dijkstra算法。每个边的“权重”是时间从源点出发求“到达”每个格子的最短时间最后统计时间 t的格子数。八方向扩散如果扩散规则包括斜角方向八连通。那么一个格子到源点的最短距离就变成了切比雪夫距离max(|x-sx|, |y-sy|)。BFS算法依然适用只需将方向数组从4个方向改为8个方向即可。有障碍物的扩散在某些格子存在障碍物无法被扩散或阻碍扩散。这变成了一个标准的带障碍物的多源BFS问题。在遍历邻居时需要检查邻居格子是否为障碍物。6.2 变体二计算达到完全覆盖的时间原题是给定时间求覆盖数。反过来也可以问要覆盖一个指定区域如一个矩形内所有格子或者覆盖至少N个格子需要多少时间这类问题通常需要二分答案。我们猜测一个时间T然后用上面的BFS算法判断在时间T内是否能覆盖目标区域或至少N个格子。如果能说明答案可能更小如果不能说明答案需要更大。通过二分查找找到满足条件的最小时间T。二分的时间复杂度是O(log R)其中R是时间范围每次检查是O(k * T^2)。6.3 从BFS到数学公式的推导对于最简单的单源点、无限制扩散覆盖的格子数是一个数学序列1, 5, 13, 25, 41... 其通项公式为2*n*(n1)1其中n是时间t。这实际上是一个菱形或称旋转45度的正方形内的整点数。对于多源点问题转化为求多个菱形的并集的面积整点数。这是一个计算几何问题可以通过扫描线算法或多边形布尔运算来求解但实现复杂度远高于BFS。在竞赛中除非t非常大导致BFS不可行否则BFS是更优的选择。6.4 在实际场景中的应用联想这个“扩散”模型抽象自许多现实场景网络传播谣言、信息在社交网络中的传播简化版。火灾蔓延模拟火势在均匀草地上的蔓延。细胞自动机某些规则下的状态演化如康威生命游戏。图像处理形态学膨胀操作一个二值图像中的白色区域向四周膨胀。理解了这个模型的算法核心多源BFS求最短距离你就掌握了解决这一类“均匀扩散覆盖”问题的钥匙。在遇到具体问题时关键在于准确地将现实规则映射为图的边和节点的定义然后选择合适的图搜索算法BFS, Dijkstra, 二分BFS等。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻