当前位置: 首页 > news >正文

算法-深度优先搜索DFS

深度优先搜索(DFS)是一种用于遍历或搜索树、图等数据结构的算法。它的核心思想是尽可能深地探索一个分支,直到无法继续前进,再回溯到上一个分叉点尝试其他路径。以下通过细胞数量问题为你详细讲解DFS的核心逻辑和应用场景:


一、DFS的基本原理

DFS的工作方式类似于走迷宫:

  1. 选择一个起点开始探索。

  2. 沿着一条路径走到尽头,并标记走过的位置。

  3. 遇到死路后回退到上一个分叉点,尝试其他未走过的路径。

  4. 重复上述过程,直到所有可能的路径都被探索。

伪代码表示
void dfs(当前位置) {
    if (当前位置不合法) return;
    if (当前位置已访问过) return;

    标记当前位置为已访问;
    for (所有相邻位置) {
        dfs(相邻位置); // 递归探索所有方向
    }
}

常见DFS题型

DFS  数学问题

数学问题可以用  “位数“  作为突破口。dfs移位,for循环遍历每一位可以填什么值,也就是“几种选择” 选?不选?选哪几种?枚举可选情况。该类问题常需要回溯

典例1:全排列

学习DFS全排列是一个非常经典的例子。

对于n=3的全排列

从1开始遍历不断dfs深搜,一定要从头到尾看代码把所有结果捋一遍,dfs函数返回到哪里,返回之后执行什么逻辑?

package DFS;

import java.util.Scanner;

//全排列   那很经典了
public class Main04 {
	static boolean[] visited=new boolean[20];
	public static void dfs(int x,int []arr,int N) {
		if(x>N) {
			//遍历到N+1层了,说明前N位的数都找到了,arr数组已存满
			//打印一下
			for(int i=0;i<N;i++) {
				System.out.print(arr[i]);
			}
			System.out.println();
			return;
		}
		
		for(int i=1;i<=N;i++) {//这遍历到了第i个数字
			if(!visited[i]) {//第i个数还没用过,那这层就用一下吧。
							//另外要是用过的话,就直接跳到下一个循环看看i+1呗
				arr[x-1]=i;
				visited[i]=true;
				dfs(x+1, arr,N);
				//恢复状态
				arr[x-1]=0;
				visited[i]=false;//完事了,跳到下一次循环看看i+1什么情况。
								//要是已经i=N了那就可以返回调用处,到上一层了
			}
		}
		
		
	}
	public static void main(String[] args) {
		Scanner scanner=new Scanner(System.in);
		int N=scanner.nextInt();
		int[] arr=new int[N];
		dfs(1,arr,N);
	}
}
典例2:组合
package DFS;

import java.util.Scanner;

//组合  5个数字选3个   
//那就是三层 枚举每一层可以放那些数字
public class Main05 {
	static boolean[] visited=new boolean[100];
	public static void dfs(int x,int N,int[] arr,int start,int R) {//x是当前枚举到了第x层
		
		if(x>R) {
			for(int i=1;i<=R;i++) {
				System.out.print(arr[i]);	
			}
			System.out.println();
			return;
		}
		
		//要保证前一位的数字大于后一位的,则后一位要从上一位的数字+1开始遍历
			for(int i=start;i<=N;i++) {
				visited[i]=true;
				arr[x]=i;
				
				dfs(x+1, N, arr, i+1, R);
				visited[i]=false;
				arr[x]=0;
			
		}
	
	}
	public static void main(String[] args) {
		Scanner scanner=new Scanner(System.in);
		int N=scanner.nextInt();
		int R=scanner.nextInt();
		int []arr=new int[20];
		
		dfs(1,N,arr,1,R);//起始层数是1
		
		
			
	}

}

要保证前一位的数字大于后一位的,则后一位要从上一位的数字+1开始遍历

要知道每调用一次dfs,位数就要后移一位。X表示当前枚举到了哪一位

后移的一位要经历for循环中每一个数的遍历

DFS应用题 

DFS递归调用,进入DFS方法首先要把当前坐标设置为已经访问过,相当于f(n)=f(n-1)  ,注意n-1,这是跳到下一层递归的条件,还要注意定义flag,if语句限定已经访问过的数据(点,坐标)不再访问

