【深度优先搜索3】本课导读

一、回顾

二、知识精讲 => 连通块

三、例题一 数水坑(八联通 连通块)

题目描述

输入格式

输出格式

样例输入

样例输出

题目解析

一、先看懂题目

二、完整代码 + 逐模块解析

三、核心重点拆解(最关键!)

1. 八连通方向数组

2. DFS 的核心作用

3. 主函数逻辑(最容易理解)

四、运行流程(样例演示)

五、代码优化点(为什么这么写)

六、总结

三、例题二 填充颜色(反向DFS)

题目描述

输入格式

输出格式

样例输入

样例输出

题目解析

一、先看懂题目

二、核心思路(最重要!)

正向思路(难):找圈里的 0

反向思路(简单,代码用的这个):

三、完整代码 + 逐行解析

四、重点模块逐句拆解

1. 方向数组(四联通)

2. DFS 函数(核心)

3. 关键一步:从 (0,0) 开始搜索

4. 输出判断逻辑

五、运行流程(样例演示)

六、为什么这道题要用「反向 DFS」?

七、总结(背会这套模板)

四、例题三 细胞(正向 DFS Flood Fill)

题目描述

输入格式

输出格式

样例输入

样例输出

题目解析

一、题意理解

二、核心思路(最重要!)

三、完整代码 + 逐行解析

四、重点模块拆解

1. 方向数组(四联通)

2. DFS 核心函数

3. 字符串输入处理

4. 主函数逻辑

五、运行流程(样例演示)

六、易错点提醒

七、核心总结(必背模板)


Go~

一、回顾

void dfs(当前位置) {
    判断当前位置是否处于终点位置

    for (向可以移动的位置进行枚举判断) {
        int 移动到的新位置;
        if (满足可以移动的位置) {
            标记新位置已经访问过
            dfs(新位置);
            可能需要取消标记 // 回溯
        }
    }
}

二、知识精讲 => 连通块

  • 连通块定义 连通块是指在网格中,所有满足 “可直接或间接相连” 条件的元素组成的集合。比如消消乐中,同一种动物的相邻(上下左右)元素就构成一个连通块。

  • 核心算法逻辑(伪代码拆解)

    • 方向数组dx[]dy[] 是用来控制移动方向的数组,上下左右四个方向可以表示为:
      int dx[] = {-1, 1, 0, 0}; // 上、下、左、右
      int dy[] = {0, 0, -1, 1};
      
    • DFS 过程
      1. 标记当前位置为已访问(避免重复统计)。
      2. 遍历四个方向,检查新位置是否在网格内、未被访问且属于同一连通块。
      3. 对符合条件的新位置递归调用 DFS,继续探索。
  • 典型应用场景

    • 消消乐类游戏中,统计可消除的同色元素数量。
    • 图论中的岛屿数量问题、洪水填充问题。
    • 迷宫 / 地图中的区域划分问题。

三、例题一 数水坑(八联通 连通块)

题目描述

由于近期的降雨,雨水汇集在田地不同的地方。用一个 N×M(1≤N≤100,1≤M≤100) 的网格图表示,每个网格有水W或是旱地.。一个网格与其周围八个网格相连,一组相连的水网格视为一个水坑。求田地中一共有多少个水坑。

输入格式

第一行两个整数 N,M。接下来 N 行,每行 M 个无空格字符,仅包含W.

输出格式

输出水坑总数量。

样例输入

10 12
W........WW.
.WWW.....WWW
....WW...WW.
.........WW.
.........W..
..W......W..
.W.W.....WW.
W.W.W.....W.
.W.W......W.
..W.......W.

样例输出

3

题目解析

这是一道经典的八连通连通块统计问题,核心就是用DFS(深度优先搜索) 找到所有相连的W(水坑),统计总共有多少组独立的水坑。

一、先看懂题目

  1. 地图是 N×M 的网格,W= 水,.= 旱地
  2. 8 连通:一个水坑能和上下左右、左上、左下、右上、右下 8 个方向的水相连
  3. 目标:统计独立水坑的总数

二、完整代码 + 逐模块解析

#include <bits/stdc++.h>  // 万能头文件,包含所有C++常用库
using namespace std;

// 全局变量:地图大小n行m列,ans统计水坑数量,mp存储地图
int n, m, ans=0; 
char mp[101][101];  // 地图数组,最大100x100,开101方便从1开始计数

// 八联通方向数组:对应 上、下、左、右、左上、左下、右上、右下
int dx[]={0, -1, 1, 0, 0, -1, 1, -1, 1};
int dy[]={0, 0, 0, -1, 1, -1, -1, 1, 1}; 

