迷宫传送[最短路径]

问题描述

有一个由 H 行、W 列的网格组成的迷宫。用 (i, j) 表示从上往下第 i 行、从左往右第 j 列的格子。

每个格子 (i, j) 的类型由一个字符 S(i, j) 给出,其含义如下:

  • '.' :空单元格

  • '#' :障碍单元格

  • 小写英文字母 (a--z) :传送单元格

在迷宫中,你可以按任意顺序执行以下两种动作,次数不限:

  1. 行走:从当前单元格移动到相邻的上、下、左、右单元格之一。但不能移动到障碍格或网格外。

  2. 传送:当你位于传送单元格时,可以移动到相同字母的任意传送单元格。

请判断能否从单元格 (1,1) 移动到单元格 (H,W) 。如果可能,请输出所需的最小总动作次数;否则输出 -1。

约束条件

  • 1 ≤ H, W ≤ 1000

  • H × W ≥ 2

  • H, W 为整数

  • S(i, j) 是 '.'、'#' 或小写英文字母

  • S(1,1) ≠ '#'

  • S(H,W) ≠ '#'

输入格式

复制代码
H W
S(1,1) S(1,2) ... S(1,W)
...
S(H,1) S(H,2) ... S(H,W)

输出格式

如果可以从 (1,1) 移动到 (H,W),则输出所需的最小总动作数;否则输出 -1。

问题分析

这是一个在网格中寻找从起点(1,1)到终点(H,W)最短路径的问题,但有一个特殊机制:传送。当站在某个字母格子上时,可以瞬间传送到所有相同字母的格子上。这相当于在图中添加了大量"快捷边"。

核心思路

1. 图模型转换

  • 每个格子是一个节点

  • 相邻格子(上下左右)之间有一条无向边(代价为1)

  • 相同字母的所有格子之间完全连接(互相可达,代价为1)

2. 朴素方法的陷阱

如果直接将所有相同字母的格子之间都建边,复杂度会爆炸:

  • 假设有k个字母'a'的格子,它们之间需要建立O(k²)条边

  • 当k很大时(比如样例3中全是'x',k=H×W=16),边数达到O((HW)²),不可接受

3. 关键优化:传送门的一次性使用

核心观察:每个字母的传送机制只需要在第一次遇到该字母时使用一次

为什么?

  1. 第一次遇到字母c的格子时,通过一次传送动作,可以到达所有字母c的格子

  2. 这些新到达的格子,到起点的距离 = 当前距离 + 1

  3. 之后如果再从其他路径到达字母c的格子,距离一定 ≥ 当前距离 + 1

  4. 通过BFS的性质,第一次访问时就是最短路径

  5. 所以之后再次访问同字母的格子不会得到更短路径

因此,我们可以:

  • 在第一次访问某个字母的任意格子时,将该字母的所有格子都加入队列

  • 然后立即清空这个字母的格子列表

  • 之后遇到相同字母的格子时,不再进行传送操作

算法实现详解

数据结构设计

复制代码
vector<string> S(H);           // 存储迷宫
vector<vector<pair<int, int>>> tele(26);  // 26个字母对应的格子列表
vector<vector<int>> dist(H, vector<int>(W, INF));  // 最短距离
queue<pair<int, int>> q;       // BFS队列

预处理:收集传送门

复制代码
for (int i = 0; i < H; i++) {
    for (int j = 0; j < W; j++) {
        if (S[i][j] >= 'a' && S[i][j] <= 'z') {
            tele[S[i][j] - 'a'].emplace_back(i, j);
        }
    }
}

这里为每个小写字母建立一个列表,存储所有该字母格子的坐标。

BFS主循环