做题思路

  1. 前提条件:地图(图),树
  2. 准备工作:定义全局变量(static)visited[][]数组,标记节点访问情况
  3. for循环遍历地图的每一个节点,找到符合题意的节点进行dfs
  4. dfs偏移量递归,一般偏移量不止一种情况,需要用到for循环
  5. 回溯(非必须)
  6. 注意事项:某点进入dfs后要更新标记情况true。考虑数组越界情况

例一:小朋友崇拜圈

package DFS;

import java.util.Arrays;
import java.util.Scanner;

//遍历小朋友,dfs崇拜对象,dfs回到最开始的小朋友则为一圈
public class Main02 {

	static int maxCount = 0;

//i第i-1个小朋友,j崇拜对象,arr小朋友崇拜数组
	public static void dfs(int current, int Q, int[] arr, int count, boolean[] visited) {
		if (current == Q && count > 0) {// 形成环
			maxCount = Math.max(maxCount, count);
			return;
		}
		if (visited[current - 1] == true) {// 访问到已经访问过的节点
			return;
		}
		//判断完current基本情况后,标记访问数组
		visited[current - 1] = true;

		dfs(arr[current - 1], Q, arr, count + 1, visited);

	}

	public static void main(String[] args) {
		Scanner scanner = new Scanner(System.in);
		int N = scanner.nextInt();
		int[] arr = new int[N];
		boolean[] visited = new boolean[N];
		for (int i = 0; i < N; i++) {
			arr[i] = scanner.nextInt();
		}

		// 小朋友初始化好了,arr[i]是第i+1个小朋友崇拜的对象
		for (int Q = 1; Q <= N; Q++) {
			dfs(Q, Q, arr, 0, visited);
			Arrays.fill(visited, false);
		}

		System.out.println(maxCount);

	}

}

为什么使用DFS?因为需要遍历每一个小朋友,并且每个小朋友都有相邻节点图结构

根据做题方法,

  1. 前提条件:小朋友崇拜圈(图结构),考虑使用DFS遍历所有小朋友节点
  2. 准备工作:visited数组,标记节点情况
  3. 开始for循环遍历每一个小朋友,每一个小朋友都符合题意(即有相邻节点),因此每个小朋友都要用dfs搜索一遍
  4. dfs偏移量递归,本题偏移量就是下一个小朋友
  5. 另外需要考虑是否要回溯?索引越界情况?visited数组是否在正确标记了true?

例二:全球变暖

经典的DFS问题  -->岛屿问题

根据题意找出一个#,他的上下左右都是#,这样形状的至少五个#形成的岛屿不会被侵蚀。题目要求找到含有#但不含有上述五个#形成的岛屿,即会被侵蚀的岛屿。

为什么用DFS?因为要遍历整个地图(图),并且符合题意的节点都有要搜索的相邻节点,搜索相邻节点就需要用DFS

根据做题方法

1.前提条件:海域地图(图结构),考虑使用DFS遍历所有地图上的节点

2.准备工作:visited数组,标记节点情况

3.开始for循环遍历每一个地图节点,只有#是符合DFS的节点(即有符合题意的相邻节点),因此每个#都要用dfs搜索一遍

4.dfs偏移量递归,本题偏移量就是#的四个方向上的节点

5.另外需要考虑是否要回溯?索引越界情况?visited数组是否正确标记了true?

 

三、回溯适用条件判断(难点)

在深度优先搜索(DFS)中,是否使用回溯取决于问题的状态管理需求。以下是关键区别及示例说明:


需要回溯的情况

不同递归路径共享同一状态,且需要尝试所有可能性时,必须回溯以恢复状态。

特点
  1. 状态共享:多个路径修改同一数据结构(如列表、数组)。

  2. 路径探索:需撤销当前选择以尝试其他可能(如排列、组合问题)。

示例
  1. 全排列
    生成数组的所有排列时,每次选择一个元素后,需记录已选元素。递归返回后需撤销选择,以便尝试其他排列.....代码如上

  2. 路径总和II
    记录所有满足条件的路径时,需在递归后移除当前节点,防止污染其他路径:

    void dfs(TreeNode node, int target, List<Integer> path, List<List<Integer>> res) {
        if (node == null) return;
        path.add(node.val);
        if (node.left == null && node.right == null && target == node.val) {
            res.add(new ArrayList<>(path));
        }
        dfs(node.left, target - node.val, path, res);
        dfs(node.right, target - node.val, path, res);
        path.remove(path.size() - 1); // 回溯:移除当前节点
    }

