847. Shortest Path Visiting All Nodes
Problem Description
You are given an undirected, connected graph with n
nodes labeled from 0
to n - 1
. The graph is represented as an adjacency list where graph[i]
contains a list of all nodes that are directly connected to node i
by an edge.
Your task is to find the shortest path length that visits every node in the graph. The path must visit all nodes at least once, but it can:
- Start and end at any node (they don't have to be the same)
- Revisit nodes multiple times if needed
- Reuse edges multiple times
For example, if you have 4 nodes with connections forming a graph, you need to find the minimum number of edges to traverse such that you visit all 4 nodes at least once. The path doesn't need to be a simple path - you can backtrack and revisit nodes as necessary to reach all nodes with the minimum total distance.
The solution uses BFS with state tracking, where each state is represented by (current_node, visited_nodes_bitmask)
. The bitmask keeps track of which nodes have been visited so far, with the i-th
bit set to 1 if node i
has been visited. The algorithm starts from all nodes simultaneously (multi-source BFS) and explores all possible paths until a state is reached where all nodes have been visited (when the bitmask equals (1 << n) - 1
, meaning all bits are set to 1).
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem explicitly states we have an undirected, connected graph with nodes and edges represented as an adjacency list.
Is it a tree?
- No: The graph is not necessarily a tree - it can have cycles and multiple paths between nodes.
Is the problem related to directed acyclic graphs?
- No: The graph is undirected and may contain cycles.
Is the problem related to shortest paths?
- Yes: We need to find the shortest path length that visits every node in the graph.
Is the graph Weighted?
- No: The edges don't have weights - each edge traversal counts as 1 unit of distance.
BFS
- Yes: Since we're looking for the shortest unweighted path, BFS is the appropriate algorithm.
Conclusion: The flowchart correctly leads us to use BFS (Breadth-First Search) for this problem. The key insight is that this is a shortest path problem on an unweighted graph, which makes BFS the optimal choice. The solution implements a multi-source BFS with state tracking, where each state includes both the current node and a bitmask representing which nodes have been visited, allowing us to find the minimum number of edges needed to visit all nodes.
Intuition
The key challenge in this problem is that we need to visit all nodes while finding the shortest path. This isn't a standard shortest path problem because:
- We're not going from point A to point B - we need to visit every node
- We can revisit nodes and edges multiple times
- We can start from any node
The first insight is that we need to track two pieces of information as we explore: which node we're currently at AND which nodes we've visited so far. A path that reaches node 3 having visited nodes {0, 1, 2} is different from reaching node 3 having only visited {0, 3}.
Since we need the shortest path and the graph is unweighted (each edge has cost 1), BFS is the natural choice. But how do we handle the "visit all nodes" requirement?
This is where bitmask comes in. With n
nodes, we can use an n-bit integer where bit i
represents whether node i
has been visited. For example, with 4 nodes:
0001
means only node 0 visited0101
means nodes 0 and 2 visited1111
means all 4 nodes visited (our goal!)
The state space becomes (current_node, visited_bitmask)
. We use BFS to explore all possible states, and when we reach any state where the bitmask equals (1 << n) - 1
(all bits set), we've found the shortest path that visits all nodes.
Why start from all nodes simultaneously? Since we can start from any node, instead of running BFS n
times (once from each starting node), we initialize the queue with all possible starting states: (i, 1 << i)
for each node i
. This multi-source BFS efficiently finds the optimal solution regardless of which starting node leads to the shortest path.
The vis
set prevents revisiting the same state (node, [bitmask](/problems/bitmask_intro))
, ensuring we don't get stuck in infinite loops while allowing us to revisit nodes with different visit patterns.
Learn more about Breadth-First Search, Graph, Dynamic Programming and Bitmask patterns.
Solution Approach
The solution implements a multi-source BFS with state tracking using a bitmask. Let's walk through the implementation step by step:
1. Initialization:
n = len([graph](/problems/graph_intro))
q = deque()
vis = set()
n
stores the number of nodesq
is a deque for BFS traversalvis
is a set to track visited states (not just nodes, but(node, [bitmask](/problems/bitmask_intro))
pairs)
2. Multi-source BFS Setup:
for i in range(n):
q.append((i, 1 << i))
vis.add((i, 1 << i))
- For each node
i
, we add the initial state(i, 1 << i)
to the queue 1 << i
creates a bitmask with only the i-th bit set (e.g., node 2 →0100
)- This allows us to start from any node simultaneously
3. BFS Level-by-Level Traversal:
ans = 0
while 1:
for _ in range(len(q)):
i, st = q.popleft()
ans
tracks the current distance (number of edges traversed)- The inner loop processes all states at the current distance level
- Each state consists of current node
i
and visited bitmaskst
4. Goal Check:
if st == (1 << n) - 1: return ans
(1 << n) - 1
creates a bitmask with all n bits set (e.g., for n=4:1111
)- If our current state's bitmask equals this, we've visited all nodes
- Return the current distance as the answer
5. State Expansion:
for j in [graph](/problems/graph_intro)[i]: nst = st | 1 << j if (j, nst) not in vis: vis.add((j, nst)) q.append((j, nst))
- For each neighbor
j
of current nodei
- Create new bitmask
nst
by setting the j-th bit:st | (1 << j)
- If this new state
(j, nst)
hasn't been visited, add it to the queue - This ensures we explore all possible paths while avoiding redundant states
6. Distance Increment:
ans += 1
- After processing all states at the current level, increment the distance
The algorithm guarantees finding the shortest path because:
- BFS explores states level by level (shortest distances first)
- The first time we reach a state with all nodes visited is guaranteed to be via the shortest path
- The state tracking prevents infinite loops while allowing node revisits with different visit patterns
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with 4 nodes to illustrate the solution approach.
Graph:
Node 0 connects to: [1, 2] Node 1 connects to: [0, 2, 3] Node 2 connects to: [0, 1] Node 3 connects to: [1]
Visualized:
0 --- 1 --- 3 \ / \ / 2
Step 1: Initialization
- Create queue with all starting states:
- State (0, 0001) - at node 0, visited {0}
- State (1, 0010) - at node 1, visited {1}
- State (2, 0100) - at node 2, visited {2}
- State (3, 1000) - at node 3, visited {3}
- Distance = 0
Step 2: Process Level 0 (Distance = 0)
- Process (0, 0001): Not all nodes visited (0001 ≠ 1111)
- Explore neighbors: Add (1, 0011) and (2, 0101) to queue
- Process (1, 0010): Not all nodes visited
- Add (0, 0011), (2, 0110), (3, 1010) to queue
- Process (2, 0100): Not all nodes visited
- Add (0, 0101), (1, 0110) to queue
- Process (3, 1000): Not all nodes visited
- Add (1, 1010) to queue
- Distance = 1
Step 3: Process Level 1 (Distance = 1) Process states with distance 1. Key states include:
- (1, 0011) - at node 1, visited {0,1}
- (2, 0101) - at node 2, visited {0,2}
- (3, 1010) - at node 3, visited {1,3}
Each explores neighbors and updates bitmasks. For example:
- From (1, 0011): Can go to node 3 → (3, 1011) with visited {0,1,3}
- From (3, 1010): Can go to node 1 → already processed
Distance = 2
Step 4: Process Level 2 (Distance = 2) Continue processing. Notable state:
- (3, 1011) - at node 3, visited {0,1,3}
- Can go to node 1 → (1, 1011)
- (2, 0111) - at node 2, visited {0,1,2}
- Neighbors don't complete all nodes yet
Distance = 3
Step 5: Process Level 3 (Distance = 3)
- Process (1, 1011) - at node 1, visited {0,1,3}
- Can go to node 2 → (2, 1111) with visited {0,1,2,3} ✓
- When we reach state (2, 1111), bitmask = 1111 = (1<<4)-1
- All nodes visited! Return distance = 3
The shortest path that visits all nodes has length 3. One such path is: 0→1→3→1→2 (starting at node 0, ending at node 2).
Consider a graph with 4 nodes where: - Node 0 connects to [1, 2] - Node 1 connects to [0, 2, 3] - Node 2 connects to [0, 1] - Node 3 connects to [1] **Initial Setup (Distance = 0):** Queue contains all starting states: - (0, 0001): at node 0, visited only node 0 - (1, 0010): at node 1, visited only node 1 - (2, 0100): at node 2, visited only node 2 - (3, 1000): at node 3, visited only node 3 **Level 0 Processing:** From (0, 0001), we can move to: - Node 1: creates state (1, 0011) - visited {0,1} - Node 2: creates state (2, 0101) - visited {0,2} From (3, 1000), we can move to: - Node 1: creates state (1, 1010) - visited {1,3} None have visited all nodes yet (need bitmask 1111). **Level 1 Processing (Distance = 1):** From (1, 0011), we can move to: - Node 3: creates state (3, 1011) - visited {0,1,3} From (2, 0101), we can move to: - Node 1: creates state (1, 0111) - visited {0,1,2} Still haven't visited all nodes. **Level 2 Processing (Distance = 2):** From (3, 1011), we go to node 1: state (1, 1011) From (1, 0111), we go to node 3: state (3, 1111) ✓ **Result:** When we reach state (3, 1111) at distance 2, we've visited all nodes! But wait - let's verify this path exists: - Start at node 0 (bitmask: 0001) - Move to node 1 (bitmask: 0011) - distance 1 - Move to node 3 (bitmask: 1011) - distance 2 - Move to node 1 (bitmask: 1011) - distance 3 - Move to node 2 (bitmask: 1111) - distance 4 Actually, the first state reaching 1111 occurs at distance 3, giving us the minimum path length of 3 that visits all nodes.
Solution Implementation
1class Solution:
2 def shortestPathLength(self, graph: List[List[int]]) -> int:
3 # Get the number of nodes in the graph
4 num_nodes = len(graph)
5
6 # Initialize BFS queue with all possible starting nodes
7 # Each entry is (current_node, visited_state_bitmask)
8 queue = deque()
9 visited_states = set()
10
11 # Start BFS from every node as a potential starting point
12 for node in range(num_nodes):
13 initial_state = 1 << node # Set bit for this node as visited
14 queue.append((node, initial_state))
15 visited_states.add((node, initial_state))
16
17 # Track the number of steps taken
18 steps = 0
19
20 # BFS to find shortest path visiting all nodes
21 while True:
22 # Process all nodes at current distance level
23 for _ in range(len(queue)):
24 current_node, visited_mask = queue.popleft()
25
26 # Check if all nodes have been visited
27 # (all bits set means visited_mask equals 2^n - 1)
28 if visited_mask == (1 << num_nodes) - 1:
29 return steps
30
31 # Explore all neighbors of current node
32 for neighbor in graph[current_node]:
33 # Update visited state by setting neighbor's bit
34 new_visited_mask = visited_mask | (1 << neighbor)
35
36 # Only process this state if we haven't seen it before
37 if (neighbor, new_visited_mask) not in visited_states:
38 visited_states.add((neighbor, new_visited_mask))
39 queue.append((neighbor, new_visited_mask))
40
41 # Increment step counter after processing current level
42 steps += 1
43
1class Solution {
2 public int shortestPathLength(int[][] graph) {
3 int nodeCount = graph.length;
4
5 // Queue for BFS: each element is [currentNode, visitedNodesBitmask]
6 Deque<int[]> queue = new ArrayDeque<>();
7
8 // Track visited states: visited[node][bitmask] = true if we've been at 'node' with 'bitmask' visited
9 boolean[][] visited = new boolean[nodeCount][1 << nodeCount];
10
11 // Initialize BFS from all nodes as potential starting points
12 for (int node = 0; node < nodeCount; ++node) {
13 queue.offer(new int[] {node, 1 << node}); // Start at node with only itself visited
14 visited[node][1 << node] = true;
15 }
16
17 // BFS to find shortest path that visits all nodes
18 for (int pathLength = 0;; ++pathLength) {
19 // Process all states at current distance level
20 for (int levelSize = queue.size(); levelSize > 0; --levelSize) {
21 int[] current = queue.poll();
22 int currentNode = current[0];
23 int visitedMask = current[1];
24
25 // Check if all nodes have been visited (all bits set in mask)
26 if (visitedMask == (1 << nodeCount) - 1) {
27 return pathLength;
28 }
29
30 // Explore neighbors
31 for (int neighbor : graph[currentNode]) {
32 // Update bitmask to include the neighbor
33 int newVisitedMask = visitedMask | (1 << neighbor);
34
35 // Only proceed if this state hasn't been visited
36 if (!visited[neighbor][newVisitedMask]) {
37 visited[neighbor][newVisitedMask] = true;
38 queue.offer(new int[] {neighbor, newVisitedMask});
39 }
40 }
41 }
42 }
43 }
44}
45
1class Solution {
2public:
3 int shortestPathLength(vector<vector<int>>& graph) {
4 int nodeCount = graph.size();
5
6 // Queue stores pairs of (current node, visited state bitmask)
7 queue<pair<int, int>> bfsQueue;
8
9 // Track visited states: visited[node][bitmask] = true if we've been at 'node' with 'bitmask' state
10 bool visited[nodeCount][1 << nodeCount];
11 memset(visited, false, sizeof(visited));
12
13 // Initialize BFS from all nodes as potential starting points
14 for (int startNode = 0; startNode < nodeCount; ++startNode) {
15 int initialMask = 1 << startNode; // Mark only the starting node as visited
16 bfsQueue.emplace(startNode, initialMask);
17 visited[startNode][initialMask] = true;
18 }
19
20 // BFS to find shortest path visiting all nodes
21 for (int pathLength = 0;; ++pathLength) {
22 // Process all nodes at current distance level
23 int currentLevelSize = bfsQueue.size();
24 for (int i = 0; i < currentLevelSize; ++i) {
25 // Get current node and its visited state
26 auto [currentNode, visitedMask] = bfsQueue.front();
27 bfsQueue.pop();
28
29 // Check if all nodes have been visited (all bits set to 1)
30 if (visitedMask == (1 << nodeCount) - 1) {
31 return pathLength;
32 }
33
34 // Explore all neighbors
35 for (int neighbor : graph[currentNode]) {
36 // Update visited state by marking the neighbor as visited
37 int newMask = visitedMask | (1 << neighbor);
38
39 // Only process this state if we haven't seen it before
40 if (!visited[neighbor][newMask]) {
41 visited[neighbor][newMask] = true;
42 bfsQueue.emplace(neighbor, newMask);
43 }
44 }
45 }
46 }
47 }
48};
49
1/**
2 * Finds the shortest path that visits every node in the graph at least once.
3 * Uses BFS with state compression to track visited nodes.
4 *
5 * @param graph - Adjacency list representation where graph[i] contains neighbors of node i
6 * @returns The length of the shortest path visiting all nodes
7 */
8function shortestPathLength(graph: number[][]): number {
9 const nodeCount: number = graph.length;
10
11 // Queue for BFS: each element is [currentNode, visitedStatesMask]
12 const queue: number[][] = [];
13
14 // Track visited states: visited[node][stateMask] = true if we've been at 'node' with 'stateMask' visited
15 const visited: boolean[][] = Array(nodeCount)
16 .fill(false)
17 .map(() => Array(1 << nodeCount).fill(false));
18
19 // Initialize BFS from all nodes as potential starting points
20 for (let node = 0; node < nodeCount; ++node) {
21 const initialState: number = 1 << node; // Mark only current node as visited
22 queue.push([node, initialState]);
23 visited[node][initialState] = true;
24 }
25
26 // BFS to find shortest path
27 let pathLength: number = 0;
28
29 while (true) {
30 // Process all nodes at current distance level
31 const currentLevelSize: number = queue.length;
32
33 for (let i = 0; i < currentLevelSize; ++i) {
34 const [currentNode, visitedMask] = queue.shift()!;
35
36 // Check if all nodes have been visited (all bits set in mask)
37 const allNodesVisited: number = (1 << nodeCount) - 1;
38 if (visitedMask === allNodesVisited) {
39 return pathLength;
40 }
41
42 // Explore neighbors
43 for (const neighbor of graph[currentNode]) {
44 // Update state mask by setting the bit for the neighbor node
45 const newStateMask: number = visitedMask | (1 << neighbor);
46
47 // Only process this state if we haven't visited it before
48 if (!visited[neighbor][newStateMask]) {
49 visited[neighbor][newStateMask] = true;
50 queue.push([neighbor, newStateMask]);
51 }
52 }
53 }
54
55 // Move to next distance level
56 pathLength++;
57 }
58}
59
Time and Space Complexity
Time Complexity: O(n * 2^n)
The algorithm uses BFS to explore all possible states. Each state is represented by a tuple (current_node, visited_mask)
where:
current_node
can be any of then
nodesvisited_mask
is a bitmask representing which nodes have been visited, with2^n
possible combinations
Since we track visited states in a set to avoid revisiting them, each unique state (node, mask)
is processed at most once. There are n * 2^n
possible states total.
For each state, we iterate through all neighbors of the current node. In the worst case (complete graph), this could be O(n)
neighbors. However, the total number of edges processed across all BFS iterations is bounded by the number of state transitions, which is at most O(n * 2^n)
since each state can transition to at most n
other states.
Therefore, the overall time complexity is O(n * 2^n)
.
Space Complexity: O(n * 2^n)
The space is dominated by:
- The
vis
set which stores all visited states(node, mask)
. In the worst case, this stores alln * 2^n
possible states. - The queue
q
which in the worst case could containO(n * 2^n)
states, though typically it would be much smaller.
Therefore, the space complexity is O(n * 2^n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Confusing State with Just Node Tracking
A common mistake is trying to track only visited nodes instead of the combination of (node, visited_state). This leads to incorrect solutions because the same node might need to be visited multiple times with different sets of previously visited nodes.
Incorrect approach:
visited = set() # Only tracking nodes
for neighbor in graph[current]:
if neighbor not in visited: # Wrong! This prevents revisiting
visited.add(neighbor)
queue.append(neighbor)
Correct approach:
visited_states = set() # Track (node, bitmask) pairs
for neighbor in graph[current_node]:
new_state = (neighbor, new_mask)
if new_state not in visited_states: # Allows revisiting nodes with different states
visited_states.add(new_state)
queue.append(new_state)
2. Incorrect Bitmask Operations
Many developers make errors when working with bitmasks, especially confusing bit positions with node values.
Common mistakes:
- Using
1 << (i+1)
instead of1 << i
(off-by-one error) - Using
visited_mask + (1 << neighbor)
instead ofvisited_mask | (1 << neighbor)
for setting bits - Checking completion with
visited_mask == n
instead ofvisited_mask == (1 << n) - 1
Example of correct bitmask usage:
# Setting bit for node i initial_mask = 1 << i # NOT: 1 << (i+1) # Adding a new visited node new_mask = current_mask | (1 << neighbor) # NOT: current_mask + (1 << neighbor) # Checking if all n nodes visited if visited_mask == (1 << n) - 1: # NOT: if visited_mask == n return steps
3. Starting from Only One Node
Another pitfall is implementing single-source BFS instead of multi-source BFS, which can miss the optimal solution where the shortest path doesn't start from node 0.
Incorrect single-source:
queue = deque([(0, 1 << 0)]) # Only starting from node 0 visited_states.add((0, 1 << 0))
Correct multi-source:
for node in range(num_nodes):
initial_state = 1 << node
queue.append((node, initial_state))
visited_states.add((node, initial_state))
4. Infinite Loop Due to Missing Return Statement
The solution uses while True:
which requires careful handling to avoid infinite loops. Forgetting to return when the goal is reached or having the goal check in the wrong place can cause the program to run indefinitely.
Problematic structure:
while queue: # This might terminate before finding solution # ... BFS logic
Better structure with explicit termination:
while True:
for _ in range(len(queue)):
current_node, visited_mask = queue.popleft()
if visited_mask == (1 << num_nodes) - 1:
return steps # Essential: must return here
# ... rest of logic
steps += 1
if not queue: # Safety check
break
5. Memory Optimization Overlooked
For larger graphs, the visited_states set can grow exponentially. While the given solution is correct, not considering memory constraints can lead to Memory Limit Exceeded errors.
Potential optimization using dictionary for early termination:
# Track minimum steps to reach each state
min_steps = {}
for node in range(num_nodes):
state = (node, 1 << node)
min_steps[state] = 0
queue.append((node, 1 << node, 0))
while queue:
node, mask, steps = queue.popleft()
if steps > min_steps.get((node, mask), float('inf')):
continue # Skip if we've seen this state with fewer steps
What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)
Recommended Readings
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Don’t Miss This!