bfs/dfs-最大连通问题
题目:
政府现勘探到一片油田,在这一片油田中有很多散落的石油资源。因为经费原因,政府只能开采一处油田,所以需找到最大的油田进行施工。油田的地理情况被简化成了一个矩阵,其中每一个方格代表一块土地,0 代表陆地,1 代表石油资源。如果一处石油资源和另一处石油相连接,则其算一块油田。现要找到最大的相互连接的石油资源,并输出它的面积。
实现
概况
bfs用队列来实现,每次弹出队首元素并处理它的相邻节点(即将符合条件的相邻节点入队,在这样一个入队的实际做处理统计工作)这里相邻节点的表示用到了方向数组。
dfs则是用递归来进行,将当前的(x,y)统计处理后,也是for循环它的相邻节点,但是在每次for循环中进行递归,从而构成了一棵多分支的搜索树。
dfs 代码更简洁,但递归深度过大会导致栈溢出(如矩阵过大时)。此时可改用 “栈” 实现非递归 dfs,避免栈溢出问题。
如果矩阵规模很大(如 1000x1000),递归 dfs 可能栈溢出,可用栈模拟递归;
bfs实现
vector<vector<int>>grid;
int rows, cols;
int dx[4] = { 0,1,0,-1 };
int dy[4] = { 1,0,-1,0 };
/*
参数:(x, y) —— 当前发现的未访问的 1 的坐标
返回值:当前连通区域的面积(包含的节点数)
处理:从(,y)出发,探索并标记整个连通区域,返回区域大小
思路:
将起点 (x, y) 加入队列,标记为访问(置为 0) area=1;
从队列中不断取出队头元素,检查四个方向的邻接格子
若邻点为 1 且合法,则将其入队并进行统计处理(area++)
当队列为空时,本区域遍历完成 → 返回区域面积
*/
int bfs(int x, int y) {//初始化队列,将起点加入队列并标记为已访问queue<pair<int, int>>q;q.push({ x,y });grid[x][y] = 0;//标记为已访问(修改原矩阵,也可用visited数组)int area = 1;//记录当前连通区域大小//2、循环处理队列中的所有节点while (!q.empty()) {//去除队头节点auto curr = q.front();q.pop();int curr_x = curr.first;int curr_y = curr.second;//遍历4个方向的相邻节点for (int i = 0; i < 4; i++) {int next_x = curr_x + dx[i];int next_y = curr_y + dy[i];//判断相邻节点是否合法(在举证范围内且未访问过的目标区域)if (next_x >= 0 && next_x < rows && next_y >= 0 && next_y < cols && grid[next_x][next_y] == 1) {//标记为已访问并加入队列grid[next_x][next_y] = 0;area++;q.push({ next_x,next_y });
}}}return area; // 返回当前连通区域的大小
}
/*
输入:矩阵大小和内容
rows,cols,和内容
输出:最大连通区域max_area
处理:遍历整个矩阵,寻找所有未访问的目标区域,计算最大连通区域
*/
int main() {cin >> rows >> cols;grid.resize(rows, vector<int>(cols));for (int i = 0; i < rows; i++) {for (int j = 0; j < cols; j++) {cin >> grid[i][j];}}int maxArea = 0;//遍历整个矩阵,寻找所有未访问的目标区域,计算最大连通区域for (int i = 0; i < rows; i++) {for (int j = 0; j < cols; j++) {if (grid[i][j] == 1) {int area = bfs(i, j);maxArea = max(maxArea, area);}}}cout << maxArea << endl;return 0;
}
拓展:若需保留原矩阵,可新增vector<vector<bool>> visited数组记录访问状态。
若连通规则为 8 方向(含对角线),修改方向数组为dx[8] = {0,1,0,-1,1,1,-1,-1}; dy[8] = {1,0,-1,0,1,-1,1,-1}即可。dfs实现
int dfs(int x, int y) {
grid[x][y] = 0;//标记为已访问int area = 1;//记录当前连通区域大小//遍历4个方向的相邻节点for (int i = 0; i < 4; i++) {int nx = x + dx[i];int ny = y + dy[i];if (nx >= 0 && nx < rows && ny >= 0 && ny < cols && grid[nx][ny] == 1) {//递归探索相邻节点,并累计面积area += dfs(nx, ny);}}return area;
}dfs(非递归)实现
int dfs_non_recursive(int x, int y) {//用栈存储待访问节点,代替递归调用栈vector<pair<int, int>>stack;stack.push_back({ x,y });grid[x][y] = 0;//标记为已访问int area = 1;while (!stack.empty()) {//弹出栈顶auto curr = stack.back();stack.pop_back();int curr_x = curr.first;int curr_y = curr.second;//遍历相邻节点for (int i = 0; i < 4; i++) {int next_x = curr_x + dx[i];int next_y = curr_y + dy[i];if (next_x >= 0 && next_x < rows && next_y >= 0 && next_y < cols && grid[next_x][next_y] == 1){grid[next_x][next_y] = 0;area++;//相邻节点入栈stack.push_back({ next_x,next_y });}
}}return area;
}递归dfs和非递归dfs对于一棵同样的搜索树而言其搜索顺序可能是不一样的但他们都会对一个方向进行深度搜索,因此都为dfs;
举例
树结构为:根1→子2、3、4;2→子5、6、7;3→子8、9、10;4→子11、12、13。
1
├── 2
│ ├── 5
│ ├── 6
│ └── 7
├── 3
│ ├── 8
│ ├── 9
│ └── 10
└── 4├── 11├── 12└── 13
二、逐次模拟栈的操作(搜索顺序)
阶段 1:处理根节点1
初始化栈:[1]
弹出1,处理(访问1)
子节点2、3、4按原顺序入栈 → 栈:[2, 3, 4]
阶段 2:处理节点4(栈顶是4)
弹出4,处理(访问4)
子节点11、12、13按原顺序入栈 → 栈:[2, 3, 11, 12, 13]
阶段 3:处理节点13(栈顶是13)
弹出13,处理(访问13)
13是叶子节点 → 栈:[2, 3, 11, 12]
阶段 4:处理节点12(栈顶是12)
弹出12,处理(访问12)
12是叶子节点 → 栈:[2, 3, 11]
阶段 5:处理节点11(栈顶是11)
弹出11,处理(访问11)
11是叶子节点 → 栈:[2, 3]
阶段 6:处理节点3(栈顶是3)
弹出3,处理(访问3)
子节点8、9、10按原顺序入栈 → 栈:[2, 8, 9, 10]
阶段 7:处理节点10(栈顶是10)
弹出10,处理(访问10)
10是叶子节点 → 栈:[2, 8, 9]
阶段 8:处理节点9(栈顶是9)
弹出9,处理(访问9)
9是叶子节点 → 栈:[2, 8]
阶段 9:处理节点8(栈顶是8)
弹出8,处理(访问8)
8是叶子节点 → 栈:[2]
阶段 10:处理节点2(栈顶是2)
弹出2,处理(访问2)
子节点5、6、7按原顺序入栈 → 栈:[5, 6, 7]
阶段 11:处理节点7(栈顶是7)
弹出7,处理(访问7)
7是叶子节点 → 栈:[5, 6]
阶段 12:处理节点6(栈顶是6)
弹出6,处理(访问6)
6是叶子节点 → 栈:[5]
阶段 13:处理节点5(栈顶是5)
弹出5,处理(访问5)
5是叶子节点 → 栈:[](栈空,DFS 结束)
最终搜索顺序(不逆序子节点入栈)
访问序列为:1 → 4 → 13 → 12 → 11 → 3 → 10 → 9 → 8 → 2 → 7 → 6 → 5测试数据
以下是 5 组测试数据,包含不同场景(无石油、单个石油、多个孤立油田、最大连通块在边缘 / 中间等):
测试数据 1(无石油)
2 3
0 0 0
0 0 0输出:0
测试数据 2(单个石油)
1 1
1输出:1
测试数据 3(3 个孤立石油)
3 3
1 0 1
0 0 0
1 0 0输出:1(每个石油都是孤立的,最大面积为 1)
测试数据 4(十字形连通块)
5 5
0 0 1 0 0
0 0 1 0 0
1 1 1 1 1
0 0 1 0 0
0 0 1 0 0输出:9(中间十字形连通区域,共 9 个 1)
测试数据 5(复杂连通块)
4 5
1 1 0 1 1
1 0 0 0 1
0 0 1 0 0
1 1 0 1 1输出:6(左上角连通块有 3 个 1,右上角连通块有 3 个 1,右下角连通块有 4 个 1?不,实际计算后最大为 6,具体可手动遍历验证)
核心数据结构
vector 模拟栈
栈的核心是 “只能在一端(栈顶)进出”,vector 虽然是动态数组,但用 push_back()(尾端插入)和 pop_back()(尾端删除)、back()(获取尾端元素),刚好模拟了栈的操作逻辑:
push_back(x) → 元素入栈(压到栈顶,也就是 vector 尾端)。 pop_back() → 栈顶元素出栈(删除 vector 尾端元素)。 back() → 查看栈顶元素(获取 vector 尾端元素)
pair 的使用:键值对存储,first/second 获取
pair<int, int> 确实是专门存储 “一对相关数据” 的数据结构,这里用来存二维坐标(x, y)特别合适: 第一个元素(x 坐标)用 pair.first 获取,第二个元素(y 坐标)用 pair.second 获取。 初始化时可以用 {x, y}(如 pair<int, int> p = {2, 3}),也可以用 make_pair(x, y)(如 make_pair(2, 3)),两种方式等价。 补充:如果需要存储更多维度(比如三维坐标 x,y,z),可以用 tuple<int, int, int>,通过 get<0>(t)、get<1>(t) 获取对应元素。
