Graphs: traversals, Dijkstra and A*

A graph is a set of nodes (V) connected by edges (E). Traversals visit every node; Dijkstra and A* find shortest paths when edges have weights.

Representing a graph

In the examples, when a node has several neighbors, they are taken in alphabetical order. The result depends on this convention: check the one your course uses.

Depth-first search (DFS)

Go as deep as possible before backtracking. It uses a stack, or recursion.

dfs(u):
    mark u as visited
    for each neighbor v of u (in order):
        if v is not visited: dfs(v)

Example graph (undirected): A–B, A–C, B–D, C–D, C–F, D–E.

DFS from A: A → B → D (first unvisited neighbor of B) → C (first unvisited neighbor of D) → F. C has no free neighbor left: back to D, which still has E. Visit order: A, B, D, C, F, E.

Breadth-first search (BFS)

Nodes are visited in "layers": first the start, then all its neighbors, then the neighbors' neighbors. It uses a queue (first in, first out).

bfs(start):
    queue = [start]; mark start as discovered
    while the queue is not empty:
        u = dequeue()
        visit u
        for each neighbor v of u (in order):
            if v is not discovered: mark it and enqueue it

BFS from A on the same graph: queue [A] → visit A, enqueue B, C → visit B, enqueue D → visit C, enqueue F (D is already discovered) → visit D, enqueue E → F → E. Order: A, B, C, D, F, E.

In an unweighted graph, BFS finds the path with the fewest edges. Both traversals cost O(|V| + |E|).

Dijkstra: shortest paths

With non-negative weights, Dijkstra computes the shortest distance from the start to every other node.

  1. d(start) = 0, every other distance = ∞.
  2. Choose the unvisited node with the smallest distance. Its distance becomes final.
  3. Relax each of its neighbors v: if d(u) + w(u, v) < d(v), then d(v) = d(u) + w(u, v).
  4. Repeat until every reachable node has been visited.

Example: edges A–B (4), A–C (1), B–C (2), B–D (5), C–D (8), D–E (3), start A.

ChosenUpdatesDistances
A (0)B = 4, C = 1A 0 · B 4 · C 1 · D ∞ · E ∞
C (1)B: 1 + 2 = 3 < 4; D = 1 + 8 = 9A 0 · B 3 · C 1 · D 9 · E ∞
B (3)D: 3 + 5 = 8 < 9A 0 · B 3 · C 1 · D 8 · E ∞
D (8)E = 8 + 3 = 11A 0 · B 3 · C 1 · D 8 · E 11
E (11)nonedone

The direct edge A–B (4) is not the best route: going through C costs 3. With a binary heap as the priority queue, Dijkstra costs O((|E| + |V|) log |V|).

No negative weights. Dijkstra assumes a chosen distance can never decrease. With negative weights, another algorithm is needed (Bellman-Ford).

A*: searching toward the goal

When looking for a path to a single goal, A* adds an estimate of the remaining distance. Every node in the open list has a priority:

f(n) = g(n) + h(n)

Always choose the open node with the smallest f, and stop when the goal is chosen (not when it is discovered).

Example: same graph, from A to E, with h(A) = 7, h(B) = 5, h(C) = 6, h(D) = 3, h(E) = 0.

  1. Open: A (f = 0 + 7 = 7). Choose A → B (g 4, f 9) and C (g 1, f 7).
  2. Choose C (f 7) → B improves (g 3, f 8), D (g 9, f 12).
  3. Choose B (f 8) → D improves (g 8, f 11).
  4. Choose D (f 11) → E (g 11, f 11).
  5. Choose E: it is the goal, cost 11.

A good heuristic

With h = 0 everywhere, A* becomes exactly Dijkstra. The closer h is to the true cost, the fewer nodes A* explores.

Two definitions to know