瑞瑞的无限副本

【题目来源】

瑞学堂:瑞瑞的无限副本

【题目描述】

瑞瑞正在玩一个冒险游戏,需要在一个无限大的副本中收集尽可能多的能量水晶。

副本的地图由一块 \(n×m\) 的小地图作为 种子 在水平和垂直方向无限平铺而成。小地图中的每个格子用字符 # 表示无法通行的障碍,用 . 表示可通行的空地,每块空地上恰好有一颗能量水晶。平铺时每个副本都与原种子地图完全相同,因此在大地图上,坐标为 \((i,j)\) 的格子对应的种子地图坐标为 \(((i−1)\ mod\ n+1,(j−1)\ mod\ m+1)\)(行列均从 \(1\) 开始编号)。

不同种子格子上的水晶种类互不相同,因此整个无限世界中的水晶总种类数等于种子地图中空地的数量。瑞瑞的角色出生在一块空地上,每次可以向上、下、左、右四个方向移动,每次只能移动到相邻的空地上,且可以跨越种子地图边界进入相邻的副本。每走到一块空地,上面的水晶就会被收集并消失。

如果角色能够访问到无限多个不同的格子,就可以获得无限数量的水晶(种类可能重复,但数量无限);否则只能获得有限数量的水晶。

现在给定种子地图以及出生位置,请你帮瑞瑞判断他能否获得无限数量的水晶,并计算出他最多能收集到多少种不同种类的水晶。

【输入】

第一行包含两个整数 \(n,m\),表示种子地图的行数和列数。

接下来 \(n\) 行,每行一个长度为 \(m\) 的字符串,只包含字符 # 和 .,描述种子地图。

最后一行包含两个整数 \(x,y\),表示瑞瑞的出生位置在第 \(x\) 行第 \(y\) 列(行列均从 \(1\) 开始编号)。

数据保证出生位置一定是空地(即对应字符为 .)。

【输出】

输出共两行:

第一行输出一个单词,如果瑞瑞能获得无限数量的水晶,输出 Yes,否则输出 No
第二行输出一个整数,表示瑞瑞最多能收集到的不同种类水晶的数量。

【输入样例】

2 2
.#
..
1 1

【输出样例】

Yes
3

【核心思想】

  1. 问题分析:给定 \(n \times m\) 的种子地图(. 为空地,# 为障碍),在水平和垂直方向无限平铺。瑞瑞从 \((x, y)\) 出发,每次向四邻域移动,可跨边界进入相邻副本。需判断是否能访问无限多个不同格子(即存在无限循环路径),并求最多能收集到多少种不同种类的水晶(即种子地图中可达的空地种类数)。这是一个无限网格 BFS + 环检测问题,关键在于用副本偏移量检测是否可通过不同路径到达同一种子位置的不同副本。

  2. 算法选择

    • BFS(广度优先搜索):在种子地图的坐标空间上搜索,利用队列扩展四邻域
    • 副本偏移量记录:对每个种子位置 \((x, y)\),记录到达它时的行方向副本偏移 \(da[x][y]\) 和列方向副本偏移 \(db[x][y]\)
    • 无限循环检测:若再次访问到同一种子位置 \((x, y)\),但副本偏移 \((na, nb)\) 与已记录的 \((da[x][y], db[x][y])\) 不同,则说明存在两条不同路径到达同一种子位置的不同副本,可无限循环
  3. 关键步骤

    • 初始化:将出生位置 \((stx, sty)\) 入队,标记 vis[stx][sty] = 1,副本偏移初始化为 \((0, 0)\)
    • BFS 扩展(取出队首 \((x, y)\),遍历四个方向):
      • 坐标变换:计算新位置 \((nx, ny)\),若跨边界则调整坐标(如 \(nx < 0\)\(nx = n-1, na--\))并更新副本偏移
      • 障碍跳过:若 \(g[nx][ny] = \#\),无法通行
      • 首次访问:若 !vis[nx][ny],记录副本偏移 \((na, nb)\),标记访问,入队
      • 环检测:若已访问但 \(da[nx][ny] \neq na\)\(db[nx][ny] \neq nb\),置 flag = true(存在无限循环)
    • 统计答案:遍历种子地图,vis[i][j] 为真则 cnt++(可达空地种类数)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n \cdot m)\),每个种子位置最多被访问一次,四方向扩展
    • 空间复杂度:\(O(n \cdot m)\)visdadb 数组及队列
  5. 无限网格 BFS 的核心思想

    • 模运算降维:无限地图中的任意位置 \((i, j)\) 对应种子位置 \(((i-1) \bmod n + 1, (j-1) \bmod m + 1)\),将无限空间压缩到有限的种子坐标上处理
    • 副本偏移量作为"高维坐标":用 \((da, db)\) 记录在无限地图中跨越了多少个种子副本,相当于将二维种子地图嵌入到四维状态空间 \((x, y, da, db)\)
    • 同位置不同偏移即存在环:若到达同一种子位置 \((x, y)\) 有两种不同的副本偏移,说明从该位置出发可以走一条闭合路径回到"相同相对位置但不同绝对位置"的地方,从而无限重复收集
    • 可达性即种类数:由于不同种子格子上的水晶种类互不相同,且无限平铺不改变种子地图结构,可达的种子空地数量即为不同种类水晶的最大数量
    • 适用于无限周期性网格的可达性与环检测问题,核心在于用偏移量区分"相同相对位置的不同绝对位置"