// DFS函数:把(x,y)所在的整个水坑全部变成.(标记为已访问)
void dfs(int x, int y) {
	mp[x][y] = '.';  // 核心:访问过的水坑,直接改成旱地,避免重复统计
	
	// 遍历8个方向(i从1到8,对应8个联通方向)
	for(int i=1;i<=8;i++) {
		int nx = x + dx[i];  // 新位置的行号
		int ny = y + dy[i];  // 新位置的列号
		
		// 判断条件:
		// 1. 新坐标不越界  2. 新位置是水(W)
		if(nx>0 && ny>0 && nx<=n && ny<=m && mp[nx][ny] == 'W') 
			dfs(nx, ny);  // 递归继续搜索
	} 
	return;
}

int main() {
	// 1. 输入地图尺寸 N行 M列
	cin >> n >> m;
	
	// 2. 输入地图(从1,1开始存,不用处理0行0列,越界判断更简单)
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			cin >> mp[i][j];
	
	// 3. 遍历整个地图,寻找水坑
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			// 找到一个未被访问的水坑(W)
			if(mp[i][j] == 'W') {
				dfs(i, j);  // DFS把整个水坑标记为.
				ans++;      // 找到一个独立水坑,计数+1
			}
	
	// 4. 输出答案
	cout << ans << endl;
	return 0;
}

三、核心重点拆解(最关键!)

1. 八连通方向数组
int dx[]={0, -1, 1, 0, 0, -1, 1, -1, 1};
int dy[]={0, 0, 0, -1, 1, -1, -1, 1, 1}; 
  • dx:行的变化(上下)
  • dy:列的变化(左右)
  • i=1~8 刚好对应8 个方向,覆盖所有相邻格子
2. DFS 的核心作用

dfs(x,y) 做一件事:把坐标 (x,y) 所在的整个连通水坑 **,全部变成 .**

  • 相当于给这个水坑打标记:我已经统计过了
  • 后续遍历就不会重复计算这个水坑
3. 主函数逻辑(最容易理解)
  1. 读入地图
  2. 逐行逐列扫描
  3. 遇到 W → 说明找到新水坑
  4. 调用 DFS 把整个水坑消掉 → 答案 + 1
  5. 最后输出总答案

四、运行流程(样例演示)

以样例输入为例:

  1. 扫描到第一个 W
  2. DFS 把这一整块连通的 W 全变成 .
  3. 答案 +1
  4. 继续扫描,遇到下一个没被消掉的 W
  5. 重复操作,直到扫描完整个地图
  6. 最终统计出 3 个独立水坑,输出 3

五、代码优化点(为什么这么写)

  1. 数组从 1 开始存:不用判断 nx>=0,越界判断更简单
  2. 直接修改地图:不用额外开标记数组,节省空间、代码更简洁
  3. 8 方向循环:完美匹配题目要求

六、总结

这道题就是连通块模板题,记住这套逻辑:

  1. 定义方向数组
  2. 写 DFS:标记当前点 + 遍历所有方向 + 递归合法点
  3. 遍历地图,遇到未标记的起点就 DFS + 计数

三、例题二 填充颜色(反向DFS)

题目描述

由数字 0 组成的方阵中,存在由数字 1 构成的闭合圈,围墙仅上下左右四方向连通。要求将闭合圈内部所有 0 改成 2,圈外 0 保持不变,数字 1 保持不变。

输入格式

第一行一个整数 n(1≤n≤30)。接下来 n 行,每行 n 个整数,构成 n×n 的 01 方阵。

输出格式

输出修改完成后的完整方阵,数字间空格隔开。

样例输入

6
0 0 0 0 0 0
0 0 1 1 1 1
0 1 1 0 0 1
1 1 0 0 0 1
1 0 0 0 1 1
1 1 1 1 1 0

样例输出

0 0 0 0 0 0 
0 0 1 1 1 1 
0 1 1 2 2 1 
1 1 2 2 2 1 
1 2 2 2 1 1 
1 1 1 1 1 0 

题目解析

这道题是经典的反向 DFS Flood Fill(洪水填充) 题目,核心思路:从边界外开始填充,标记所有外部的 0,剩下没被标记的内部 0 就是要填 2 的区域

一、先看懂题目

  1. 方阵里有 0(空白)1(围墙)
  2. 1 围成闭合圈
  3. 要求:圈内部的 0 → 改成 2,圈外部的 0 保持不变,1 保持不变

二、核心思路(最重要!)

