LeetCode 847. 访问所有节点的最短路径
题目描述


题意分析
在无向连通图中寻找一条访问全部节点的最短行走路线,长度按经过的边数计算。起点和终点可以任选,节点、边都允许重复经过。题目最多有 12 个节点,可以用一个整数的二进制位记录已经访问过哪些节点。
解法:状态 BFS
核心思路
[!blue]
只知道当前位置还不够:到达同一节点时,已经访问的节点集合不同,剩余任务也不同。因此状态定义为
(node, mask),mask的第i位为 1 表示节点i已访问;dist[node][mask]记录到达这个状态的最少边数,-1 表示尚未发现。从
node沿边走到邻居nei后,新状态为(nei, mask | (1 << nei)),距离增加 1。即使nei已经出现在集合里,也允许走过去,因为可能需要借它通往尚未访问的节点。判重必须针对整个二元状态;若同一状态再次出现,当前位置和剩余任务都相同,更长的历史路线没有保留价值。每条转移都只经过一条边,可以在这个状态图上做 BFS。起点任选,所以同时将所有
(i, 1 << i)以距离 0 入队;这等价于一起搜索所有起点,避免先固定某个起点而错过更短路线。全部节点已访问时,掩码为
(1 << n) - 1。BFS 按距离从小到大处理状态,第一次取出这个掩码对应的状态,其距离就是所有起终点选择中的最优值。入队时立即填写距离,保证每个状态只进入队列一次。
解题步骤
- 将距离表初始化为 -1,计算全集掩码
full。- 每个节点各自作为起点,初始掩码只包含自身,距离设为 0 并入队。
- 取出状态后先检查
mask == full,满足则返回当前距离。只有一个节点时,初始状态就满足条件,答案为 0。- 遍历当前节点的邻居,更新访问集合;若新状态未发现,就记录距离加 1 并入队。题目保证图连通,因此一定能找到覆盖全部节点的路线。
代码实现
class Solution {
public int shortestPathLength(int[][] graph) {
int n = graph.length;
int full = (1 << n) - 1;
int[][] dist = new int[n][1 << n];
Queue<int[]> queue = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
for (int mask = 0; mask < (1 << n); mask++) {
dist[i][mask] = -1;
}
}
for (int i = 0; i < n; i++) {
// 每个节点都可作为零步起点,掩码先包含自身
int mask = 1 << i;
dist[i][mask] = 0;
queue.offer(new int[] {
i,
mask
});
}
while (!queue.isEmpty()) {
int[] cur = queue.poll();
int node = cur[0];
int mask = cur[1];
if (mask == full) {
return dist[node][mask];
}
for (int nei : graph[node]) {
// 允许重访物理节点,但节点与访问集合共同判重
int nextMask = mask | (1 << nei);
if (dist[nei][nextMask] == -1) {
dist[nei][nextMask] = dist[node][mask] + 1;
queue.offer(new int[] {
nei,
nextMask
});
}
}
}
return -1;
}
}
func shortestPathLength(graph [][]int) int {
n := len(graph)
full := (1 << n) - 1
dist := make([][]int, n)
for i := 0; i < n; i++ {
dist[i] = make([]int, 1<<n)
for j := 0; j < (1 << n); j++ {
dist[i][j] = -1
}
}
queue := make([][2]int, 0)
for i := 0; i < n; i++ {
// 每个节点都可作为零步起点,掩码先包含自身
mask := 1 << i
dist[i][mask] = 0
queue = append(queue, [2]int{
i,
mask,
})
}
for head := 0; head < len(queue); head++ {
node := queue[head][0]
mask := queue[head][1]
if mask == full {
return dist[node][mask]
}
for _, nei := range graph[node] {
// 允许重访物理节点,但节点与访问集合共同判重
nextMask := mask | (1 << nei)
if dist[nei][nextMask] == -1 {
dist[nei][nextMask] = dist[node][mask] + 1
queue = append(queue, [2]int{
nei,
nextMask,
})
}
}
}
return -1
}
复杂度分析
- 时间复杂度:$O((n+E)2^n)$,其中 $E$ 为原图边数。初始化 $n2^n$ 个距离项,每种访问集合下至多扫描全部邻接边。
- 空间复杂度:$O(n2^n)$,距离表与队列最多保存这一数量级的状态。
关键点总结
[!green]
- 同一节点带不同掩码是不同状态,不能一概去重。
- 起点任选通过多源同层初始化表达。
易错点总结
[!yellow]
- 只从零出发可能错过更优起点。
- 初始掩码为零,会漏记已站上的节点。
- 全集写成二的 n 次方,而不是减一,永远无法由合法节点位组成。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 864. 获取所有钥匙的最短路径 | 困难 | 同样把当前位置与已完成集合组成状态,原题记录钥匙集合,本题记录已访问节点集合。 |
| 943. 最短超级串 | 困难 | 同样全覆盖并保留最后位置,原题利用字符串重叠定义转移代价,本题图上每步代价为1。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!