SEN2212 · Week 11 · Analyzing and implementing graph algorithms.

Shortest path and cycle detection: BFS, Dijkstra, Bellman-Ford

Shortest paths with BFS in an unweighted graph, with Dijkstra for positive weights, with Bellman-Ford for negative edges; finding cycles in directed and undirected graphs. The exam's table-filling questions come from here.

shortest pathsingle-sourceunweighted graphweighted graphDijkstra's algorithmrelaxationknown / visitednegative edgeBellman-Fordnegative cyclecycle detectionback edgeDAG

Last week you saw how to store a graph and how to traverse it with BFS/DFS. This week we put those traversals to work: the questions the shortest path from one source (single-source) to every node and is there a cycle in the graph. A “fill in the Dijkstra table” question almost certainly appears in the exam. Alongside it come short questions such as “why is Dijkstra wrong here?” or “is there a cycle in this graph?”.

Golden rules:

  1. If there are no weights, BFS is enough. Shortest path = fewest edges = BFS layer.
  2. If weights are positive, Dijkstra. At each step pick the node with the smallest distance among the unknown ones, finalize it, and relax its edges.
  3. If there is a negative edge, Bellman-Ford. Dijkstra may silently give a wrong answer.
  4. The path is read backward from the prev array. Walk from the target to the source, then reverse.
  5. In a directed graph, cycle = an edge to a gray node. In an undirected one = an edge whose two ends are in the same set.

Unweighted shortest path: BFS

If the cost of every edge is 1, the shortest path is the path with the fewest edges. Since BFS takes nodes out in order of distance from the source (layer by layer), the distance of a node is final the moment it is first discovered. The rule: dist[w] = dist[v] + 1, prev[w] = v.

An example undirected graph (neighbor lists in ascending order):

      0
     / \
    1   2
     \ /
      3
      |
      4
     / \
    5   6
     \ /
      7
queue          dequeued  discovered (dist)
[0]            0         1(1) 2(1)
[1,2]          1         3(2)
[2,3]          2         -    (3 was already seen)
[3]            3         4(3)
[4]            4         5(4) 6(4)
[5,6]          5         7(5)
[6,7]          6         -
[7]            7         -
BFS dist: [0, 1, 1, 2, 3, 4, 4, 5]
BFS prev: [-1, 0, 0, 1, 3, 4, 4, 5]
BFS path 0->7: [0, 1, 3, 4, 5, 7]

The time is O(V+E)O(V + E): each node enters the queue once, and each edge is looked at once (twice in an undirected graph).

PITFALL · write the distance when discovering, not when dequeuing. Give dist and prev and count it as “seen” the moment you put the node in the queue. Otherwise the same node enters the queue twice.

Dijkstra’s algorithm

The edges have weights and all are non-negative. The idea: the node closest to the source cannot have a shorter path through any other node (each extra edge increases the cost). So finalize (known) the node with the smallest tentative distance and relax the edges leaving it:

du>dv+w(v,u)  ⇒  du=dv+w(v,u),  prevu=vd_u > d_v + w(v,u) \;\Rightarrow\; d_u = d_v + w(v,u),\; prev_u = v

The steps:

  1. d[source] = 0, the others ∞. None is known.
  2. From the unknown ones, pick the v with the smallest d. On a tie, pick the smaller label.
  3. Make v known. For each unknown neighbor u, relax.
  4. Go back to 2 until no unknown remains (or all remaining are ∞).

Worked example

The graph G1 (directed), shown as neighbor lists:

A: B(4) C(1)
B: D(5)
C: B(2) D(8) E(10)
D: E(2) F(6)
E: F(2)
F: -

In the exam the table is filled like this. Each row is one pick; * means known.

step pick  A   B   C   D   E   F
 0    -    0   ∞   ∞   ∞   ∞   ∞
 1    A    0*  4   1   ∞   ∞   ∞
 2    C    0*  3   1*  9   11  ∞
 3    B    0*  3*  1*  8   11  ∞
 4    D    0*  3*  1*  8*  10  14
 5    E    0*  3*  1*  8*  10* 12
 6    F    0*  3*  1*  8*  10* 12*

The program’s output:

Dijkstra order: A C B D E F
Dijkstra final: A=0 B=3 C=1 D=8 E=10 F=12
prev: A=- B=C C=A D=B E=D F=E
  path to F: [A, C, B, D, E, F] cost 12

Two moments to notice: in step 2 the value of B dropped from 4 to 3 (cheaper via C). In step 4, E dropped from 11 to 10. This is relaxation: when you find a better path you update the value and the prev.

TIP · keep a prev column in the table too. If the exam asks for the “path from A to F”, you need to have written down through whom you came at each update. Computing it back afterwards wastes time.

Complexity

