Graphs

advanced30 min

Dropping the hierarchy

A tree has a root, and every node has exactly one parent. A graph drops both rules. Any node may connect to any other, connections can form loops, and there is no top.

That freedom makes graphs the most general structure in this module. Trees and linked lists are both special cases of a graph.

The pieces have standard names:

  • Node (or vertex) — a thing
  • Edge — a connection between two things
Kind of graphMeaningExample
UndirectedEdges work both waysFriendship; two-way roads
DirectedEdges work one wayPrerequisites; one-way streets
WeightedEdges carry a numberDistances; travel costs
UnweightedAll edges count equallyAdjacent grid cells

Recognising one

The word “graph” almost never appears in a problem. What appears is:

  • Rooms connected by doors
  • Cities joined by roads
  • Tasks where one must finish before another
  • Grid cells you can walk between
  • Web pages linking to each other

All graphs. If the problem describes things and connections between them, that is what you have.

Storing a graph

The usual choice is an adjacency list: for each node, the list of nodes it connects to.

import java.util.*;

int n = 5;                                   // nodes numbered 0..4
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) {
    adj.add(new ArrayList<>());
}

// add an undirected edge between u and v
void addEdge(List<List<Integer>> adj, int u, int v) {
    adj.get(u).add(v);
    adj.get(v).add(u);        // omit this line for a directed graph
}

The most common graph bug

Forgetting the second line for an undirected graph.

If a traversal reaches fewer nodes than you expect, check this first. A one-directional edge in a graph you believe is two-directional produces results that look almost right, which makes it hard to spot.

When nodes are named rather than numbered, a map works:

Map<String, List<String>> adj = new HashMap<>();

void addEdge(Map<String, List<String>> adj, String a, String b) {
    adj.computeIfAbsent(a, k -> new ArrayList<>()).add(b);
    adj.computeIfAbsent(b, k -> new ArrayList<>()).add(a);
}

computeIfAbsent creates the list on first use, so you avoid a null check on every insert.

Go as deep as possible along one path before backing up. Naturally recursive.

static void dfs(int node, List<List<Integer>> adj, boolean[] visited) {
    visited[node] = true;
    System.out.println("visiting " + node);

    for (int neighbour : adj.get(node)) {
        if (!visited[neighbour]) {
            dfs(neighbour, adj, visited);
        }
    }
}

The visited array is not optional. Unlike a tree, a graph can contain loops, so without it the traversal follows a cycle forever. Mark the node at the start of the call, before recursing — marking it afterwards still loops.

Explore all immediate neighbours first, then their neighbours, spreading outward in rings. Uses a queue.

static int[] bfs(int start, List<List<Integer>> adj, int n) {
    int[] distance = new int[n];
    Arrays.fill(distance, -1);          // -1 means not reached

    Queue<Integer> queue = new ArrayDeque<>();
    distance[start] = 0;
    queue.offer(start);

    while (!queue.isEmpty()) {
        int node = queue.poll();
        for (int neighbour : adj.get(node)) {
            if (distance[neighbour] == -1) {
                distance[neighbour] = distance[node] + 1;
                queue.offer(neighbour);
            }
        }
    }
    return distance;
}

Here distance doubles as the visited marker — -1 means unreached. Setting it when you offer the node, not when you poll it, is what stops the same node being queued twice.

Choosing between them

  • Just need to reach everything — either works. DFS is shorter.
  • Need the shortest path in an unweighted graph — BFS. It reaches every node by the fewest edges, and DFS does not.
  • Very deep graph — BFS, or an iterative DFS. Recursive DFS can overflow the call stack past a few tens of thousands of levels.
  • Weighted edges — neither is enough on its own. That needs Dijkstra’s algorithm, which is beyond this lesson.

Counting connected components

A common question: how many separate groups are there? Start a traversal from every node not yet reached; each fresh start is one new group.

boolean[] visited = new boolean[n];
int groups = 0;

for (int i = 0; i < n; i++) {
    if (!visited[i]) {
        groups++;
        dfs(i, adj, visited);
    }
}
System.out.println(groups + " separate groups");

The outer loop matters. Starting only from node 0 finds one group and misses every disconnected part of the graph.

Grids are graphs

A grid problem is a graph problem where cells are nodes and adjacent cells are connected. You do not build an adjacency list — the neighbours are computed from the coordinates.

static final int[] DR = {-1, 1, 0, 0};
static final int[] DC = {0, 0, -1, 1};

static void explore(char[][] grid, boolean[][] seen, int r, int c) {
    seen[r][c] = true;

    for (int d = 0; d < 4; d++) {
        int nr = r + DR[d];
        int nc = c + DC[d];

        if (nr < 0 || nr >= grid.length) continue;
        if (nc < 0 || nc >= grid[0].length) continue;
        if (seen[nr][nc] || grid[nr][nc] == '#') continue;

        explore(grid, seen, nr, nc);
    }
}

The bounds checks must come before reading grid[nr][nc]. Java stops evaluating || at the first true condition, so an out-of-range index is never used. Reordering these throws ArrayIndexOutOfBoundsException.

The offset arrays give the four orthogonal neighbours. For diagonal movement too, use all eight combinations of -1, 0, and 1 except (0, 0).

Common mistakes

  • Adding only one direction for an undirected edge.
  • No visited tracking, so a cycle loops forever.
  • Marking visited too late — mark on discovery, not after processing.
  • Only starting from one node, missing disconnected parts.
  • Bounds check after array access in grid code.
  • Using DFS for shortest paths. It finds a path, not the shortest.
  • Deep recursion overflowing the stack on large graphs.

Practice

  1. Build an undirected graph of 6 nodes with a few edges, then print every node reachable from node 0 using DFS.
  2. Write a method that counts the connected components of an undirected graph.
  3. Write a method that returns the fewest edges between two nodes, or -1 if unreachable.
  4. Given a grid of . and #, count the separate regions of connected . cells.
  5. Given a directed graph, determine whether a path exists from node a to node b.
Hints
  1. Build the adjacency list, then one DFS call with a visited array.
  2. The counting loop above.
  3. BFS from the first node, then read the distance to the second.
  4. The grid traversal, counting how many times you start a fresh exploration.
  5. DFS or BFS from a, then check whether b was visited. Build the adjacency list with edges in one direction only.

Related

Going further