1. 问题背景与核心挑战
LeetCode 864题"获取所有钥匙的最短路径"是一个典型的图论与状态压缩结合的算法问题。给定一个二维网格,其中包含:
- 起点 '@'
- 墙壁 '#'
- 空地 '.'
- 小写字母表示钥匙(a-f)
- 大写字母表示对应的锁(A-F)
玩家需要收集所有钥匙(每个字母钥匙只能开对应字母的锁),求从起点出发收集全部钥匙的最短路径步数。这个问题在现实中有诸多应用场景,比如:
- 游戏中的关卡设计(如解谜游戏中的钥匙门机制)
- 物流仓储中的权限区域访问
- 网络安全中的多级认证路径优化
关键难点在于:路径搜索过程中需要动态记录已获取的钥匙状态,传统的BFS无法直接处理这种带有状态变化的路径搜索。
2. 算法选择与思路解析
2.1 为什么选择BFS+状态压缩
常规BFS适用于无权图的最短路径查找,但本题的特别之处在于:
- 路径有效性取决于钥匙获取状态
- 同一位置在不同钥匙状态下应被视为不同节点
状态压缩使用位运算来表示钥匙获取情况(Java中int类型足够表示a-f六把钥匙):
- 每位代表一把钥匙(a=1<<0, b=1<<1,...)
- 按位或操作记录新钥匙
- 按位与操作检查是否有对应钥匙
2.2 三维状态表示法
我们需要扩展传统的(x,y)坐标到(x,y,keys)三维状态:
- keys的二进制表示当前持有的钥匙
- 例如:keys=0b000101表示持有a和c钥匙
- 目标状态是持有所有钥匙(对于k把钥匙是(1<<k)-1)
3. Java实现详解
3.1 数据结构设计
class State { int x, y; int keys; State(int x, int y, int keys) { this.x = x; this.y = y; this.keys = keys; } // 重写equals和hashCode用于HashSet @Override public boolean equals(Object o) {...} @Override public int hashCode() {...} }3.2 BFS核心框架
public int shortestPathAllKeys(String[] grid) { int m = grid.length, n = grid[0].length(); int allKeys = 0; Queue<State> queue = new LinkedList<>(); Set<State> visited = new HashSet<>(); // 初始化:找到起点和所有钥匙 for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { char c = grid[i].charAt(j); if (c == '@') { queue.offer(new State(i, j, 0)); visited.add(new State(i, j, 0)); } else if (c >= 'a' && c <= 'f') { allKeys |= (1 << (c - 'a')); } } } int[][] dirs = {{0,1},{1,0},{0,-1},{-1,0}}; int steps = 0; while (!queue.isEmpty()) { int size = queue.size(); for (int i = 0; i < size; i++) { State curr = queue.poll(); if (curr.keys == allKeys) return steps; for (int[] dir : dirs) { int x = curr.x + dir[0]; int y = curr.y + dir[1]; int keys = curr.keys; if (x < 0 || x >= m || y < 0 || y >= n) continue; char c = grid[x].charAt(y); if (c == '#') continue; // 墙 // 遇到锁且没有对应钥匙 if (c >= 'A' && c <= 'F' && (keys & (1 << (c - 'A'))) == 0) continue; // 遇到钥匙则更新状态 if (c >= 'a' && c <= 'f') keys |= (1 << (c - 'a')); State newState = new State(x, y, keys); if (!visited.contains(newState)) { visited.add(newState); queue.offer(newState); } } } steps++; } return -1; }4. 关键优化与注意事项
4.1 状态判重优化
常规BFS使用二维坐标判重,但本题需要三维判重(x,y,keys)。实测发现:
- 使用HashSet 存储已访问状态
- 必须正确实现State类的equals和hashCode方法
- 错误示例:仅比较x,y会导致错误剪枝
4.2 方向数组技巧
使用dirs数组表示四个方向比写四个if更简洁:
int[][] dirs = {{0,1},{1,0},{0,-1},{-1,0}}; // 右,下,左,上4.3 钥匙数量计算
可以在初始化时统计钥匙数量:
int keyCount = 0; for (String row : grid) { for (char c : row.toCharArray()) { if (c >= 'a' && c <= 'f') keyCount++; } } allKeys = (1 << keyCount) - 1;5. 复杂度分析与边界情况
5.1 时间复杂度
设网格大小为M×N,钥匙数量为K:
- 状态总数:M×N×2^K
- 每个状态处理O(1)(四个方向)
- 总复杂度:O(M×N×2^K)
5.2 空间复杂度
主要消耗在visited集合:
- O(M×N×2^K)
5.3 特殊测试用例
- 无钥匙情况:应返回0
- 钥匙被墙包围:返回-1
- 需要绕路获取钥匙顺序的情况
- 最大网格尺寸(30x30)和最多钥匙(6把)的性能测试
6. 实际应用扩展
这种BFS+状态压缩的技术还可用于:
- 多目标点最短路径问题(如同时收集多个物品)
- 动态障碍物场景(如随时间变化的迷宫)
- 多条件解锁的路径规划(如需要特定道具组合)
在游戏AI中,类似的算法可用于:
- NPC的寻路决策
- 自动解谜系统
- 关卡难度测试
7. 常见错误与调试技巧
7.1 典型错误模式
- 忘记处理锁的检查条件:
// 错误:漏掉钥匙检查 if (c >= 'A' && c <= 'F') continue;- 钥匙状态更新错误:
// 错误:直接修改curr.keys会影响其他方向 curr.keys |= (1 << (c - 'a'));- 状态判重不完整:
// 错误:仅用坐标判重 visited.add(x + "," + y);7.2 调试建议
- 打印关键状态:
System.out.println(x+","+y+" keys:"+Integer.toBinaryString(keys));- 可视化小规模测试用例:
@a.A ... B.b- 使用单元测试覆盖:
- 无钥匙情况
- 不可达情况
- 需要特定顺序的情况
8. 算法变种与进阶
8.1 多玩家协作版本
假设可以有多人同时移动,求最短时间。这需要:
- 状态扩展为(x1,y1,x2,y2,keys)
- 协同移动策略
8.2 带权版本
如果不同格子有不同的移动代价(如沼泽减速),可以改用Dijkstra算法。
8.3 动态障碍物
如果障碍物会随时间变化,状态需要增加时间维度。
9. 性能优化实战
当网格较大(30x30)且钥匙较多(6把)时:
- 使用位运算优化状态处理
- 双向BFS搜索
- 启发式搜索(A*):
// 估算剩余步数=曼哈顿距离到最远钥匙 PriorityQueue<State> pq = new PriorityQueue<>(Comparator.comparingInt(s -> s.steps + heuristic(s)));10. 工程实践建议
- 将网格解析与BFS逻辑分离
- 使用常量定义方向数组
- 添加详细的注释说明状态表示
- 编写完备的单元测试
- 对于游戏开发实际应用,可以考虑:
- 预处理可通行区域
- 分层路径规划
- 结合导航网格(NavMesh)技术