正向思路(难):找圈里的 0
反向思路(简单,代码用的这个):
  1. 从方阵最最外面的 (0,0) 开始 DFS
  2. 把所有能连通到外部的 0全部标记为「已访问」
  3. 最后遍历方阵:
    • 是 1 → 原样输出
    • 是 0 + 没被访问 → 圈内部 → 输出 2
    • 是 0 + 被访问 → 圈外部 → 输出 0

三、完整代码 + 逐行解析

#include <bits/stdc++.h>  // 万能头文件
using namespace std;

int n, mp[32][32];      // n是方阵大小,mp存地图
bool vis[32][32];      // 标记是否访问过(标记外部的0)

// 四联通:上下左右
int dx[] = {-1, 1, 0, 0};
int dy[] = {0, 0, -1, 1};

// DFS:从外部往内部填充,标记所有外部0
void dfs(int x, int y) {
    // 递归终止条件(4个满足任意一个就返回)
    // 1. 越界  2. 已经访问过  3. 遇到墙(1)
    if (x < 0 || x > n + 1 || y < 0 || y > n + 1 || vis[x][y] || mp[x][y] == 1) 
        return;
    
    vis[x][y] = true;  // 标记当前位置:这是外部的0
    
    // 遍历4个方向,继续填充
    for (int i = 0; i < 4; i++)
        dfs(x + dx[i], y + dy[i]);
}

int main() {
    cin >> n;
    
    // 输入n*n方阵(从1~n存,方便外围留空间)
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            cin >> mp[i][j];

    // 关键!从 (0,0) 开始DFS,标记所有外部0
    dfs(0, 0);
    
    // 输出最终方阵
    for (int i = 1; i <= n; i++){
        for (int j = 1; j <= n; j++){
            // 如果是0 且 没被访问 → 内部0 → 输出2
            if (mp[i][j] == 0 && !vis[i][j]) 
                cout << 2 << " ";
            // 否则原样输出(1 或 外部0)
            else 
                cout << mp[i][j] << " ";
        }
        cout << endl;  // 每行结束换行
    }
    return 0;
}

四、重点模块逐句拆解

1. 方向数组(四联通)
int dx[] = {-1, 1, 0, 0};
int dy[] = {0, 0, -1, 1};

对应:

  • 上:x-1, y
  • 下:x+1, y
  • 左:x, y-1
  • 右:x, y+1题目只允许上下左右走,所以用 4 个方向。
2. DFS 函数(核心)
void dfs(int x, int y) {
    if (x < 0 || x > n + 1 || y < 0 || y > n + 1 || vis[x][y] || mp[x][y] == 1) 
        return;
    vis[x][y] = true;
    for (int i = 0; i < 4; i++)
        dfs(x + dx[i], y + dy[i]);
}

作用:把所有外部连通的 0全部标记为已访问。

终止条件解释

  • x < 0 || x > n+1:超出整个地图范围,停止
  • vis[x][y]:已经访问过,停止
  • mp[x][y]==1:遇到围墙,无法穿过,停止
3. 关键一步:从 (0,0) 开始搜索
dfs(0, 0);
  • (0,0) 不在方阵内部,是最外围
  • 从这里 DFS,能把所有和外部连通的 0全部标记
  • 1 是墙,DFS 穿不过去 → 内部 0 不会被标记
4. 输出判断逻辑
if (mp[i][j] == 0 && !vis[i][j]) 
    cout << 2 << " ";
else 
    cout << mp[i][j] << " ";
  • 0 + 没被访问 → 圈内部 → 填 2
  • 0 + 被访问 → 圈外部 → 保持 0
  • 1 → 围墙 → 保持 1

五、运行流程(样例演示)

  1. 输入 6×6 地图
  2. 从 (0,0) 开始 DFS
  3. 标记所有外部 0
  4. 遍历地图:
    • 没被标记的 0 → 输出 2
    • 其他 → 原样输出
  5. 得到正确答案

六、为什么这道题要用「反向 DFS」?

  • 正向找闭合圈内部很难
  • 反向从外部填充,把外部 0 标记,剩下的自然就是内部
  • 代码简单、效率高、不易错
  • 这是算法竞赛最常用的套路

七、总结(背会这套模板)

  1. 反向 DFS:从边界外开始搜索
  2. 标记外部 0,内部 0 不标记
  3. 输出:未标记的 0 → 2
  4. 属于 Flood Fill 洪水填充 经典题型

四、例题三 细胞(正向 DFS Flood Fill)

题目描述

一矩形阵列由数字 0 到 9 组成,数字 1 到 9 代表细胞。细胞的定义为:沿细胞数字上下左右还是细胞数字,则为同一细胞。求给定矩形阵列的细胞个数。