复制代码
while (!q.empty()) {
    auto [x, y] = q.front();
    q.pop();
    int d = dist[x][y];
    
    // 到达终点,直接输出(BFS第一次到达就是最短)
    if (x == H-1 && y == W-1) {
        cout << d << "\n";
        return 0;
    }
    
    // 1. 行走扩展:向四个方向移动
    for (int k = 0; k < 4; k++) {
        int nx = x + dx[k];
        int ny = y + dy[k];
        // 检查边界、不是障碍、且未访问过
        if (nx >= 0 && nx < H && ny >= 0 && ny < W && 
            S[nx][ny] != '#' && dist[nx][ny] > d + 1) {
            dist[nx][ny] = d + 1;
            q.emplace(nx, ny);
        }
    }
    
    // 2. 传送扩展:如果当前是字母格子
    if (S[x][y] >= 'a' && S[x][y] <= 'z') {
        int c = S[x][y] - 'a';
        // 遍历该字母的所有格子
        for (auto [tx, ty] : tele[c]) {
            // 如果这个格子还没被访问过
            if (dist[tx][ty] > d + 1) {
                dist[tx][ty] = d + 1;
                q.emplace(tx, ty);
            }
        }
        tele[c].clear();  // 关键:清空,避免重复传送
    }
}

关键点解释

为什么用 dist[nx][ny] > d + 1而不是 dist[nx][ny] == INF

这是为了处理重复入队的情况。在某些情况下,一个格子可能通过不同路径多次被尝试访问,我们只保留最短距离。

为什么传送后要清空列表

这是算法的核心优化:

  • 第一次遇到字母c时,距离是d

  • 将所有字母c的格子标记为距离d+1

  • 之后如果再遇到字母c的其他格子,距离至少是d+1

  • 而d+1 ≥ d+1,不会产生更短路径

  • 清空后,后续遇到同字母格子时,tele[c]为空,不会进入循环

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

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

    复制代码
      int H, W;
      cin >> H >> W;
      
      vector<string> S(H);
      for (int i = 0; i < H; i++) {
          cin >> S[i];
      }
      
      vector<vector<pair<int, int>>> tele(26);
      for (int i = 0; i < H; i++) {
          for (int j = 0; j < W; j++) {
              if (S[i][j] >= 'a' && S[i][j] <= 'z') {
                  tele[S[i][j] - 'a'].emplace_back(i, j);
              }
          }
      }
      
      const int INF = 1e9;
      vector<vector<int>> dist(H, vector<int>(W, INF));
      queue<pair<int, int>> q;
      
      dist[0][0] = 0;
      q.emplace(0, 0);
      
      const int dx[4] = {1, -1, 0, 0};
      const int dy[4] = {0, 0, 1, -1};
      
      while (!q.empty()) {
          auto [x, y] = q.front();
          q.pop();
          int d = dist[x][y];
          
          if (x == H-1 && y == W-1) {
              cout << d << "\n";
              return 0;
          }
          
          for (int k = 0; k < 4; k++) {
              int nx = x + dx[k];
              int ny = y + dy[k];
              if (nx >= 0 && nx < H && ny >= 0 && ny < W && S[nx][ny] != '#' && dist[nx][ny] > d + 1) {
                  dist[nx][ny] = d + 1;
                  q.emplace(nx, ny);
              }
          }
    
          if (S[x][y] >= 'a' && S[x][y] <= 'z') {
              int c = S[x][y] - 'a';
              for (auto [tx, ty] : tele[c]) {
                  if (dist[tx][ty] > d + 1) {
                      dist[tx][ty] = d + 1;
                      q.emplace(tx, ty);
                  }
              }
              tele[c].clear();
          }
      }
      
      cout << "-1\n";
      return 0;

    }

相关推荐
计算机安禾2 小时前
【数据结构与算法】第5篇:线性表(一):顺序表(ArrayList)的实现与应用
c语言·开发语言·数据结构·c++·算法·visual studio code·visual studio
仰泳的熊猫2 小时前
题目2584:蓝桥杯2020年第十一届省赛真题-数字三角形
数据结构·c++·算法·蓝桥杯
2401_864959282 小时前
C++与Python混合编程实战
开发语言·c++·算法
鄭郑2 小时前
Figma学习笔记--02
笔记·学习·figma
左左右右左右摇晃2 小时前
Java并发——锁的状态演变
java·开发语言·笔记
2501_945424802 小时前
C++与硬件交互编程
开发语言·c++·算法
2301_818419012 小时前
C++中的表达式模板
开发语言·c++·算法
Roselind_Yi2 小时前
排查Visual C++堆损坏(HEAP CORRUPTION)错误:从报错到解决的完整复盘
java·开发语言·c++·spring·bug·学习方法·远程工作
_日拱一卒2 小时前
LeetCode:三数之和
算法·leetcode·排序算法