ImplementationFinding the minimumTotal
Array scanO(V)O(V), V timesO(V2)O(V^2)
Binary heap (PriorityQueue)O(log⁡V)O(\log V)O(Elog⁡V)O(E \log V)

When writing it with Java’s PriorityQueue there is no “decreaseKey”. The practical solution: when a distance drops, add the node again with its new value, and if the node that comes out of the queue is already known, skip it (lazy deletion).

PriorityQueue<int[]> pq = new PriorityQueue<>((x, y) -> x[1] - y[1]);
pq.add(new int[]{src, 0});
while (!pq.isEmpty()) {
    int[] top = pq.poll();
    int v = top[0];
    if (known[v]) continue;          // old copy
    known[v] = true;
    for (int[] e : adj.get(v)) {     // e = {neighbor, weight}
        int u = e[0], w = e[1];
        if (!known[u] && d[v] + w < d[u]) {
            d[u] = d[v] + w;
            prev[u] = v;
            pq.add(new int[]{u, d[u]});
        }
    }
}

Negative edges and Bellman-Ford

Dijkstra’s “finalize the closest” step trusts that an edge found later cannot decrease the distance. A negative edge breaks this trust.

A --2--> B --1--> D
|        ^
5       -4
|        |
+-> C ---+
Dijkstra order: A B D C
Dijkstra final: A=0 B=2 C=5 D=3
BF round 1: A=0 B=1 C=5 D=2

Dijkstra finalized B with 2 and made D 3. C came last; the edge C→B could have lowered B to 1, but B was closed. The real answer is D = 2.

Bellman-Ford makes no picks: in each round it relaxes all the edges in order. Since a shortest path contains at most V−1 edges, V−1 rounds are enough. The time is O(V⋅E)O(V \cdot E), slower than Dijkstra but it works correctly with negative edges.

Arrays.fill(d, INF);
d[src] = 0;
for (int round = 1; round <= V - 1; round++)
    for (Edge e : edges)
        if (d[e.from] != INF && d[e.from] + e.w < d[e.to])
            d[e.to] = d[e.from] + e.w;

The edge order affects the number of rounds. In the order e1 B→C 2, e2 A→B −1, e3 S→A 4, e4 S→B 5, e5 C→D 3, e6 A→C 6, from S:

BF round 1: S=0 A=4 B=5 C=10 D=inf
BF round 2: S=0 A=4 B=3 C=7 D=10
BF round 3: S=0 A=4 B=3 C=5 D=8
BF round 4: S=0 A=4 B=3 C=5 D=8 (no change)

If nothing changed in a round, you can stop early. If an edge can still be relaxed after V−1 rounds, there is a cycle with a negative total; then the “shortest path” is undefined (by going around the cycle again and again, the cost decreases forever). In the graph S→A 1, A→B 2, B→C −4, C→A 1, the total of A→B→C→A is −1 and the program prints BF negative cycle: true.

PITFALL · the “add a constant to all weights” trick does not work. If you add 5 to every edge to get rid of the negative edge, paths with many edges get penalized more, and the shortest path changes.

Cycle detection

Directed graph: DFS with three colors

  • White (0): never seen.
  • Gray (1): entered in the DFS, not yet finished (on the recursion stack).
  • Black (2): all its descendants are finished.

An edge to a gray node means going back to an ancestor on the current path: a back edge, that is, a cycle. An edge to a black node is not a cycle; that branch is already finished.

0 --> 1 --> 2
|     ^     |
v     |     v
4     +---- 3

DFS goes down in the order 0, 1, 2, 3, and all four are gray. 3→1 goes to a gray node:

G2 directed cycle: true back edge 3->1
DAG directed cycle: false

A directed graph with no cycle is called a DAG. Only DAGs have a topological order; in Kahn’s algorithm (last week), if all nodes cannot be removed, there is also a cycle.

Undirected graph

In an undirected graph, every edge is two-directional, so “the neighbor was already seen” is not always a cycle: the parent you came from has been seen too. Two ways:

  • DFS + parent: a seen neighbor that is not the parent means a cycle.
  • Union-find: add the edges one by one. If the two ends are already in the same set, this edge closes a cycle. Kruskal (week 13) uses exactly this.
undirected: first cycle-closing edge 1-3

A quick check: in a connected undirected graph, if E≥VE \ge V there is definitely a cycle (in a tree E=V−1E = V - 1).

Which algorithm when?

SituationAlgorithmTime
UnweightedBFSO(V+E)O(V+E)
Weights ≥ 0Dijkstra (heap)O(Elog⁡V)O(E \log V)
Negative edges possibleBellman-FordO(VE)O(VE)
DAG, any weightsRelaxing in topological orderO(V+E)O(V+E)
Is there a directed cycle3-color DFSO(V+E)O(V+E)
Is there an undirected cycleDFS + parent or union-findO(V+E)O(V+E)