输入格式

第一行为矩阵的行数 n 和列数 m(1≤n,m≤1000)。下面为一个 n×m 的矩形阵列。

输出格式

一个整数,表示细胞的个数。

样例输入

4 10
0234500067
1034560500
2045600671
0000000089

样例输出

4

题目解析

这道题是经典的正向 DFS Flood Fill(洪水填充) 题目,核心思路:遍历地图,每找到一个未被访问的细胞(1-9),就用 DFS 把整片连通的细胞全部标记消除,同时计数加一

一、题意理解

  • 方阵元素:0(无细胞)、1-9(细胞)
  • 规则:上下左右四联通,相连的细胞视为同一个
  • 要求:统计总共有多少个独立的细胞块

二、核心思路(最重要!)

  1. 遍历扫描:逐行逐列扫描整个地图
  2. 发现起点:如果遇到非 0 数字(细胞)
  3. DFS 消除:调用 DFS,把这一整片连通的细胞都变成 0
  4. 计数 + 1:每消除一整片,就代表找到一个细胞
  5. 最终输出:总计数就是答案

三、完整代码 + 逐行解析

#include <bits/stdc++.h>  // 万能头文件
using namespace std;

int n, m, ans=0;        // n行m列,ans统计细胞个数
int mp[1001][1001];     // 地图数组,数据范围1000x1000

// 四联通:上 下 左 右
int dx[]={0, -1, 1, 0, 0};
int dy[]={0, 0, 0, -1, 1}; 

// DFS函数:把(x,y)所在的整片细胞变成0(消除标记)
void dfs(int x, int y) {
	mp[x][y] = 0;  // 核心:把当前细胞变成0,标记为已访问
	
	// 遍历4个方向(i=1到4)
	for(int i=1;i<=4;i++) {
		int nx = x + dx[i];
		int ny = y + dy[i];
		
		// 判断:不越界 + 不是0(是细胞)
		if(nx>0 && ny>0 && nx<=n && ny<=m && mp[nx][ny] != 0) 
			dfs(nx, ny);  // 递归搜索
	} 
	return;
}

int main() {
	cin >> n >> m;
	
	// 输入地图:按字符串读取,转成数字存入数组
	for(int i=1;i<=n;i++) {
        string tmp; 
        cin >> tmp;
        for(int j=0;j<m;j++) {
            mp[i][j+1] = tmp[j] - '0';
        }
    }
	
	// 遍历地图找细胞
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			// 找到一个未被访问的细胞
			if(mp[i][j] != 0) {
				dfs(i, j);  // 消除整片细胞
				ans++;      // 计数+1
			}
	
	// 输出答案
	cout << ans << endl;
	return 0;
}

四、重点模块拆解

1. 方向数组(四联通)
int dx[]={0, -1, 1, 0, 0};
int dy[]={0, 0, 0, -1, 1}; 
  • 对应移动:上、下、左、右
  • 题目要求四联通,所以只遍历 i=1~4
2. DFS 核心函数

作用:消除一整片连通的细胞

  • 把当前位置变成 0,防止重复搜索
  • 向四个方向扩散,遇到非 0 数字就继续递归
3. 字符串输入处理
string tmp; cin >> tmp;
mp[i][j+1] = tmp[j] - '0';
  • 输入是连续数字串(如0234500067),不能直接读整数
  • string读取,再转成数字存入地图
  • tmp[j] - '0':字符转数字('0'→0,'1'→1...)
4. 主函数逻辑
  1. 读入地图
  2. 双重循环扫描
  3. 遇到非 0 数字 → DFS 消除整片 → 答案 + 1
  4. 输出总细胞数

五、运行流程(样例演示)

  1. 输入 4×10 地图
  2. 扫描到第一个非 0 数字2
  3. DFS 把整片连通细胞变 0 → 答案 = 1
  4. 继续扫描,遇到下一个非 0 数字1
  5. DFS 消除 → 答案 = 2
  6. 重复直到扫描结束
  7. 最终答案 = 4

六、易错点提醒

  1. 方向数量错误:题目是四联通,不能写成 8
  2. 输入处理:必须用字符串读取连续数字
  3. 数组大小:数据范围 1000,数组要开到1001

七、核心总结(必背模板)

  1. 解题思想:正向 DFS + 洪水填充
  2. 核心操作:找到非 0 起点 → DFS 消除整片 → 计数 + 1
  3. 联通方式:上下左右四联通
  4. 题型归类:连通块统计标准模板题
  5. 适用场景:数水坑、细胞、岛屿、连通块数量

更多推荐