✅博客主页:爆打维c-CSDN博客 🐾
一、分析题目
原题力扣链接:https://leetcode.cn/problems/grid-teleportation-traversal/description/
这是一道典型的网格图上找最短路径的问题
- 传送门权重为0(题目说明"通过传送门不计入移动次数")
- 普通单元格权重为1
当图中的边权只有 0 和 1 时,最适合的算法是 0-1 BFS(双端队列广度优先搜索)。使用 Dijkstra 也可以,但 0-1 BFS 的时间复杂度更优,为 O(M×N)。
解题思路
1.预处理传送门位置:遍历整个网格,使用一个哈希表(或数组)记录每种字符('A'-'Z')出现的所有坐标位置。
groups['A'] = [(r1, c1), (r2, c2), ...]
2.0-1 BFS 状态定义:队列中存储元组 (row, col, cost)。row, col: 当前坐标;cost: 到达该点的最少移动次数。
3.核心逻辑:
使用双端队列 deque:
当我们从当前点 (r, c)走向相邻格子(上下左右)时,代价+1,将新状态加入队列尾部。
当我们从当前点 (r, c)使用传送门传送到同字母的其他格子时,代价+0,将新状态加入队列 头部(因为花费没有增加,应该优先处理)。
4.防止死循环与优化:
普通去重:
- dist 数组:既充当了记录最短距离的作用,也充当了
visited的作用。如果我们发现到达某个点的new_cost大于等于已记录的dist[r][c],就不需要再次入队。传送门去重:
- IsUsed 数组:题目规定"每个字母对应的传送门在旅程中最多只能使用一次"。这不仅是规则,也是优化的关键。当我们第一次到达某个字符(比如 'A')并触发了传送,我们将所有其他的 'A' 都加入了队列。如果我们再次遇到 'A',没有必要再传送一次(因为之前的路径肯定更短或相等)。因此,我们需要一个数组来记录已经触发过的字母。
二、算法原理
01BFS(双端队列广度优先搜索): 维护一个双端队列 (std::deque)。
- 如果边的权重是 0 (传送),我们将新节点加入队列的 头部 (Front) 。
- 这意味着我们不需要花费额外代价就能到达那里,应该立即处理它。
- 如果边的权重是 1 (移动),我们将新节点加入队列的 尾部 (Back) 。
- 这保证了队列中的节点始终按照"当前最小代价"排序。
通常我们用 BFS 处理边权全为 1 的图,用 Dijkstra 处理边权非负的图。但在边权只有 0 和 1 的情况下,Dijkstra 的O(ElogV) 有点慢,而 0-1 BFS 可以在 O(V+E) 的线性时间内解决问题。
01BFS例题出入队列过程
三、实现代码
C++版
cpp
#include <vector>
#include <string>
#include <deque>
#include <tuple>
#include <climits> // for INT_MAX
using namespace std;
class Solution {
public:
int minMoves(vector<string>& matrix) {
int m = matrix.size();
int n = matrix[0].size();
// 1. 预处理:记录每种字母出现的所有位置
// group[0] 存储 'A' 的坐标,group[1] 存储 'B' 的坐标...
vector<pair<int, int>> group[26];
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (isupper(matrix[i][j])) {
group[matrix[i][j] - 'A'].push_back({i, j});
}
}
}
// 2. 初始化数据结构
// dist[i][j] 记录到达 (i, j) 的最小步数,初始化为无穷大
vector<vector<int>> dist(m, vector<int>(n, INT_MAX));
// 双端队列,存储坐标 {row, col}
deque<pair<int, int>> dq;
// 起点初始化
dist[0][0] = 0;
dq.push_front({0, 0});
// 记录某个字母的传送门是否已经被使用(展开)过,避免重复计算
vector<bool> portalUsed(26, false);
// 方向数组:上、下、左、右
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
while (!dq.empty()) {
auto [r, c] = dq.front();
dq.pop_front();
// 当前花费
int current_cost = dist[r][c];
// 到达终点
if (r == m - 1 && c == n - 1) {
return current_cost;
}
char cell = matrix[r][c];
// --- 逻辑 A: 尝试使用传送门 (权重为 0) ---
if (isupper(cell)) {
int pIdx = cell - 'A';
// 如果这个字母的传送门还没被使用过
if (!portalUsed[pIdx]) {
portalUsed[pIdx] = true; // 标记已使用,防止后续重复入队
for (auto& next_pos : group[pIdx]) {
int nr = next_pos.first;
int nc = next_pos.second;
// 松弛操作:如果找到了更短的路径
if (dist[nr][nc] > current_cost) {
dist[nr][nc] = current_cost; // 传送不增加步数
dq.push_front({nr, nc}); // 权重为0,加到队首
}
}
}
}
// --- 逻辑 B: 尝试普通移动 (权重为 1) ---
for (int i = 0; i < 4; ++i) {
int nr = r + dx[i];
int nc = c + dy[i];
// 检查边界和障碍物
if (nr >= 0 && nr < m && nc >= 0 && nc < n && matrix[nr][nc] != '#') {
// 松弛操作
if (dist[nr][nc] > current_cost + 1) {
dist[nr][nc] = current_cost + 1; // 移动增加1步
dq.push_back({nr, nc}); // 权重为1,加到队尾
}
}
}
}
return -1; // 无法到达
}
};
Python版
python
from collections import deque, defaultdict
class Solution:
def minMoves(self, matrix: list[str]) -> int:
m, n = len(matrix), len(matrix[0])
# 1. 预处理:记录所有传送门的位置
portals = defaultdict(list)
for r in range(m):
for c in range(n):
char = matrix[r][c]
if 'A' <= char <= 'Z':
portals[char].append((r, c))
# 2. 初始化 0-1 BFS
# 队列存储: (r, c, cost)
q = deque([(0, 0, 0)])
# 记录已访问的坐标,避免重复处理
visited = [[False] * n for _ in range(m)]
visited[0][0] = True
# 记录已使用过的传送门字母,避免重复展开
used_portal_chars = set()
# 方向数组
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
while q:
r, c, cost = q.popleft()
# 到达终点
if r == m - 1 and c == n - 1:
return cost
curr_char = matrix[r][c]
# --- 逻辑 A: 尝试使用传送门 (权重为 0) ---
# 如果当前是传送门字母,且该字母未被使用过
if 'A' <= curr_char <= 'Z' and curr_char not in used_portal_chars:
used_portal_chars.add(curr_char) # 标记该字母已使用
for nr, nc in portals[curr_char]:
if not visited[nr][nc]:
visited[nr][nc] = True
# 代价为 0,加入队首
q.appendleft((nr, nc, cost))
# --- 逻辑 B: 尝试普通移动 (权重为 1) ---
for dr, dc in directions:
nr, nc = r + dr, c + dc
# 检查边界和障碍物
if 0 <= nr < m and 0 <= nc < n and matrix[nr][nc] != '#':
if not visited[nr][nc]:
visited[nr][nc] = True
# 代价为 1,加入队尾
q.append((nr, nc, cost + 1))
# 无法到达
return -1