Common mistakes

Wrong ideaThe truth
In Dijkstra the next node is a neighbor of the last one pickedThe smallest d among all unknown nodes is picked
The value of a known node is updated laterIt is not updated in Dijkstra; if it changes, there is a negative edge
Few edges = short pathOnly in an unweighted graph. In a weighted one, the total weight
Dijkstra works correctly with negative edges tooIt can give a wrong answer, use Bellman-Ford
Bellman-Ford does V roundsV−1 rounds are enough; the V-th is only the negative cycle check
In a directed graph an edge to a seen node = cycleOnly an edge to a GRAY node is a cycle
In undirected DFS, returning to the parent is a cycleThe parent is excluded
The path is found by reading the prev array from the startIt is followed backward from the target, then reversed

Test yourself

Exam-style questions. Solve them on paper first, then reveal the answer.

Question 1easy

The undirected graph has edges 0-1, 0-2, 1-3, 2-3, 3-4, 4-5, 4-6, 5-7, 6-7 (adjacency lists in ascending order). Run BFS from vertex 0. Give the shortest-path distance (number of edges) of every vertex 0..7.

Show answer
0, 1, 1, 2, 3, 4, 4, 5

In an unweighted graph the shortest path = the BFS layer. Queue: 0 → (1, 2) distance 1 → 3 distance 2 (via 1) → 4 distance 3 → (5, 6) distance 4 → 7 distance 5. Array: [0, 1, 1, 2, 3, 4, 4, 5].

Question 2medium

In the same BFS from 0, using the prev array (first discoverer wins), what is the shortest path reported from 0 to 7?

Show answer
0 → 1 → 3 → 4 → 5 → 7

The prev array is [-1, 0, 0, 1, 3, 4, 4, 5]. Backward from 7: prev[7]=5, prev[5]=4, prev[4]=3, prev[3]=1, prev[1]=0. Reverse it: 0, 1, 3, 4, 5, 7. The first discoverer of 3 is 1 because 1 left the queue before 2.

Question 3medium

Directed graph G1: A→B 4, A→C 1, C→B 2, B→D 5, C→D 8, C→E 10, D→E 2, D→F 6, E→F 2. Run Dijkstra from A. Give the final distances of A, B, C, D, E, F.

Show answer
A=0, B=3, C=1, D=8, E=10, F=12

At each step pick the smallest distance among the unknown ones, and relax its outgoing edges:

pick A: B=4 C=1
pick C: B=min(4,1+2)=3 D=9 E=11
pick B: D=min(9,3+5)=8
pick D: E=min(11,8+2)=10 F=14
pick E: F=min(14,10+2)=12
pick F: done

Question 4easy

For the Dijkstra run on G1 from A, in which order do the vertices become known?

Show answer
A, C, B, D, E, F

The pick is always the smallest distance among the unknown ones: A(0), C(1), B(3), D(8), E(10), F(12). Distances are finalized in increasing order; this is the key property of Dijkstra.

Question 5medium

Using the Dijkstra result on G1, what is the shortest path from A to F and its cost? Is the direct-looking route A→B→D→F shorter?

Show answer
A → C → B → D → E → F, cost 12; A→B→D→F costs 15

The prev chain: F←E←D←B←C←A. The cost is 1+2+5+2+2 = 12. A→B→D→F = 4+5+6 = 15. A path with fewer edges is not necessarily shorter; in a weighted graph the total weight counts, not the number of edges.

Question 6easy

What is the running time of Dijkstra's algorithm (a) with a simple array scan to find the minimum, (b) with a binary heap priority queue? V = vertices, E = edges.

Show answer
(a) O(V²) (b) O(E log V)

(a) To find the minimum V times, scan V elements: O(V2)O(V^2), and the edge relaxations total O(E)O(E). (b) Each relaxation inserts into the heap at a cost of O(log⁡V)O(\log V), for a total of O(Elog⁡V)O(E \log V). In a sparse graph (E ≈ V) the heap is better, in a dense graph (E ≈ V²) the array is better.

Question 7hard

Graph: A→B 2, A→C 5, C→B -4, B→D 1. Dijkstra from A (a known vertex is never updated again) reports dist(D) = ? What is the true shortest distance to D?

Show answer
Dijkstra: 3, true: 2

The Dijkstra order is A, B, D, C. B is finalized early with distance 2, and D = 2+1 = 3. C is picked last; with C→B -4, B could have been 1, but B is already known and is not updated. The correct path is A→C→B→D = 5-4+1 = 2. Bellman-Ford finds B=1, D=2 in the first round. Lesson: if there is a negative edge, do not trust Dijkstra.

Question 8hard

