🧠
Bidirectional BFS with State Compression
💡 This approach further optimizes BFS by searching simultaneously from the start and end states, meeting in the middle to reduce search space.
Intuition
Run two BFS traversals: one from all nodes with their initial visited bitmask, and one from the state where all nodes are visited. When the two searches meet, the shortest path length is found.
Algorithm
- Initialize two queues and visited sets: one from all single-node starts, one from the all-visited state.
- Alternate expanding states from each queue.
- If a state appears in both visited sets, return the sum of distances from both sides.
- Continue until the two searches meet.
💡 By searching from both ends, the number of states explored is drastically reduced, improving performance on larger graphs.
from collections import deque
from typing import List
def shortestPathLength(graph: List[List[int]]) -> int:
n = len(graph)
all_visited = (1 << n) - 1
# Queue elements: (node, visited_mask)
q1 = deque()
q2 = deque()
visited1 = dict()
visited2 = dict()
for i in range(n):
mask = 1 << i
q1.append((i, mask))
visited1[(i, mask)] = 0
q2.append((i, all_visited ^ mask))
visited2[(i, all_visited ^ mask)] = 0
while q1 and q2:
# Expand from q1
for _ in range(len(q1)):
node, mask = q1.popleft()
dist = visited1[(node, mask)]
if (node, mask) in visited2:
return dist + visited2[(node, mask)]
for nei in graph[node]:
next_mask = mask | (1 << nei)
if (nei, next_mask) not in visited1:
visited1[(nei, next_mask)] = dist + 1
q1.append((nei, next_mask))
# Expand from q2
for _ in range(len(q2)):
node, mask = q2.popleft()
dist = visited2[(node, mask)]
if (node, mask) in visited1:
return dist + visited1[(node, mask)]
for nei in graph[node]:
next_mask = mask & ~(1 << nei)
if (nei, next_mask) not in visited2:
visited2[(nei, next_mask)] = dist + 1
q2.append((nei, next_mask))
return -1
# Driver code
if __name__ == '__main__':
graph = [[1,2,3],[0],[0],[0]]
print(shortestPathLength(graph)) # Output: 4
Line Notes
q1 = deque()Queue for forward BFS
q2 = deque()Queue for backward BFS
visited1 = dict()Visited states for forward BFS
visited2 = dict()Visited states for backward BFS
for i in range(n)Initialize both BFS queues and visited sets
if (node, mask) in visited2Check if forward and backward searches meet
next_mask = mask | (1 << nei)Update visited mask forward
next_mask = mask & ~(1 << nei)Update visited mask backward
visited1[(nei, next_mask)] = dist + 1Mark forward visited
visited2[(nei, next_mask)] = dist + 1Mark backward visited
import java.util.*;
public class Solution {
public int shortestPathLength(int[][] graph) {
int n = graph.length;
int allVisited = (1 << n) - 1;
Queue<int[]> q1 = new LinkedList<>();
Queue<int[]> q2 = new LinkedList<>();
Map<String, Integer> visited1 = new HashMap<>();
Map<String, Integer> visited2 = new HashMap<>();
for (int i = 0; i < n; i++) {
int mask = 1 << i;
q1.offer(new int[]{i, mask});
visited1.put(i + "," + mask, 0);
q2.offer(new int[]{i, allVisited ^ mask});
visited2.put(i + "," + (allVisited ^ mask), 0);
}
while (!q1.isEmpty() && !q2.isEmpty()) {
int res = expand(graph, q1, visited1, visited2);
if (res != -1) return res;
res = expand(graph, q2, visited2, visited1);
if (res != -1) return res;
}
return -1;
}
private int expand(int[][] graph, Queue<int[]> queue, Map<String, Integer> visitedCurr, Map<String, Integer> visitedOther) {
int size = queue.size();
for (int i = 0; i < size; i++) {
int[] cur = queue.poll();
int node = cur[0], mask = cur[1];
int dist = visitedCurr.get(node + "," + mask);
if (visitedOther.containsKey(node + "," + mask)) {
return dist + visitedOther.get(node + "," + mask);
}
for (int nei : graph[node]) {
int nextMask = (visitedCurr == visitedOther) ? (mask | (1 << nei)) : (mask & ~(1 << nei));
String key = nei + "," + nextMask;
if (!visitedCurr.containsKey(key)) {
visitedCurr.put(key, dist + 1);
queue.offer(new int[]{nei, nextMask});
}
}
}
return -1;
}
// Driver code
public static void main(String[] args) {
Solution sol = new Solution();
int[][] graph = {{1,2,3},{0},{0},{0}};
System.out.println(sol.shortestPathLength(graph)); // Output: 4
}
}
Line Notes
int n = graph.lengthNumber of nodes
int allVisited = (1 << n) - 1Bitmask for all nodes visited
Queue<int[]> q1 = new LinkedList<>()Forward BFS queue
Queue<int[]> q2 = new LinkedList<>()Backward BFS queue
Map<String, Integer> visited1 = new HashMap<>()Forward visited states
Map<String, Integer> visited2 = new HashMap<>()Backward visited states
for (int i = 0; i < n; i++)Initialize BFS queues and visited maps
if (visitedOther.containsKey(node + "," + mask))Check if searches meet
int nextMask = (visitedCurr == visitedOther) ? (mask | (1 << nei)) : (mask & ~(1 << nei))Update mask differently for forward/backward
visitedCurr.put(key, dist + 1)Mark visited in current BFS
queue.offer(new int[]{nei, nextMask})Enqueue next state
#include <bits/stdc++.h>
using namespace std;
class Solution {
public:
int shortestPathLength(vector<vector<int>>& graph) {
int n = graph.size();
int allVisited = (1 << n) - 1;
queue<pair<int,int>> q1, q2;
unordered_map<int,int> visited1, visited2;
auto encode = [&](int node, int mask) {
return (mask << 5) | node; // pack node and mask into int
};
for (int i = 0; i < n; i++) {
int mask = 1 << i;
q1.emplace(i, mask);
visited1[encode(i, mask)] = 0;
q2.emplace(i, allVisited ^ mask);
visited2[encode(i, allVisited ^ mask)] = 0;
}
while (!q1.empty() && !q2.empty()) {
int res = expand(graph, q1, visited1, visited2, encode);
if (res != -1) return res;
res = expand(graph, q2, visited2, visited1, encode);
if (res != -1) return res;
}
return -1;
}
private:
int expand(const vector<vector<int>>& graph, queue<pair<int,int>>& q, unordered_map<int,int>& visitedCurr, unordered_map<int,int>& visitedOther, function<int(int,int)> encode) {
int size = q.size();
for (int i = 0; i < size; i++) {
auto [node, mask] = q.front(); q.pop();
int dist = visitedCurr[encode(node, mask)];
if (visitedOther.count(encode(node, mask))) {
return dist + visitedOther[encode(node, mask)];
}
for (int nei : graph[node]) {
int nextMask = (visitedCurr == visitedOther) ? (mask | (1 << nei)) : (mask & ~(1 << nei));
int key = encode(nei, nextMask);
if (!visitedCurr.count(key)) {
visitedCurr[key] = dist + 1;
q.emplace(nei, nextMask);
}
}
}
return -1;
}
};
// Driver code
int main() {
Solution sol;
vector<vector<int>> graph = {{1,2,3},{0},{0},{0}};
cout << sol.shortestPathLength(graph) << "\n"; // Output: 4
return 0;
}
Line Notes
int n = graph.size()Number of nodes
int allVisited = (1 << n) - 1Bitmask for all nodes visited
queue<pair<int,int>> q1, q2Queues for forward and backward BFS
unordered_map<int,int> visited1, visited2Visited states for both BFS
auto encode = [&](int node, int mask)Pack node and mask into single int for map keys
for (int i = 0; i < n; i++)Initialize BFS queues and visited maps
if (visitedOther.count(encode(node, mask)))Check if searches meet
int nextMask = (visitedCurr == visitedOther) ? (mask | (1 << nei)) : (mask & ~(1 << nei))Update mask differently for forward/backward
visitedCurr[key] = dist + 1Mark visited in current BFS
q.emplace(nei, nextMask)Enqueue next state
function shortestPathLength(graph) {
const n = graph.length;
const allVisited = (1 << n) - 1;
const q1 = [];
const q2 = [];
const visited1 = new Map();
const visited2 = new Map();
function encode(node, mask) {
return node + ',' + mask;
}
for (let i = 0; i < n; i++) {
const mask = 1 << i;
q1.push([i, mask]);
visited1.set(encode(i, mask), 0);
q2.push([i, allVisited ^ mask]);
visited2.set(encode(i, allVisited ^ mask), 0);
}
function expand(queue, visitedCurr, visitedOther) {
const size = queue.length;
for (let i = 0; i < size; i++) {
const [node, mask] = queue.shift();
const dist = visitedCurr.get(encode(node, mask));
if (visitedOther.has(encode(node, mask))) {
return dist + visitedOther.get(encode(node, mask));
}
for (const nei of graph[node]) {
const nextMask = visitedCurr === visitedOther ? (mask | (1 << nei)) : (mask & ~(1 << nei));
const key = encode(nei, nextMask);
if (!visitedCurr.has(key)) {
visitedCurr.set(key, dist + 1);
queue.push([nei, nextMask]);
}
}
}
return -1;
}
while (q1.length && q2.length) {
let res = expand(q1, visited1, visited2);
if (res !== -1) return res;
res = expand(q2, visited2, visited1);
if (res !== -1) return res;
}
return -1;
}
// Driver code
const graph = [[1,2,3],[0],[0],[0]];
console.log(shortestPathLength(graph)); // Output: 4
Line Notes
const n = graph.lengthNumber of nodes
const allVisited = (1 << n) - 1Bitmask for all nodes visited
const q1 = []Forward BFS queue
const q2 = []Backward BFS queue
const visited1 = new Map()Forward visited states
const visited2 = new Map()Backward visited states
function encode(node, mask)Create unique key for visited maps
if (visitedOther.has(encode(node, mask)))Check if searches meet
const nextMask = visitedCurr === visitedOther ? (mask | (1 << nei)) : (mask & ~(1 << nei))Update mask differently for forward/backward
visitedCurr.set(key, dist + 1)Mark visited in current BFS
queue.push([nei, nextMask])Enqueue next state
TimeO(n * 2^(n/2))
SpaceO(n * 2^(n/2))
Bidirectional BFS reduces the search depth roughly by half, significantly reducing states explored compared to single BFS.
💡 This approach is more complex but can handle larger graphs faster by meeting in the middle, cutting down exponential growth.
Interview Verdict: Accepted / Advanced optimization
While not always necessary, bidirectional BFS is a powerful technique to mention for optimization in interviews.