跳转至

P1443 马的遍历

题目描述

有一个 \(n \times m\) 的棋盘,在某个点 \((x, y)\) 上有一个马,要求你计算出马到达棋盘上任意一个点最少要走几步。

输入格式

输入只有一行四个整数,分别为 \(n, m, x, y\)

输出格式

一个 \(n \times m\) 的矩阵,代表马到达某个点最少要走几步(不能到达则输出 \(-1\))。

输入输出样例 #1

输入 #1

3 3 1 1

输出 #1

0 3 2    
3 -1 1    
2 1 4    

说明/提示

数据规模与约定

对于全部的测试点,保证 \(1 \leq x \leq n \leq 400\)\(1 \leq y \leq m \leq 400\)

2022 年 8 月之后,本题去除了对输出保留场宽的要求。为了与之兼容,本题的输出以空格或者合理的场宽分割每个整数都将判作正确。

📌 题目分析

给定数据:

n 行 m 列的棋盘 (1 ≤ n, m ≤ 400)
起点坐标 (x, y)

要求计算出马按照“日”字步规则,到达棋盘上每一个格子所需要的最少步数。如果无法到达,则输出 -1

👉 本质:

无权图的单源最短路径搜索

👉 类型:

广度优先搜索 (BFS) 基础模板题

🧠 解题思路

1️⃣ 为什么求最短步数必须用 BFS?

这是搜索类题目的一条铁律: * 求所有方案数 / 连通性:用 DFS。 * 求无权图的“最短路径 / 最少步数”必须用 BFS

DFS 是一条路走到黑,它最先找到的路径不一定是最短的(除非你把所有路径都搜出来对比,但这绝对会超时)。 BFS 则是像水波涟漪一样,一层一层向外扩散。第一步能到的点全搜完,再去搜第二步能到的点。因此,BFS 第一次踩到某个格子的步数,绝对就是从起点到它的最短步数

2️⃣ 马的八个方向

国际象棋中的马(和中国象棋类似)走“日”字,在一个没有任何阻挡的 5×5 区域中心,它一步可以跳到 8 个不同的位置。 我们需要用两个方向数组来记录这 8 种横纵坐标的变化规律:

int dx[8] = {-2, -2, -1, -1, 1, 1, 2, 2};
int dy[8] = {-1, 1, -2, 2, -2, 2, -1, 1};

3️⃣ 状态数组的妙用

题目要求不能到达的地方输出 -1。 为了极致精简代码,我们可以直接把存储答案的 dist 二维数组初始化为全部是 -1。 这样一来,这个数组同时具备了两个功能: 1. 记录答案:走到某个点时,存入所需的步数。 2. 充当 vis 访问标记:如果在 BFS 扩散时,发现下一个格子的 dist 已经不是 -1 了,说明之前已经被更短(或相等)的步数访问过,直接跳过即可。

4️⃣ 时间复杂度

在 BFS 中,借助队列,棋盘上的每个格子最多只会被入队和出队一次。

时间复杂度:O(n × m)

最大 \(400 \times 400 = 160000\) 个格子,计算量极小,在 1 秒时限内轻轻松松 \(0\) 毫秒秒杀。


💻 C++代码

#include <iostream>
#include <vector>
#include <queue>

using namespace std;

// 马走“日”字的 8 个方向
int dx[8] = {-2, -2, -1, -1, 1, 1, 2, 2};
int dy[8] = {-1, 1, -2, 2, -2, 2, -1, 1};

int main() {
    // I/O 优化
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m, x, y;
    cin >> n >> m >> x >> y;

    // 初始化距离数组,全部默认赋值为 -1
    // 大小开到 n+1 和 m+1,方便下标从 1 开始对应坐标系
    vector<vector<int>> dist(n + 1, vector<int>(m + 1, -1));
    queue<pair<int, int>> q;

    // 1. 起点初始化
    dist[x][y] = 0;
    q.push({x, y});

    // 2. 广度优先搜索 BFS
    while (!q.empty()) {
        int cx = q.front().first;
        int cy = q.front().second;
        q.pop();

        // 尝试向 8 个方向跳跃
        for (int i = 0; i < 8; i++) {
            int nx = cx + dx[i];
            int ny = cy + dy[i];

            // 检查边界,并且必须是尚未访问过的格子(dist 为 -1)
            if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && dist[nx][ny] == -1) {
                // 走到这一步的最短步数 = 上一步的最短步数 + 1
                dist[nx][ny] = dist[cx][cy] + 1;
                q.push({nx, ny});
            }
        }
    }

    // 3. 按矩阵格式输出结果
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            cout << dist[i][j] << " ";
        }
        cout << "\n";
    }

    return 0;
}

⚠️ 易错点

❌ 1. 执迷不悟使用 DFS

有些新手拿到这种棋盘走格子的题目,肌肉记忆就敲了 DFS 进去,还会特意写一个很复杂的逻辑:“如果走到某个格子的步数比以前记录的小,就更新它并继续搜”。 这是灾难级的写法! 这会导致原本 \(O(N)\) 的图遍历退化成近乎指数级的盲目回溯,在 \(400 \times 400\) 的图里绝对会 TLE(超时)到令人发指。记住:无权最短路,只用 BFS。

❌ 2. 坐标数组对应写错

马的 8 个方向虽然很常规,但如果 dxdy 没有完全匹配对应(比如写成了直走 2 步又拐 2 步),就会导致马变成了“变异马”。写完方向数组后,最好脑内过一遍:\(1^2 + 2^2 = 5\),保证每一对坐标的变化量平方和都是 \(5\),这才是一步纯正的“日”字。

❌ 3. 多重访问导致队列撑爆 (MLE)

入队的瞬间就要立刻改变状态/标记。 如果我们在 q.push({nx, ny}) 之后,不立刻更新 dist[nx][ny] = ...,而是等到出队 q.pop() 的时候才去更新。这会导致同一个目标格子被周边的多个邻居反复塞入队列,队列会指数级膨胀,直接触发内存超限 (MLE)。


🚀 一句话总结

求最短步数认准 BFS,初始化全局 -1 既当答案又当标记,队列一层层推平整个棋盘!

🔥 补充:硬核知识点

为什么 BFS 第一次搜到就必定是最短路?

我们可以从队列的单调性来深度理解 BFS: 在 BFS 执行的过程中,队列里存储的结点的步数(距离源点的距离)总是满足一个规律:要么全都是 \(k\),要么一部分是 \(k\),一部分是 \(k+1\),绝对不可能出现跨越式的值(比如同时存在步数为 \(2\)\(4\) 的节点)。

这种特性被称为“两段性”和“单调性”。它保证了: 当我们从队列里拿出距离为 \(k\) 的点,去更新它的邻居时,邻居的距离必然是 \(k+1\)。而任何想要到达该邻居的其他路径,只可能在队列的更后方出现,所以后面出现的路径距离一定是 \(\ge k+1\) 的。因此,“先到先得”的原则在无权图中完美成立!