无需回溯的情况

当状态独立维护或修改后无需恢复时,不需要回溯

特点
  1. 状态独立:每次递归传递值的副本(如基本类型、不可变对象)。

  2. 永久修改:状态修改后无需撤销(如标记已访问的岛屿)。

示例
  1. 二叉树的前序遍历
    每个节点仅访问一次,路径独立,无需恢复状态:

    void dfs(TreeNode node, List<Integer> res) {
        if (node == null) return;
        res.add(node.val); // 无需回溯
        dfs(node.left, res);
        dfs(node.right, res);
    }
  2. 岛屿数量
    遍历后将陆地标记为水(永久修改),无需恢复:

    void dfs(char[][] grid, int i, int j) {
        if (i < 0 || j < 0 || i >= grid.length || j >= grid[0].length || grid[i][j] == '0') return;
        grid[i][j] = '0'; // 直接修改,无需回溯
        dfs(grid, i + 1, j);
        dfs(grid, i - 1, j);
        dfs(grid, i, j + 1);
        dfs(grid, i, j - 1);
    }


回溯总结

场景是否需要回溯原因示例
共享状态且需尝试所有路径不同路径共享同一数据结构,需恢复状态全排列、组合问题
独立状态或永久修改状态通过值传递或修改后无需撤销二叉树遍历、岛屿数量

核心判断:是否需要撤销当前选择以允许其他路径的正确执行。若多个分支共享可变状态,则必须回溯;反之,若状态独立或修改不可逆,则无需回溯。

以例题形式理解回溯

以P1451 求细胞数量 - 洛谷为例,目标是统计矩阵中连通的非零区域(细胞)的数量。DFS在此处的核心作用是标记并探索整个连通区域

关键步骤
  1. 遍历矩阵:逐个检查每个单元格。

  2. 触发DFS的条件:当遇到未被访问过的非零单元格时,启动DFS。

  3. 标记连通区域:在DFS中标记所有与该单元格连通的非零单元格为已访问。

  4. 计数细胞:每次触发DFS时,计数器加1(表示发现一个新细胞)。

package DFS;

import java.util.Scanner;

public class Main08 {
	static int n;
	static int m;
	static boolean[][] visited=new boolean [100][100];
	static int[][] map;
		static int count=0;
		static int[] dy= {0,0,1,-1};
		static int[] dx= {1,-1,0,0};
		public static void dfs(int y,int x) {
			
			if(y<0||y>n-1||x<0||x>m-1) {//索引越界
				return ;
			}
			
			if(map[y][x]==0) {//这个方向走到0了,不是细胞
				return;
			}
			
			if(visited[y][x]) {//访问过了
				return;
			}
			
			visited[y][x]=true;
			
			int nextY;
			int nextX;
			
			for(int i=0;i<4;i++) {
				nextY=y+dy[i];
				nextX=x+dx[i];
				dfs(nextY,nextX);
				
			}
			
			
			
		}
	public static void main(String[] args) {
		Scanner sc=new Scanner(System.in);
		 n=sc.nextInt();
		 m=sc.nextInt();
		 map=new int[n][m];
		 // 读取地图
	        for (int i = 0; i < n; i++) {
	            String line = sc.next();
	            for (int j = 0; j < m; j++) {
	                map[i][j] = line.charAt(j) - '0';
	            }
	        }
		for(int i=0;i<n;i++) {
			for(int j=0;j<m;j++){
				if(map[i][j]!=0&&!visited[i][j])
					count++;
				dfs(i,j);
			}
		}
		
		System.out.println(count);
	}

}

为什么不需要回溯?

在细胞数量问题(岛屿类问题)中,DFS不需要回溯,因为:

  1. 永久标记:一旦标记某个单元格为已访问(visited[i][j] = true),后续遍历会直接跳过它,避免重复计数。

  2. 目标单一:只需统计连通区域的数量,而非列举所有可能的路径。DFS的职责是“淹没”整个细胞区域,无需恢复状态。

