幻象迷宫

GitHub跳转原题关系图返回列表

在模板范围内搜索,并记录每个模格子第一次对应的绝对坐标;若同一模格子被不同绝对坐标到达,就能无限走远。

OJ: luogu

题目 ID: P1363

难度:普及+/提高

标签:BFS图论网格周期python

日期: 2026-06-20 14:41

题意

给一个 n * m 的迷宫模板,它会向四周无限平铺。

模板中:

  • . 是路
  • # 是墙
  • S 是起点

每次可以上下左右移动一格,不能走到墙上。

问能否从起点走到距离起点无限远的地方。

思路

先看一个小数据暴力:

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 15;

int n, m;
char mp[MAXN][MAXN];
int sx, sy;

// brute.cpp:小数据暴力解。
// 把原迷宫向四周复制很多份,起点放在中间那份里,
// 如果能走到这个大盒子的边界,就认为可以无限走远。

int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};

struct Node {
    int x, y;
};

int norm_x(int x) {
    x %= n;
    if (x < 0) x += n;
    return x;
}

int norm_y(int y) {
    y %= m;
    if (y < 0) y += m;
    return y;
}

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

    while (cin >> n >> m) {
        sx = -1;
        sy = -1;
        for (int i = 0; i < n; i++) {
            string s;
            cin >> s;
            for (int j = 0; j < m; j++) {
                mp[i][j] = s[j];
                if (mp[i][j] == 'S') {
                    sx = i;
                    sy = j;
                }
            }
        }

        int tile_radius = n * m + 1;
        int H = (tile_radius * 2 + 1) * n;
        int W = (tile_radius * 2 + 1) * m;

        vector<string> big(H, string(W, '#'));
        for (int i = 0; i < H; i++) {
            for (int j = 0; j < W; j++) {
                char c = mp[i % n][j % m];
                if (c == 'S') c = '.';
                big[i][j] = c;
            }
        }

        int start_x = tile_radius * n + sx;
        int start_y = tile_radius * m + sy;

        vector<vector<char> > vis(H, vector<char>(W, 0));
        queue<Node> q;
        vis[start_x][start_y] = 1;
        q.push({start_x, start_y});

        bool ok = false;

        while (!q.empty()) {
            Node cur = q.front();
            q.pop();

            // 能到达大盒子边界,说明可以走得足够远,
            // 对于这个规模的盒子,就等价于能无限走远。
            if (cur.x == 0 || cur.x == H - 1 || cur.y == 0 || cur.y == W - 1) {
                ok = true;
                break;
            }

            for (int k = 0; k < 4; k++) {
                int nx = cur.x + dx[k];
                int ny = cur.y + dy[k];
                if (nx < 0 || nx >= H || ny < 0 || ny >= W) {
                    continue;
                }
                if (vis[nx][ny] || big[nx][ny] == '#') {
                    continue;
                }
                vis[nx][ny] = 1;
                q.push({nx, ny});
            }
        }

        if (ok) {
            cout << "Yes\n";
        }
        else {
            cout << "No\n";
        }
    }

    return 0;
}

它把模板向四周复制很多份,在一个足够大的盒子里做 BFS。

这个做法适合帮助理解,也适合对拍,但不能用于正式数据。

本题真正的关键是:

  • 迷宫虽然无限大
  • 但地形只由 (x mod n, y mod m) 决定

也就是说,不同的绝对坐标,可能对应模板中的同一个格子。

于是可以得到一个非常关键的等价条件:

如果同一个模格子,被两个不同的绝对坐标访问到了,那么答案就是 Yes

原因是这两个位置周围环境完全相同,所以从一个走到另一个的路径可以不断平移复制,进而无限走远。

反过来,如果真的能无限走远,由于模板里只有 n*m 个模格子,根据抽屉原理,也一定会出现这种情况。

所以做法就变成:

  1. 队列里存绝对坐标
  2. 访问判重只按模格子判
  3. 但每个模格子要记录“第一次到达它时的绝对坐标”
  4. 如果后来同一个模格子又被另一个不同绝对坐标访问到,立刻输出 Yes

如果整次 BFS 结束都没发生这种情况,输出 No

Python 知识

  • Python 的 % 对负数仍返回非负余数,next_x % n 可直接映射到模板行。
  • array('i') 用 4 字节整数保存最多 225 万个坐标,避免两个 Python 整数列表占用过多内存。
  • 两个 array 加读取指针 head 组成紧凑队列,避免为每个格子创建坐标元组。
  • 网格行保持为 bytes,用 ASCII 码 35 判断 # 墙。
  • /home/rainboy/mycode/hugo-blog/content/program_language/python/bfs_shortest.md:BFS 队列和访问状态。
  • /home/rainboy/mycode/hugo-blog/content/program_language/python/cpp_to_python_pitfalls.md:大网格对象内存与递归限制。

代码

python
import sys
from array import array


UNVISITED = -2_147_483_648
DIRECTIONS = ((-1, 0), (1, 0), (0, -1), (0, 1))


def can_escape(grid, start):
    n, m = len(grid), len(grid[0])
    total = n * m
    first_x = array("i", [UNVISITED]) * total
    first_y = array("i", [UNVISITED]) * total
    start_x, start_y = start
    start_index = start_x * m + start_y
    first_x[start_index] = start_x
    first_y[start_index] = start_y

    queue_x = array("i", [start_x])
    queue_y = array("i", [start_y])
    head = 0
    while head < len(queue_x):
        x, y = queue_x[head], queue_y[head]
        head += 1
        for dx, dy in DIRECTIONS:
            next_x, next_y = x + dx, y + dy
            row, column = next_x % n, next_y % m
            if grid[row][column] == 35:
                continue
            index = row * m + column
            if first_x[index] == UNVISITED:
                first_x[index] = next_x
                first_y[index] = next_y
                queue_x.append(next_x)
                queue_y.append(next_y)
            elif first_x[index] != next_x or first_y[index] != next_y:
                return True
    return False


def main():
    read = sys.stdin.buffer.readline
    answer = []
    while True:
        header = read().split()
        if not header:
            break
        n, m = map(int, header)
        grid = []
        start = None
        for row in range(n):
            line = read().strip()
            column = line.find(b"S")
            if column != -1:
                start = (row, column)
            grid.append(line)
        answer.append("Yes" if can_escape(grid, start) else "No")
    print("\n".join(answer))


if __name__ == "__main__":
    main()

复杂度

  • 时间复杂度:O(nm)O(nm)
  • 空间复杂度:O(nm)O(nm)

总结

这题最核心的转化是:

  • 把“无限迷宫能否走出去”
  • 变成“同一个模格子是否会对应多个可达绝对坐标”

一旦抓住这个等价关系,整题就只需要在模板大小范围内搜索。