跳转至

P1605 迷宫

题目描述

给定一个 \(N \times M\) 方格的迷宫,迷宫里有 \(T\) 处障碍,障碍处不可通过。

在迷宫中移动有上下左右四种方式,每次只能移动一个方格。数据保证起点上没有障碍。

给定起点坐标和终点坐标,每个方格最多经过一次,问有多少种从起点坐标到终点坐标的方案。

输入格式

第一行为三个正整数 \(N,M,T\),分别表示迷宫的长宽和障碍总数。

第二行为四个正整数 \(SX,SY,FX,FY\)\(SX,SY\) 代表起点坐标,\(FX,FY\) 代表终点坐标。

接下来 \(T\) 行,每行两个正整数,表示障碍点的坐标。

输出格式

输出从起点坐标到终点坐标的方案总数。

输入输出样例 #1

输入 #1

2 2 1
1 1 2 2
1 2

输出 #1

1

说明/提示

对于 \(100\%\) 的数据,\(1 \le N,M \le 5\)\(1 \le T \le 10\)\(1 \le SX,FX \le N\)\(1 \le SY,FY \le M\)

📌 题目分析

给定数据:

长宽 N, M (最大均为 5)
障碍总数 T (最大 10)
求从起点 (SX, SY) 到终点 (FX, FY) 的所有合法路径总数。
每个格子最多经过一次。

👉 本质:

图的遍历 / 枚举所有可能的路径

👉 类型:

深度优先搜索 (DFS) / 回溯法 (Backtracking)

🧠 解题思路

1️⃣ 为什么使用 DFS 而不是 BFS?

这是迷宫题里最容易搞混的知识点: * 如果题目问的是“最短路径是多少”或者“最少走几步”,我们首选 BFS (广度优先搜索)。 * 但这道题问的是“一共有多少种走法”,要求我们找出所有能到达终点的路径。在极端小的数据范围(最多 25 个格子)下,通过 DFS (深度优先搜索) 一条路走到黑,撞墙再退回来换条路走(回溯),是标准的解法。

2️⃣ 核心回溯逻辑(保护与恢复现场)

所谓回溯,就是“试错并退回”。 当我们走到坐标 (x, y) 时: 1. 标记:我们把这个格子标记为“已走过”,防止在这一条路径里绕圈子死循环。 2. 深入:尝试向上下左右四个方向走。 3. 撤销:当我们把这四个方向都试完,准备退回到上一步时,必须把 (x, y) 的“已走过”标记擦除。因为在这个大格子里,另一条完全不同的路径可能还会经过这里。

3️⃣ 边界与障碍物处理

开一个二维数组 maze 记录障碍物(如 1 表示障碍,0 表示空地)。 开一个二维布尔数组 vis 记录当前路径是否走过。 每次向四个方向走之前,做一次安全检查: 不能越界、不能是障碍物、不能是当前路径已经走过的格子。

4️⃣ 时间复杂度

在没有任何剪枝的情况下,每次最多有 3 个方向可以走(除了来的方向)。最坏的时间复杂度接近指数级: \(O(3^{N \times M})\) 但在 \(N, M \le 5\) 的超小数据范围内,运算量极小,绝对可以在 1 秒内跑完。


💻 C++代码

#include <iostream>
using namespace std;

int n, m, t;
int sx, sy, fx, fy;
int ans = 0;

int maze[10][10]; // 0 表示空地,1 表示障碍
bool vis[10][10]; // true 表示当前路径已访问过该点

// 方向数组:上下左右
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};

void dfs(int x, int y) {
    // 边界条件:到达终点
    if (x == fx && y == fy) {
        ans++; // 方案数加一
        return;
    }

    // 枚举四个方向
    for (int i = 0; i < 4; i++) {
        int nx = x + dx[i];
        int ny = y + dy[i];

        // 检查:1. 是否越界  2. 是否是障碍  3. 是否已访问
        if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && maze[nx][ny] == 0 && !vis[nx][ny]) {

            vis[nx][ny] = true; // 【标记】走到新格子,保护现场

            dfs(nx, ny);        // 【深入】继续往下搜

            vis[nx][ny] = false; // 【回溯】退回来时,恢复现场,供其他路径使用
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m >> t;
    cin >> sx >> sy >> fx >> fy;

    // 读入障碍物坐标
    for (int i = 0; i < t; i++) {
        int r, c;
        cin >> r >> c;
        maze[r][c] = 1; // 标记为障碍物
    }

    // !!! 极其重要:起点在出发前必须被标记为已访问
    vis[sx][sy] = true;

    // 从起点开始搜索
    dfs(sx, sy);

    cout << ans << "\n";

    return 0;
}

⚠️ 易错点

❌ 1. 忘记标记起点

这是无数人吃过亏的地方:

// 必须在调用 dfs 之前写上这句:
vis[sx][sy] = true; 

如果没有这一句,你在搜索的过程中,很可能会从起点的旁边又走回到起点,导致起点被经过了两次,最终答案会比正确答案偏大。

❌ 2. 回溯漏掉恢复现场

如果忘记写 vis[nx][ny] = false;,那么你的 DFS 就不再是找“所有”路径,而是类似于涂色游戏,走过的地方就永远被堵死了。这会导致最终搜出来的路径数量极少。

❌ 3. 数组越界导致 RE

题目的行列是从 1\(N\)\(M\)。所以在判断边界时,必须是 >= 1<= n。如果习惯了从 0 开始遍历的写法,直接写成 >= 0 会导致访问到矩阵外层的默认零值,从而产生逻辑错误甚至越界崩溃。


🚀 一句话总结

迷宫数路径的本质就是一条路撞南墙,撤回来擦掉脚印换条路走,直到撞出所有的终点。

🔥 补充:硬核知识点

搜索算法的核心哲学:状态树的遍历

很多初学者觉得 DFS 和回溯法很抽象。其实你可以把迷宫想象成一棵巨大的树: * 树根就是起点 (SX, SY)。 * 树的枝干就是你在每个格子上做出的四个方向的选择。 * 树的叶子要么是死胡同(撞墙/越界),要么就是终点 (FX, FY)

在这个视角下: * DFS(深度优先) 是一只老鼠,它顺着一根树枝一直爬,要么爬到终点(答案+1),要么爬到尽头死胡同。无论如何它都会原路退回(也就是所谓的回溯),去爬下一根树枝。所以它能极其方便地统计出共有多少条路。 * BFS(广度优先) 则像是一滴水滴在迷宫起点,它以涟漪的形态同时向四周扩散。最先触碰到终点的那一圈波纹,必定就是最短的一条路。但它很难搞清楚到底有多少种独立的波纹轨迹(因为波纹在扩散时融合了)。

理解了老鼠(DFS)与水波(BFS)的意象,你就能在看到题目问“几条路”还是“几步路”时,瞬间选出最正确的算法武器!