【算法标签】

BFS-二维

【代码详解】

#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;  // 定义PII为pair<int,int>,用于队列存储坐标
const int N = 1505;          // 定义数组最大尺寸为1505
int n, m, stx, sty;          // n,m为种子地图行列数;stx,sty为出生位置(0-based)
char g[N][N];                // g[i][j]存储种子地图的字符('#'障碍,'.'空地)
int dx[4] = {-1, 1, 0, 0};   // 四个方向的x偏移量:上、下、左、右
int dy[4] = {0, 0, -1, 1};   // 四个方向的y偏移量:上、下、左、右
int da[N][N], db[N][N];      // da[x][y]记录到达(x,y)时横向(行方向)跨越的副本数;db为纵向(列方向)
bool flag = false;           // flag标记是否能到达无限多个不同格子(即存在环可以无限循环)
bool vis[N][N];              // vis[x][y]标记种子坐标(x,y)是否已被BFS访问过

// BFS遍历:在无限平铺的地图中搜索,同时检测是否存在可无限循环的路径
void bfs()
{
    queue<PII> q;            // BFS队列,存储种子地图中的坐标
    q.push({stx, sty});      // 将出生位置入队
    vis[stx][sty] = 1;       // 标记出生位置已访问
    da[stx][sty] = db[stx][sty] = 0;  // 出生位置对应的副本偏移为(0,0)

    while (!q.empty())       // 当队列不为空时继续
    {
        auto [x, y] = q.front(); q.pop();  // 取出队首坐标
        int a = da[x][y], b = db[x][y];    // 当前位置对应的副本偏移
        for (int i = 0; i < 4; i++)        // 枚举四个移动方向
        {
            int nx = x + dx[i];            // 计算新位置的行坐标
            int ny = y + dy[i];            // 计算新位置的列坐标
            int na = a, nb = b;            // 新位置的副本偏移,初始与当前相同

            // 处理跨边界情况:当移动超出种子地图边界时,进入相邻副本
            if (nx < 0)                    // 向上跨出种子地图上边界
            {
                nx = n - 1;                // 从下方相邻副本的对应位置进入
                --na;                      // 行方向副本偏移减1
            }
            else if (nx >= n)              // 向下跨出种子地图下边界
            {
                nx = 0;                    // 从上方相邻副本的对应位置进入
                ++na;                      // 行方向副本偏移加1
            }
            else if (ny < 0)               // 向左跨出种子地图左边界
            {
                ny = m - 1;                // 从右方相邻副本的对应位置进入
                --nb;                      // 列方向副本偏移减1
            }
            else if (ny >= m)              // 向右跨出种子地图右边界
            {
                ny = 0;                    // 从左方相邻副本的对应位置进入
                ++nb;                      // 列方向副本偏移加1
            }

            if (g[nx][ny] == '#')          // 如果新位置是障碍
                continue;                  // 无法通行,跳过

            if (!vis[nx][ny])              // 如果新位置在种子地图中首次被访问
            {
                vis[nx][ny] = 1;           // 标记为已访问
                da[nx][ny] = na;           // 记录到达该位置的行方向副本偏移
                db[nx][ny] = nb;           // 记录到达该位置的列方向副本偏移
                q.push({nx, ny});          // 将新位置入队继续搜索
            }
            else if (da[nx][ny] != na || db[nx][ny] != nb)  // 如果该种子位置已被访问,但副本偏移不同
                flag = true;               // 说明可以通过不同路径到达同一种子位置的不同副本,存在无限循环
        }
    }
}

int main()
{
    cin >> n >> m;           // 读入种子地图的行数和列数
    for (int i = 0; i < n; i++)      // 读入种子地图
        for (int j = 0; j < m; j++)
            cin >> g[i][j];
    cin >> stx >> sty;       // 读入出生位置(1-based)
    stx--, sty--;            // 转换为0-based坐标

    bfs();                   // 执行BFS搜索

    // 统计种子地图中所有可达的空地数量(即不同种类水晶的数量)
    int cnt = 0;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            if (vis[i][j])   // 如果该种子位置可达
                cnt++;       // 不同种类水晶数加1

    // 输出结果:第一行判断是否能获得无限数量水晶,第二行输出不同种类数
    cout << (flag == true ? "Yes" : "No") << endl;
    cout << cnt << endl;
    return 0;
}

【运行结果】

2 2
.#
..
1 1
Yes
3
posted @ 2026-08-06 08:45  团爸讲算法  阅读(6)  评论(0)    收藏  举报