Problem overview
The method bfsMinDistance receives the number of vertices n and an int[][] of edges, where each edge is a pair [u, v]. Vertices are labeled 0 to n - 1 and every edge has the same cost: one step. The method returns an array dist of length n in which dist[i] is the smallest number of edges needed to get from vertex 0 to vertex i, or -1 if vertex i can't be reached.
For n = 5 and edges = [[0,1],[0,2],[1,3],[2,4]] the answer is [0, 1, 1, 2, 2]. Vertex 0 is zero steps from itself, vertices 1 and 2 are one step away, and vertices 3 and 4 are two steps away, through 1 and 2.
Approach: breadth-first search
Breadth-first search (BFS) explores a graph in layers. It first handles the start vertex, then everything one edge away, then everything two edges away, and so on. Since all edges cost the same, the first time BFS reaches a vertex is along a shortest path, and no later route can be shorter. That is why the distance can be written down at that moment and never changed.
The layer order comes from a queue: first in, first out. Vertices discovered earlier are processed earlier, so closer vertices always come before farther ones. Taking vertices from the wrong end would make the search depth-first and break the distances.
The distance array also serves as the visited record. It starts filled with -1, meaning "not reached yet". A neighbor whose entry is still -1 is new: give it the current distance plus one and queue it. A neighbor whose entry is not -1 has already been reached, at an equal or smaller distance, so it is skipped. A vertex that is never reached keeps -1, which is exactly what the problem wants for unreachable vertices.
Step by step
- Build the adjacency list: a
List<List<Integer>>with one emptyArrayListper vertex, sograph.get(i)holds the vertices that vertexileads to. - Fill it from
edges. For eachint[] edge, addedge[1]to the list ofedge[0]. The lesson's code follows each edge from its first vertex to its second. - Create the queue as an
ArrayDequedeclared as aDeque<Integer>, and add vertex0.addputs a vertex at the back andpolltakes one from the front. - Create
minDistas anint[]of sizen. A newintarray starts with zeros, which would wrongly claim every vertex is at distance 0, so fill it with-1usingArrays.fill. Then setminDist[0]to0. - Loop while the queue is not empty. Each vertex enters the queue at most once, so the loop ends.
- Take the front vertex into
int nodewithpoll. - Loop over its neighbors,
adjNode, ingraph.get(node). - Handle new vertices. If
minDist[adjNode] == -1, set it tominDist[node] + 1and addadjNodeto the queue. - Return
minDist.
The solution in Java
class Solution {
public int[] bfsMinDistance(int n, int[][] edges) {
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < n; i++) {
graph.add(new ArrayList<>());
}
for (int[] edge : edges) {
graph.get(edge[0]).add(edge[1]);
}
Deque<Integer> queue = new ArrayDeque<>();
queue.add(0);
int[] minDist = new int[n];
Arrays.fill(minDist, -1);
minDist[0] = 0;
while (!queue.isEmpty()) {
int node = queue.poll();
for (int adjNode : graph.get(node)) {
if (minDist[adjNode] == -1) {
minDist[adjNode] = minDist[node] + 1;
queue.add(adjNode);
}
}
}
return minDist;
}
}
Walking through the example: the queue starts as [0]. Processing 0 sets minDist[1] and minDist[2] to 1 and queues both. Processing 1 reaches 3 and sets it to 2, and processing 2 reaches 4 and sets it to 2. Processing 3 and 4 finds no new vertices, the queue empties, and the result is [0, 1, 1, 2, 2].
Java details worth a closer look:
DequeorQueue.ArrayDequeis the usual choice for a queue in Java. It is faster thanLinkedListfor this job, and it doesn't allownullelements. Declaring the variable asDeque<Integer>works becauseaddandpollare available on it.pollreturns anInteger. Assigning it to anintunboxes it automatically. Since the loop only runs while the queue is non-empty,pollnever returnsnullhere.Arrays.fillsets every slot of the array to the given value in one call. Without it, a loop would be needed.- Edge direction. The code reads the pairs as
edge[0]toedge[1]only. That matches the example, where every edge points away from vertex 0. If edges should work in both directions, addgraph.get(edge[1]).add(edge[0])as well. Without it, a vertex that is only listed as the first endpoint of an edge toward vertex 0 would stay at-1.
Complexity
Let m be the number of edges.
Time: O(n + m). Building the lists is O(n + m). Each vertex is queued at most once because only a -1 entry gets queued. Each vertex's neighbor list is scanned once, so every edge is examined once.
Space: O(n + m). The adjacency list stores every edge, and the queue and the distance array each hold at most n entries.
Edge cases and common mistakes
- Unreachable vertices. With
n = 4andedges = [[0,1],[2,3]], vertices 2 and 3 are never queued, so the result is[0, 1, -1, -1]. - Forgetting the
-1fill. An unfilledint[]holds zeros, so unreached vertices would look like they sit at distance 0, and the testminDist[adjNode] == -1would never fire. - Setting the source distance. Leaving
minDist[0]at-1would let a neighbor send the search back to vertex 0 and overwrite its distance. - Using a stack instead of a queue. Taking the newest vertex first gives some path, not necessarily the shortest one.
- Setting the distance when the vertex is taken from the queue instead of when it is discovered. The same vertex can then be queued several times before it is first processed.
- Cycles. A cycle can't cause an endless loop, because a vertex that already has a distance is never queued again.
Testing
n = 5,edges = [[0,1],[0,2],[1,3],[2,4]]gives[0, 1, 1, 2, 2].n = 4,edges = [[0,1],[2,3]]gives[0, 1, -1, -1].n = 3,edges = [[0,1],[1,2],[0,2]]gives[0, 1, 1].