bfs搜索加标记连通区域id实现时间优化(空间换时间)
有一个仅由数字 0 与 1 组成的 n×n 格迷宫。若你位于一格 0 上,那么你可以移动到相邻 4 格中的某一格 1 上,同样若你位于一格 1 上,那么你可以移动到相邻 4 格中的某一格 0 上。
你的任务是:对于给定的迷宫,询问从某一格开始能移动到多少个格子(包含自身)。
输入格式
第一行为两个正整数 n,m。
下面 n 行,每行 n 个字符,字符只可能是 0 或者 1,字符之间没有空格。
接下来 m 行,每行两个用空格分隔的正整数 i,j,对应了迷宫中第 i 行第 j 列的一个格子,询问从这一格开始能移动到多少格。
输出格式
m 行,对于每个询问输出相应答案。
输入输出样例
输入 #1复制
2 2 01 10 1 1 2 2
输出 #1复制
4 4
说明/提示
对于样例,所有格子互相可达。
- 对于 20% 的数据,n≤10;
- 对于 40% 的数据,n≤50;
- 对于 50% 的数据,m≤5;
- 对于 60% 的数据,n,m≤100;
- 对于 100% 的数据,1≤n≤1000,1≤m≤100000。
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
// 定义移动方向:上、下、左、右
const int directions[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
// BFS函数:从(i,j)开始探索连通区域,并返回区域大小
int bfs(int i, int j, vector<string>& grid, vector<vector<int>>& component, int componentId) {
queue<pair<int, int>> q;
q.push({i, j});
component[i][j] = componentId;
int size = 0;
while (!q.empty()) {
auto current = q.front();
q.pop();
int x = current.first;
int y = current.second;
size++;
for (int d = 0; d < 4; ++d) {
int nx = x + directions[d][0];
int ny = y + directions[d][1];
if (nx >= 0 && nx < grid.size() && ny >= 0 && ny < grid.size()) {
if (grid[x][y] != grid[nx][ny] && component[nx][ny] == 0) {
component[nx][ny] = componentId;
q.push({nx, ny});
}
}
}
}
return size;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<string> grid(n);
for (int i = 0; i < n; ++i) {
cin >> grid[i];
}
vector<vector<int>> component(n, vector<int>(n, 0));
vector<int> componentSize;
int componentId = 1;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (component[i][j] == 0) {
int size = bfs(i, j, grid, component, componentId);
componentSize.push_back(size);
componentId++;
}
}
}
while (m--) {
int i, j;
cin >> i >> j;
i--; j--;
cout << componentSize[component[i][j] - 1] << '\n';
}
return 0;
}
我用最直观的方式解释为什么 连通区域ID + 预处理 能大幅提升查询效率,而 每次查询单独BFS 会很慢。就像查字典和现场数数的区别!
场景类比
假设你管理一个学校,学生分成若干朋友圈(连通区域)。现在有 m
个问题,每个问题问:“某学生的朋友圈有多少人?”
方法1:每次现场数(单独BFS)
-
操作:每次有人提问,你就去教室找到这个学生,现场数他朋友圈的人数。
-
问题:如果同一个朋友圈被问多次(比如100次),你会重复数100次,效率极低。
方法2:提前统计 + 查表(连通区域ID)
-
操作:
-
预处理:提前把所有朋友圈找出来,给每个朋友圈编号(ID),并统计好人数。
-
查表:遇到提问时,直接根据编号查表回答。
-
-
优势:无论问多少次,每个朋友圈只需统计一次,后续全是
O(1)
查询。
在迷宫问题中的具体体现
1. 单独BFS的问题
-
每次查询:都要从起点重新BFS,可能重复遍历相同区域。
例子:
如果查询(1,1)
和(2,2)
属于同一区域,你会两次BFS遍历整个区域,浪费时间。
2. 预处理 + 连通区域ID的优势
-
预处理阶段:
用一次全图遍历(O(n²)),标记所有格子的区域ID,并记录每个区域的大小。-
component[i][j]
:格子(i,j)
的区域ID。 -
componentSize[id]
:区域id
的大小。
-
-
查询阶段:
直接通过component[i][j]
找到区域ID,再用componentSize
查大小,只需O(1)
时间。
关键点:
-
空间换时间:预处理阶段多用了
O(n²)
的空间存储component
数组,但将查询时间从O(n²)
降到O(1)
。 -
避免重复计算:同一区域的查询只需计算一次。
复杂度对比
方法 | 预处理时间 | 单次查询时间 | m次查询总时间 |
---|---|---|---|
每次单独BFS | 无 | O(n²) | O(m·n²) |
预处理 + 连通区域ID | O(n²) | O(1) | O(n² + m) |
当 m 很大时(比如 m=1e5):
-
单独BFS:1e5 × 1e6 = 1e11 次操作(不可接受)。
-
预处理:1e6 + 1e5 ≈ 1e6 次操作(快1e5倍)。
例子说明
迷宫示例
复制
下载
0 1 0 1 0 1 0 1 0
-
连通区域:所有格子互通,区域ID=1,大小=5。
-
查询 (1,1) 和 (2,3):
-
单独BFS:两次完整BFS,遍历5个格子各两次 → 共10次操作。
-
预处理:预处理遍历5格子,查询直接读表 → 共5+2=7次操作。
-
更极端例子
-
迷宫全为
0
(只有一个区域,大小n²)。 -
查询m次同一区域:
-
单独BFS:m × n² 次操作。
-
预处理:n² + m 次操作。
-
为什么 component[i][j] - 1
?
-
componentSize
是数组,下标从0
开始。 -
区域ID从
1
开始(因为0
表示未访问)。 -
所以区域
id
的大小存储在componentSize[id-1]
。
该用哪种方法?
-
预处理 + ID:适合 查询次数多(m很大) 或 同一区域被多次查询。
-
单独BFS:适合 查询次数极少(m很小) 或 迷宫极小。
最终结论
连通区域ID的本质是 提前划分好所有“团伙”并统计信息,后续查询直接“查表”,避免重复劳动。就像提前统计好班级人数,以后随问随答,不用每次都去教室数人头!