对比需要回溯的场景
场景是否需要回溯原因
细胞数量问题标记是永久的,只需统计数量
全排列问题需要撤销选择以尝试其他排列组合
迷宫路径搜索需要回退以探索不同路径的可能解

四、DFS的两种典型应用

1. 不回溯的场景(标记型DFS)
  • 问题特征:只需遍历或标记所有可达节点。

  • 常见问题

    • 统计连通区域(细胞、岛屿数量)

    • 二叉树的前序遍历

    • 图的连通性判断

代码特点

  • 使用visited数组记录已访问节点。

  • 递归后不需要撤销操作

2. 需要回溯的场景(路径探索型DFS)
  • 问题特征:需要尝试所有可能的路径或组合。

  • 常见问题

    • 全排列、组合总和

    • 棋盘问题(如八皇后)

    • 子集生成

代码特点

  • 使用path等临时结构记录当前路径。

  • 递归后必须撤销选择(如path.removeLast()),以恢复状态。


五、DFS的注意事项

  1. 递归深度:DFS可能引发栈溢出(如处理大规模数据时),可改用迭代式DFS(显式栈)。

  2. 剪枝优化:在需要回溯的问题中,通过条件判断提前终止无效路径的探索。

  3. 状态管理

    • 标记型问题:状态通常通过全局的visited数组管理。

    • 路径型问题:状态通过参数传递(如path列表)。


六、图解DFS过程(以细胞问题为例)

假设输入矩阵为:

0 2 3  
1 0 4  
0 0 0  
  1. 发现起点:遍历到(0,1)(值为2),计数器count++

  2. 标记连通区域:DFS会标记(0,1)→(0,2)→(1,2),形成一个连通区域。

  3. 后续遍历:跳过已标记的单元格,最终统计出细胞数量为2。


小结

  • DFS的本质:通过递归或栈实现的“一条路走到黑”的探索策略。

  • 是否回溯:取决于问题是否需要尝试所有可能性或恢复状态。

  • 细胞问题的特殊性:通过永久标记避免重复访问,无需回溯。

理解DFS的关键在于明确问题的目标状态管理的方式。通过对比不同场景的应用,可以更灵活地设计DFS的逻辑。

 

总结 

虽然深度优先搜索作为基础算法,但作为初学者来看,一点也不容易。尤其是在回溯判断上难以理解,作为初学者,一定要在理解全排列(n=3)图解之后,看着代码,一步一步把dfs的全部过程分析一遍,手写一遍,看看执行流程是怎么样的,如何回溯?回溯返回到哪里?

相关文章:

  • Redis 在windows下的下载安装与配置
  • 质检LIMS系统在诊所的应用 诊所质检行业的最优LIMS系统
  • knowledge-微前端(多个前端应用聚合的一个应用架构体系,每个小的应用可独立运行,独立开发,独立部署上线)
  • Linux与HTTP中的Cookie和Session
  • javaFX的使用
  • 如何在linux中利用方向键快速查找之前的敲过的命令
  • 第六:go 操作 redis-go
  • MediaPipe 手势识别全解析:如何在移动端实现实时手部跟踪
  • 网络空间安全(39)入侵排查
  • 【C++模板】
  • ambiq apollo3 Flash实例程序注释
  • 通过Typora + PicGo + 阿里云对象存储(OSS)实现图床
  • Numpy
  • 【vulhub/wordpress靶场】------获取webshell
  • 为什么 API 接口漏洞越来越多?与现代网站开发环境的关系
  • [Deepseek 学c++]初始化捕获与按值(显式与隐式)捕获傻傻分不清
  • 2025年云南食品安全员管理员考试题库
  • 代码随想录_动态规划
  • webpack等构建工具如何支持移除未使用的代码
  • 力扣hot100——三数之和(双指针)
  • 新疆巴音郭楞州和硕县发生4.6级地震,震源深度10千米
  • 长沙至赣州高铁初步设计获批,可填补湘赣两省斜向交通空白
  • 一日双赛“莎头组合”赢得强势,但国乒已开始品尝输球滋味
  • 长三角议事厅·周报|新能源汽车产业需寻求“第二增长曲线”
  • 河北邯郸回应被曝涉生猪未检疫、注水问题:将严厉查处违法行为
  • 世卫大会中国代表团:中国深入参与全球卫生治理,为构建人类卫生健康共同体贡献中国力量