算法学习笔记:25.回溯算法之迷宫寻路——从原理到实战,涵盖 LeetCode 与考研 408 例题

迷宫寻路是回溯算法的经典应用场景,其核心是在复杂路径中通过 “尝试 - 回溯” 探索所有可能的路径,最终找到通往终点的解。无论是在算法竞赛(如 LeetCode)还是考研计算机专业基础综合(408)中,迷宫寻路问题及其变种都是考察回溯思想的高频考点。


迷宫寻路的回溯算法思路

问题定义

迷宫通常被抽象为一个二维网格,其中:

  • 0 表示可通行的空地。
  • 1 表示不可通行的墙壁。
  • 起点 (startX, startY) 和终点 (endX, endY) 是网格中的两个特殊位置。

问题目标:从起点出发,通过上下左右四个方向移动,找到一条到达终点的路径(或所有路径),移动过程中不能穿过墙壁,也不能重复经过同一位置。

回溯算法核心思路

回溯算法通过递归探索所有可能的路径,若当前路径无法到达终点,则回溯到上一步,尝试其他方向。具体步骤如下:

  1. 标记当前位置:到达某位置 (x, y) 后,标记该位置为 “已访问”(避免重复进入)。
  2. 判断终点:若 (x, y) 是终点,则记录当前路径(若需所有路径)或直接返回成功。
  3. 尝试方向:依次尝试上、下、左、右四个方向:
    • 若方向未越界、不是墙壁、未被访问,则递归探索该方向。
    • 若递归返回成功(找到终点),则继续回溯或返回;若失败,则尝试下一个方向。
    • 回溯清理:若所有方向均无法到达终点,取消当前位置的 “已访问” 标记,返回上一步。

算法流程图示

(以 3×3 迷宫为例)

LeetCode例题实战

例题1:490. 迷宫(中等)

题目描述:由空地(0)和墙(1)组成的迷宫中有一个球。球可以向上下左右四个方向滚动,但在遇到墙之前不会停止滚动。当球停下时,可以选择下一个方向。给定球的起始位置、目的地和迷宫,判断球能否在目的地停下。

示例

输入:

迷宫:[[0,0,1,0,0],[0,0,0,0,0],[0,0,0,1,0],[1,1,0,1,1],[0,0,0,0,0]]

起始位置:[0,4]

目的地:[4,4]

输出:true

解释:球可以滚动到目的地(路径:右→下→左→下→右)

解题思路

1. **与传统迷宫的区别**:球会“滚动”直到撞墙才停下,而非一步一格移动。

2. **回溯思路**:

- 从起点出发,尝试四个方向滚动,记录停下的位置(撞墙前的最后一个空地)。

- 若停下的位置是终点,返回true。

- 若该位置已访问过,跳过(避免循环)。

- 否则标记为已访问,递归探索从该位置出发的四个方向。

- 回溯时无需取消标记(因滚动路径唯一,不会重复访问)。

代码实现
class Solution {

    private int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; // 上、下、左、右

    private int rows, cols;

    private boolean[][] visited;

    public boolean hasPath(int[][] maze, int[] start, int[] destination) {

        rows = maze.length;

        cols = maze[0].length;

        visited = new boolean[rows][cols];

        return backtrack(maze, start[0], start[1], destination);

    }

    private boolean backtrack(int[][] maze, int x, int y, int[] dest) {

// 若当前位置是终点,返回true

        if (x == dest[0] && y == dest[1]) {

            return true;

        }

// 标记当前位置为已访问

        visited[x][y] = true;

// 尝试四个方向滚动

        for (int[] dir : directions) {

            int nx = x, ny = y;

// 滚动直到撞墙

            while (nx + dir[0] >= 0 && nx + dir[0] < rows &&

                    ny + dir[1] >= 0 && ny + dir[1] < cols &&

                    maze[nx + dir[0]][ny + dir[1]] == 0) {

                nx += dir[0];

                ny += dir[1];

            }

// 若停下的位置未访问,递归探索

            if (!visited[nx][ny] && backtrack(maze, nx, ny, dest)) {

                return true;

            }

        }

// 所有方向均无法到达,返回false

        return false;

    }

}
复杂度分析
  • 时间复杂度:O (rows×cols×(rows+cols)),每个位置最多访问一次,每次滚动最多移动 rows+cols 格。
  • 空间复杂度:O (rows×cols),visited 数组和递归栈的空间。