Bellman-Ford on vertices S, A, B, C, D from S. Edges are relaxed in this fixed order each round: e1 B→C 2, e2 A→B -1, e3 S→A 4, e4 S→B 5, e5 C→D 3, e6 A→C 6. Give dist after round 1 and after round 2.

Show answer
Round 1: A=4 B=5 C=10 D=∞; Round 2: A=4 B=3 C=7 D=10

Round 1: e1, e2 are skipped (B, A are still ∞). e3 A=4, e4 B=5, e5 is skipped (C is ∞), e6 C=4+6=10.
Round 2: e1 C=5+2=7, e2 B=4-1=3, e5 D=7+3=10, e6 10 > 7 no change.
In round 3, C=3+2=5, D=8; in round 4 there is no change. Final: A=4, B=3, C=5, D=8. Because the edge order was bad, the correct answer took several rounds; at most V-1 = 4 rounds are needed.

Question 9easy

How many rounds does Bellman-Ford need in the worst case on a graph with V vertices, what is its running time, and how does it detect a negative cycle?

Show answer
V − 1 rounds, O(V·E); one extra round that still improves a distance means a negative cycle

A shortest path (without cycles) contains at most V-1 edges; each round places at least one more edge correctly. Each round goes over E edges: O(VE)O(VE). If an edge can still be relaxed after V-1 rounds, there is a cycle whose cost decreases forever: a negative cycle. Example: the cycle S→A 1, A→B 2, B→C -4, C→A 1 has total -1, and the check round returns true.

Question 10medium

Directed graph: 0→1, 1→2, 2→3, 3→1, 0→4. Run DFS from 0 with white/gray/black colors (adjacency in ascending order). Does the graph have a cycle? Which edge is found as the back edge?

Show answer
Yes, back edge 3→1

DFS goes from 0 to 1, to 2, to 3; all are gray (on the stack). The edge 3→1 goes to a gray node: a back edge, that is, the cycle 1→2→3→1. An edge to a black (finished) node does not mean a cycle; only an edge to a gray node is a cycle.

Question 11medium

Undirected graph on 0..5. Edges are added in the order 0-1, 1-2, 3-4, 2-4, 4-5, 1-3, 0-5 using union-find. Which edge is the first one that closes a cycle?

Show answer
1-3

0-1, 1-2 give {0,1,2}; 3-4 gives {3,4}; 2-4 merges them into {0,1,2,3,4}; 4-5 brings 5 in as well. When 1-3 arrives, find(1) = find(3), both ends are in the same set: a cycle. The rule in an undirected graph: an edge whose two ends are already connected closes a cycle.

Question 12hard

Write a method boolean hasCycle(List<List<Integer>> adj) that returns true if the directed graph (vertices 0..n-1) contains a cycle. Use DFS with three colors.

Show answer
0 = white, 1 = gray (on stack), 2 = black; an edge to a gray vertex means a cycle

Gray = on the current DFS path. Returning to a gray node is a back edge. In a disconnected graph, start separately from every white node:

static boolean hasCycle(List<List<Integer>> adj) {
    int[] color = new int[adj.size()];
    for (int v = 0; v < adj.size(); v++)
        if (color[v] == 0 && dfs(adj, v, color)) return true;
    return false;
}
static boolean dfs(List<List<Integer>> adj, int v, int[] color) {
    color[v] = 1;                       // gray
    for (int w : adj.get(v)) {
        if (color[w] == 1) return true; // back edge
        if (color[w] == 0 && dfs(adj, w, color)) return true;
    }
    color[v] = 2;                       // black
    return false;
}
Time O(V+E)O(V + E). It returns true on the ring 0→1→2→3→0, and false on the DAG 0→1, 0→2, 1→3, 2→3, 3→4.

Verification: All traces (BFS distances, Dijkstra step tables and paths, Dijkstra's failure on a negative edge, Bellman-Ford rounds, the negative cycle, the back edge with colored DFS, the cycle-closing edge with union-find) were run in the JDK with verify/SEN2212/W11.java and taken verbatim from the output (java W11.java).

Review card

Unweighted → BFS, dist[w] = dist[v] + 1, O(V+E) Positive weights → Dijkstra: pick the smallest unknown, relax its edges relax: if d[v] + w < d[u] then d[u] = d[v] + w, prev[u] = v Dijkstra: array O(V²), binary heap O(E log V) Distances are finalized in INCREASING order; a known node never changes again Negative edge → Dijkstra can be WRONG → Bellman-Ford Bellman-Ford: V-1 rounds, all edges each round, O(V·E) Still a change in round V → negative cycle Path = follow the prev chain backward from the target, reverse it Directed cycle: DFS + 3 colors, an edge to a GRAY node = back edge Undirected cycle: union-find, both ends in the same set → cycle A DAG has a topological order; if there is a cycle there is none