Topics › Graphs › Shortest Path Algorithms and Problems
Shortest path in undirected graph with unit weights
Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given an undirected graph with n nodes numbered 0 to n - 1 and a list of edges {u, v}. Every edge has length 1. For a given source node src, return an array dist where dist[i] is the length of the shortest path from src to i, that is, the minimum number of edges. If node i cannot be reached, dist[i] is -1.
Constraints: 1 ≤ n ≤ 10^4, 0 ≤ m ≤ 10^4 edges, 0 ≤ src < n.
Example 1
n = 9, src = 0
edges = [0-1, 0-3, 3-4, 4-5, 5-6, 1-2, 2-6, 6-7, 7-8, 6-8]
answer = [0, 1, 2, 1, 2, 3, 3, 4, 4]
// node 6 is 3 away along 0-1-2-6 (and also along 0-3-4-5-6, which is longer)
Example 2
n = 5, src = 0, edges = [0-1, 1-2, 3-4]
answer = [0, 1, 2, -1, -1] // 3 and 4 are in a different component
Approach 1: Brute force, relax edges until nothing changes
Intuition. Correct distances satisfy one local rule for every edge u - v: dist[v] ≤ dist[u] + 1, because you can always reach v by first reaching u and then taking one more step. So start with dist[src] = 0 and infinity everywhere else, and repair any edge that breaks the rule. Keep sweeping over all edges until a sweep repairs nothing.
How many sweeps are needed? After sweep r, every node whose shortest path uses at most r edges has its correct distance, since the next edge of that path gets repaired in the following sweep. A shortest path never repeats a node, so it uses at most n - 1 edges, and n - 1 sweeps are always enough. In practice we stop as soon as a sweep changes nothing.
Algorithm.
- Fill
distwith infinity and setdist[src] = 0. - Repeat at most
n - 1times: for each edge, in both directionsu → v, ifdist[u]is finite anddist[u] + 1 < dist[v], lowerdist[v]. - Stop early when a sweep changes nothing.
- Replace remaining infinities by
-1.
static int[] bruteForce(int n, int[][] edges, int src) {
final int INF = Integer.MAX_VALUE;
int[] dist = new int[n];
Arrays.fill(dist, INF);
dist[src] = 0;
for (int round = 1; round < n; round++) { // a shortest path uses at most n - 1 edges
boolean changed = false;
for (int[] e : edges) {
for (int d = 0; d < 2; d++) { // undirected: try u -> v and v -> u
int u = e[d], v = e[1 - d];
if (dist[u] != INF && dist[u] + 1 < dist[v]) {
dist[v] = dist[u] + 1;
changed = true;
}
}
}
if (!changed) break;
}
for (int i = 0; i < n; i++) if (dist[i] == INF) dist[i] = -1;
return dist;
}
Complexity. Time O(V × E): up to V - 1 sweeps over all E edges. Space O(V) for the distance array. This is the Bellman-Ford algorithm specialised to weight 1. It works, but it ignores the most useful fact we have: every edge costs the same.
Approach 2: Optimal, breadth-first search
Intuition. With equal edge lengths, “shortest” just means “fewest edges”, and breadth-first search explores a graph in exactly that order. It processes the source, then all nodes one edge away, then all nodes two edges away, and so on. The queue always holds at most two neighbouring layers, nearer nodes in front of farther ones.
That ordering is why the first time BFS discovers a node is along a shortest path. Suppose v is first discovered from u, so it gets dist[u] + 1. If there were a shorter route to v, its second-to-last node w would be strictly closer to the source than u, so w would have left the queue before u and would have discovered v first. Hence the distance assigned at discovery is final and is never updated, and the distance array can double as the visited array: -1 means “not discovered yet”.
Algorithm.
- Build the adjacency list, adding every edge in both directions.
- Fill
distwith-1, setdist[src] = 0and enqueuesrc. - Pop
u. For each neighbourvwithdist[v] == -1, setdist[v] = dist[u] + 1and enqueuev. - When the queue is empty,
distis the answer; unreached nodes are still-1.
static int[] optimal(int n, int[][] edges, int src) {
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
for (int[] e : edges) {
adj.get(e[0]).add(e[1]);
adj.get(e[1]).add(e[0]);
}
int[] dist = new int[n];
Arrays.fill(dist, -1); // -1 also means "not discovered yet"
dist[src] = 0;
Deque<Integer> queue = new ArrayDeque<>();
queue.add(src);
while (!queue.isEmpty()) {
int u = queue.poll();
for (int v : adj.get(u)) {
if (dist[v] == -1) {
dist[v] = dist[u] + 1;
queue.add(v);
}
}
}
return dist;
}
Dry run on Example 1 from src = 0 (neighbours are visited in the order their edges were listed):
| pop | newly discovered (distance) | queue after |
|---|---|---|
| start | 0 (0) | [0] |
| 0 | 1 (1), 3 (1) | [1, 3] |
| 1 | 2 (2) | [3, 2] |
| 3 | 4 (2) | [2, 4] |
| 2 | 6 (3) | [4, 6] |
| 4 | 5 (3) | [6, 5] |
| 6 | 7 (4), 8 (4) | [5, 7, 8] |
| 5, 7, 8 | nothing new | [] |
Node 6 is discovered from 2 with distance 3 before node 5 is even popped, so the longer route 0-3-4-5-6 is never considered. The final array is [0, 1, 2, 1, 2, 3, 3, 4, 4].
Complexity. Time O(V + E): each node is enqueued once and each adjacency list is scanned once (each undirected edge twice). Space O(V + E) for the adjacency list, plus O(V) for the distances and the queue.
Why not DFS, and when BFS stops being enough
DFS finds a path, not the shortest. On the square 0-1-2-3-0, a DFS from 0 may walk 0 → 1 → 2 → 3 and label 3 with distance 3, although the edge 0-3 gives 1. Fixing that requires revisiting nodes whenever a shorter route appears, which loses the linear bound.
BFS relies on every edge having the same length. With arbitrary non-negative weights, the node discovered first is no longer the closest one, and you need Dijkstra’s algorithm, which replaces the FIFO queue with a priority queue. If the graph is directed but still unweighted, the same BFS code works; just add each edge in one direction only.
Edge cases and pitfalls
- Unreachable nodes. They are never discovered and keep
-1. In the brute force, guard againstINF + 1, which would overflow to a negative number; thedist[u] != INFcheck does that. - Single node.
n = 1with no edges returns[0]. - Source other than
0. Nothing is special about node0; the test runs the same graph fromsrc = 6. - Self-loops and parallel edges. A self-loop never discovers anything new and a repeated edge finds its endpoint already discovered, so both are harmless.
- Marking on discovery, not on pop. Set
dist[v]when you enqueuev. Marking only when popping lets the same node be enqueued many times. - Undirected input. Forgetting the reverse edge turns the graph directed and makes some nodes look unreachable.
Related problems
- Djisktra’s Algorithm generalises this to non-negative weights.
- Shortest path in DAG handles weighted directed acyclic graphs in linear time.
- Shortest Distance in a Binary Maze is the same BFS on a grid.
- Rotten Oranges runs BFS from several sources at once.
- Distance of nearest cell having one is another multi-source BFS distance problem.
- Word ladder I is BFS on an implicit graph of words.