例题 2:79. 单词搜索(中等)

题目描述:给定一个 m x n 二维网格和一个单词,找出该单词是否存在于网格中。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中 “相邻” 单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例

输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"

输出:true

解题思路
  1. 问题抽象:网格可视为特殊迷宫,每个单元格是 “字母”,路径需按单词顺序,且不可重复访问。
  2. 回溯思路
    • 遍历网格,找到单词首字母作为起点。
    • 从起点出发,尝试四个方向,若下一个字母匹配且未访问,递归探索。
    • 若递归长度等于单词长度,返回 true。
    • 回溯时取消当前单元格的访问标记。
代码实现
class Solution {

    private int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

    private int rows, cols;

    private boolean[][] visited;

    public boolean exist(char[][] board, String word) {

        rows = board.length;

        cols = board[0].length;

        visited = new boolean[rows][cols];

// 遍历网格找起点

        for (int i = 0; i < rows; i++) {

            for (int j = 0; j < cols; j++) {

                if (board[i][j] == word.charAt(0) && backtrack(board, word, i, j, 0)) {

                    return true;

                }

            }

        }

        return false;

    }

    private boolean backtrack(char[][] board, String word, int x, int y, int index) {

// 若匹配到单词末尾,返回true

        if (index == word.length() - 1) {

            return true;

        }

        visited[x][y] = true;

// 尝试四个方向

        for (int[] dir : directions) {

            int nx = x + dir[0];

            int ny = y + dir[1];

// 检查边界、未访问、字母匹配

            if (nx >= 0 && nx < rows && ny >= 0 && ny < cols &&

                    !visited[nx][ny] && board[nx][ny] == word.charAt(index + 1)) {

                if (backtrack(board, word, nx, ny, index + 1)) {

                    return true;

                }

            }

        }

// 回溯:取消标记

        visited[x][y] = false;

        return false;

    }

}
复杂度分析
  • 时间复杂度:O (m×n×3^L),m 和 n 为网格尺寸,L 为单词长度。每个单元格最多访问一次,每次有 3 个新方向(排除来时方向)。
  • 空间复杂度:O (L),递归栈深度为 L(单词长度),visited 数组为 O (m×n)。

考研 408 例题解析

例题 1:概念辨析题(选择题)

题目:关于回溯算法求解迷宫寻路问题,下列说法正确的是( )。

A. 回溯算法总能找到最短路径

B. 回溯算法的时间复杂度一定优于广度优先搜索(BFS)

C. 回溯算法通过递归实现,无需显式栈

D. 回溯过程中必须恢复访问标记,否则可能遗漏有效路径

答案:D

解析

  • A 错误:回溯算法会探索所有路径,但不一定优先找到最短路径(BFS 更适合最短路径)。
  • B 错误:回溯算法时间复杂度通常为指数级(如 O (4^(m×n))),而 BFS 为 O (m×n),更优。
  • C 错误:回溯算法的递归本质是利用系统栈,仍属于栈结构,只是无需手动实现。
  • D 正确:若不恢复访问标记,已探索路径的标记会阻塞其他可能的有效路径,导致漏解。

例题 2:算法设计题(408 高频考点)

题目:设计一个回溯算法,找出迷宫中从起点到终点的所有路径。要求每条路径用坐标序列表示(如(0,0)→(0,1)→...→(m-1,n-1)),并分析算法的时间复杂度。

解题思路
  1. 数据结构
    • maze:二维数组表示迷宫(0 为通路,1 为墙)。
    • path:列表记录当前路径的坐标。
    • visited:二维数组标记已访问的位置。
    • result:列表存储所有有效路径。
  1. 递归函数:backtrack(x, y)表示从(x,y)出发探索路径。
  2. 步骤
    • 若(x,y)是终点,将当前path加入result。
    • 否则,对四个方向:
      • 若方向合法(不越界、非墙、未访问),标记访问,加入path,递归探索。
      • 回溯:移除path中的坐标,取消访问标记。
代码实现
import java.util.*;

public class MazeAllPaths {

    private int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

    private int rows, cols;

    private List<List<String>> result = new ArrayList<>();

