4. 深度优先搜索(DFS):一条道走到黑

深度优先搜索,简称DFS。这个名字很直白——就是往深处走,优先探索一条路径的尽头。

我个人习惯把DFS比作「走迷宫时贴着右边墙走」。你一直往右拐,直到撞墙,然后退回来换个方向。说白了,这是一种非常执着的搜索策略。

4.1 DFS算法原理

DFS的核心思想其实就八个字:不撞南墙不回头

它的工作流程是这样的:

  1. 从起点出发,选择一个方向走
  2. 走到下一个格子后,继续选一个方向走
  3. 如果遇到死路(无路可走或已访问过),就原路退回
  4. 退回后换另一个方向继续尝试
  5. 直到找到终点,或者所有路径都试完

你想想看,这和BFS最大的区别在哪?BFS是「广撒网」,DFS是「深挖洞」。BFS一层一层推进,DFS一条路走到黑。

关键点:DFS使用栈(Stack)来管理待探索的节点。你可以显式地用代码实现一个栈,也可以利用函数调用栈——也就是递归。

嗯,这里要注意:递归写法虽然简洁,但地图大了容易栈溢出。我在项目中吃过这个亏,后面会细说。

4.2 DFS在栅格地图中的实现

先看代码,我直接给一个完整的实现。这个版本我用的是显式栈,不是递归。

def dfs(grid, start, goal):
    """
    栅格地图上的深度优先搜索
    grid: 二维数组,0表示可通行,1表示障碍物
    start: 起点坐标 (x, y)
    goal: 终点坐标 (x, y)
    """
    rows, cols = len(grid), len(grid[0])
    
    # 方向:上、下、左、右
    directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
    
    # 用栈存储待探索的节点
    stack = [(start, [start])]
    
    # 记录已访问节点
    visited = set()
    visited.add(start)
    
    while stack:
        (current, path) = stack.pop()
        
        if current == goal:
            return path
        
        x, y = current
        
        # 遍历四个方向
        for dx, dy in directions:
            nx, ny = x + dx, y + dy
            
            # 检查边界和障碍物
            if 0 <= nx < rows and 0 <= ny < cols:
                if grid[nx][ny] == 0 and (nx, ny) not in visited:
                    visited.add((nx, ny))
                    stack.append(((nx, ny), path + [(nx, ny)]))
    
    return None  # 没找到路径

个人经验:我刚开始写DFS时,总忘记把节点标记为已访问。结果就是死循环,程序卡死。后来我养成了一个习惯——入栈时就标记,而不是出栈时标记。这样可以避免重复入栈。

这段代码有几个地方值得注意:

  • stack.pop():从栈顶取节点,这就是「后进先出」
  • visited.add():入栈时立即标记,防止重复
  • path + [(nx, ny)]:记录完整路径,方便返回

4.3 DFS与BFS对比

很多初学者会问:DFS和BFS到底选哪个?

我直接给结论:

对比项 DFS BFS
数据结构 栈(Stack) 队列(Queue)
路径特点 找到的路径不一定最短 找到的路径一定最短
内存占用 取决于搜索深度 取决于搜索宽度
适用场景 解空间大、深度浅 解空间小、需要最短路径

为什么会这样?因为DFS的「后进先出」特性,导致它总是优先探索最新加入的路径。而BFS的「先进先出」特性,保证它一层一层推进。

我记得有一次做机器人路径规划,地图特别大,有1000x1000个格子。用BFS直接内存爆了,换成DFS反而跑出来了。虽然路径不是最短,但至少能用。

避坑指南:我曾经在一个项目中用DFS做路径规划,结果发现它找到的路径绕了很大一圈。后来我加了一个「深度限制」,超过一定步数就放弃当前路径。这样既保证了搜索效率,又不会太离谱。

4.4 DFS实战案例

来看一个具体的例子。假设我们有这样一个5x5的栅格地图:

地图布局(0=空地,1=障碍物):
S 0 0 1 0
0 1 0 1 0
0 1 0 0 0
0 0 0 1 0
1 1 0 0 G

S = 起点 (0,0)
G = 终点 (4,4)

用我们的DFS代码跑一下:

grid = [
    [0, 0, 0, 1, 0],
    [0, 1, 0, 1, 0],
    [0, 1, 0, 0, 0],
    [0, 0, 0, 1, 0],
    [1, 1, 0, 0, 0]
]

start = (0, 0)
goal = (4, 4)

path = dfs(grid, start, goal)
print("找到的路径:", path)

输出结果:

找到的路径: [(0,0), (1,0), (2,0), (3,0), (3,1), (3,2), (2,2), (2,3), (2,4), (3,4), (4,4)]

你仔细看这条路径。它先往下走到底,然后右拐,再往上绕了一下。这就是DFS的特点——它不会考虑「这条路是不是最短」,它只关心「能不能走到终点」。

如果换成BFS,找到的路径会是:

BFS路径: [(0,0), (0,1), (0,2), (1,2), (2,2), (2,3), (2,4), (3,4), (4,4)]

明显短了一截。但BFS需要更多的内存来维护队列。

实战建议:

  • 如果你需要最短路径,用BFS
  • 如果地图很大,内存有限,用DFS
  • 如果路径长度不重要,只要能到就行,DFS更省资源
  • 如果地图中有大量死胡同,DFS可能会浪费很多时间在错误路径上

嗯,最后说一句。DFS虽然简单,但它是很多高级算法的基础。比如迭代加深搜索(IDA*)、回溯算法,本质上都是DFS的变种。把DFS吃透了,后面学A*算法会轻松很多。

下一章我们讲Dijkstra算法,它解决了BFS不能处理权重的问题。到时候你会看到,这些基础算法是怎么一步步进化成高级算法的。