- 标签:位运算、广度优先搜索、图、动态规划、状态压缩
- 难度:困难
描述:存在一个由
要求:返回能够访问所有节点的最短路径长度。可以在任一节点开始和停止,也可以多次重访节点,并且可以重用边。
说明:
-
$n == graph.length$ 。 -
$1 \le n \le 12$ 。 -
$0 \le graph[i].length < n$ 。 -
$graph[i]$ 不包含$i$ 。 - 如果
$graph[a]$ 包含$b$ ,那么$graph[b]$ 也包含$a$ 。 - 输入的图总是连通图。
示例:
- 示例 1:
输入:graph = [[1,2,3],[0],[0],[0]]
输出:4
解释:一种可能的路径为 [1,0,2,0,3]
- 示例 2:
输入:graph = [[1],[0,2,4],[1,3,4],[2],[1,2]]
输出:4
解释:一种可能的路径为 [0,1,4,2,3]
题目需要求解的是「能够访问所有节点的最短路径长度」,并且每个节点都可以作为起始点。
如果对于一个特定的起点,我们可以将该起点放入队列中,然后对其进行广度优先搜索,并使用访问数组
而本题中,每个节点都可以作为起始点,则我们可以直接将所有节点放入队列中,然后对所有节点进行广度优先搜索。
因为本题中节点数目 (u, 1 << u)
。当状态
为了方便在广度优先搜索的同事,记录当前的「路径长度」以及「节点的访问情况」。我们可以使用一个三元组
-
$u$ :表示当前节点编号。 -
$state$ :一个$n$ 位的二进制数,表示$n$ 个节点的访问情况。$state$ 第$i$ 位为$0$ 时表示未访问过,$state$ 第$i$ 位为$1$ 时表示访问过。 -
$dist$ 表示当前的「路径长度」。
同时为了避免重复搜索同一个节点
整个算法步骤如下:
- 将所有节点的
(节点编号, 起始状态, 路径长度)
作为三元组存入队列,并使用集合$visited$ 记录所有节点的访问情况。 - 对所有点开始进行广度优先搜索:
- 从队列中弹出队头节点。
- 判断节点的当前状态,如果所有节点都已经访问过,则返回答案。
- 如果没有全访问过,则遍历当前节点的邻接节点。
- 将邻接节点的访问状态标记为访问过。
- 如果节点即当前路径没有访问过,则加入队列继续遍历,并标记为访问过。
- 重复进行第
$2$ 步,直到队列为空。
import collections
class Solution:
def shortestPathLength(self, graph: List[List[int]]) -> int:
size = len(graph)
queue = collections.deque([])
visited = set()
for u in range(size):
queue.append((u, 1 << u, 0)) # 将 (节点编号, 起始状态, 路径长度) 存入队列
visited.add((u, 1 << u)) # 标记所有节点的节点编号,以及当前状态
while queue: # 对所有点开始进行广度优先搜索
u, state, dist = queue.popleft() # 弹出队头节点
if state == (1 << size) - 1: # 所有节点都访问完,返回答案
return dist
for v in graph[u]: # 遍历邻接节点
next_state = state | (1 << v) # 标记邻接节点的访问状态
if (v, next_state) not in visited: # 如果节点即当前路径没有访问过,则加入队列继续遍历,并标记为访问过
queue.append((v, next_state, dist + 1))
visited.add((v, next_state))
return 0
-
时间复杂度:$O(n^2 \times 2^n)$,其中
$n$ 为图的节点数量。 - 空间复杂度:$O(n \times 2^n)$。