    public List<List<String>> findAllPaths(int[][] maze, int[] start, int[] end) {

        rows = maze.length;

        cols = maze[0].length;

        boolean[][] visited = new boolean[rows][cols];

        List<String> path = new ArrayList<>();

// 起点合法性检查

        if (maze[start[0]][start[1]] == 1) {

            return result;

        }

        path.add("(" + start[0] + "," + start[1] + ")");

        visited[start[0]][start[1]] = true;

        backtrack(maze, start[0], start[1], end, visited, path);

        return result;

    }

    private void backtrack(int[][] maze, int x, int y, int[] end,

                           boolean[][] visited, List<String> path) {

// 到达终点,记录路径

        if (x == end[0] && y == end[1]) {

            result.add(new ArrayList<>(path));

            return;

        }

// 尝试四个方向

        for (int[] dir : directions) {

            int nx = x + dir[0];

            int ny = y + dir[1];

// 检查边界、是否为墙、是否已访问

            if (nx >= 0 && nx < rows && ny >= 0 && ny < cols &&

                    maze[nx][ny] == 0 && !visited[nx][ny]) {

// 标记访问,加入路径

                visited[nx][ny] = true;

                path.add("(" + nx + "," + ny + ")");

// 递归探索

                backtrack(maze, nx, ny, end, visited, path);

// 回溯:移除路径,取消标记

                path.remove(path.size() - 1);

                visited[nx][ny] = false;

            }

        }

    }

}

复杂度分析
  • 时间复杂度:O (4^(m×n)),每个单元格最多被访问一次,每次有 4 个方向可选,最坏情况下需探索所有可能路径。
  • 空间复杂度:O (m×n),包括visited数组(m×n)、递归栈深度(最坏为 m×n)和路径存储(最长路径长度为 m×n)。

迷宫寻路的扩展与应用

实际应用场景

  • 机器人导航:机器人在未知环境中探索路径,类似迷宫寻路逻辑。
  • 游戏开发:角色在地图中寻找宝藏、避开障碍物的路径规划。
  • 网络路由:数据包在网络节点间的传输路径选择,需避开故障节点。

与其他算法的对比

算法

核心思路

适用场景

时间复杂度

回溯算法

递归探索所有路径

求所有路径、判断路径存在性

O(4^(m×n))

广度优先搜索(BFS)

队列逐层探索

求最短路径

O(m×n)

深度优先搜索(DFS)

栈或递归深度探索

与回溯类似,实现方式不同

O(4^(m×n))

A * 算法

启发式搜索(估价函数)

大规模迷宫的高效路径规划

接近 O (m×n)(最优情况)

考研 408 备考要点

  • 核心考点:回溯算法的递归实现、访问标记的管理、路径存储与恢复。
  • 重点掌握
  1. 迷宫寻路中 “边界检查”“墙检查”“访问检查” 三要素。
  2. 回溯与递归的关系,以及回溯过程中状态的恢复逻辑。
  3. 不同迷宫变种(如滚动球、带权值迷宫)的算法调整。
  • 常见错误
    • 遗漏边界检查导致数组越界。
    • 回溯时未恢复访问标记,导致路径探索不完整。
    • 混淆 “一步一格” 与 “滚动到墙” 的移动逻辑。

总结

迷宫寻路问题是回溯算法的典型应用,其核心 “递归探索 - 标记 - 回溯” 逻辑可迁移到多种路径规划场景。本文通过 LeetCode 例题(490 题和 79 题)展示了不同约束条件下的回溯实现,通过考研 408 例题解析了概念辨析和全路径求解思路,并结合 SVG 图示直观呈现了探索与回溯的过程。

掌握迷宫寻路的关键在于:

  1. 明确问题约束(移动方式、路径规则),针对性设计探索逻辑。
  2. 严格管理访问标记,确保回溯时状态正确恢复。
  3. 理解回溯算法与其他路径算法的适用场景差异(如 BFS 更适合最短路径)。

在考研备考中,需重点关注回溯算法的时间复杂度分析和状态管理细节,这不仅有助于解决迷宫问题,也能深化对递归和组合优化问题的理解。

希望本文能够帮助读者更深入地理解贪心算法中迷宫寻路算法,并在实际项目中发挥其优势。谢谢阅读!


希望这份博客能够帮助到你。如果有其他需要修改或添加的地方,请随时告诉我。


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呆呆企鹅仔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值