Skip to the chapter
←DSAGGroundworkΣDSA
DSA/Intermediate/I5
17 / 48 written chapters
/

Levels and plan
  1. B1Complexity analysis
  2. B2JavaScript toolkit
  3. B3Arrays & strings
  4. B4Prefix sums
  5. B5Hashing
  6. B6Two pointers
  7. B7Sliding window
  8. B8Binary search
  9. B9Basic recursion
  10. B10Sorting algorithms
  11. B11Stacks & queues
  12. B12Linked lists
  1. I1Trees
  2. I2BST operations
  3. I3Tree problems in depth
  4. I4Heaps & priority queues
  5. I5Graphs: representation
  6. I6BFS on grids
  7. I7Graph problems
  8. I8Topological patterns
  9. I9Backtracking
  10. I10DP: 1D
  11. I11DP: 2D
  12. I12DP state machines
  13. I13Greedy algorithms
  14. I14Intervals
  15. I15Bit manipulation
  16. I16Matrix problems
  17. I17Tries
  18. I18Monotonic stack & queue
  19. I19Math for interviews
  20. I20Design & simulation
  21. I21Divide and conquer
  22. I22Randomization
  1. A1Advanced DP
  2. A2Union-Find
  3. A3Advanced graph algorithms
  4. A4Minimum Spanning Tree
  5. A5Advanced graphs
  6. A6Segment & Fenwick trees
  7. A7Sparse tables
  8. A8String algorithms
  9. A9Design problems
  10. A10Advanced backtracking
  11. A11Games & probability DP
  12. A12Ordered structures
  13. A13Sweep line & geometry
  14. A14Interview strategy

ΣI5 · Part 2 · Intermediate

Graphs: representation & traversal

A tree is a graph with no cycles and one root — now drop both restrictions.

22 min read4 diagrams13 sections13 exercises
17 of 48

In this chapter

  • A graph is nodes plus connections, nothing more
  • Two ways to store one, and when each wins
  • Weighted, directed and labelled graphs
  • DFS — go deep, backtrack when stuck
  • DFS and the call stack: when recursion runs out

and 8 more

about 22 min
13On this page
  1. A graph is nodes plus connections, nothing more
  2. Two ways to store one, and when each wins
  3. Weighted, directed and labelled graphs
  4. DFS — go deep, backtrack when stuck
  5. DFS and the call stack: when recursion runs out
  6. BFS — spread outward, layer by layer
  7. BFS with distances, and the path itself
  8. Watch both traversals
  9. Disconnected graphs: the outer loop
  10. DFS vs BFS — the complexity is identical, the use case isn't
  11. Clone graph: copying a structure that has cycles
  12. A grid is a graph you never build
  13. Recognizing it in an unseen problem

A graph is nodes plus connections, nothing more

Trees have exactly one root and no cycles. A graph relaxes both: any node can connect to any other node, connections can be one-way (directed) or two-way (undirected), and cycles are allowed. That generality is why graphs model almost anything — social networks, road maps, dependency chains, web pages linking to each other.

A B C D E A-B-E-D-A is a cycle — something a tree can never have
No single root, connections in any direction, and a cycle (A→B→E→D→A) — none of these are tree-legal.

Two ways to store one, and when each wins

Adjacency list A → [B, D] B → [A, C, E] C → [B, E] D → [A, E] E → [B, C, D] Adjacency matrix A B C D E A [ 0 1 0 1 0 ] B [ 1 0 1 0 1 ] C [ 0 1 0 0 1 ] D [ 1 0 0 0 1 ]
List: compact, fast to iterate neighbors. Matrix: O(1) "are X and Y connected," O(V²) space.
Adjacency listAdjacency matrix
SpaceO(V + E)O(V²) — wasteful for sparse graphs
"Are X, Y connected?"O(degree of X)O(1)
"Give me all of X's neighbors"O(degree of X) — directO(V) — scan the whole row
Best formost real interview graphs (sparse)dense graphs, or when O(1) edge lookup matters most
// building an adjacency list from an edge list — the shape you'll write constantly
function buildGraph(n, edges) {
  const graph = Array.from({ length: n }, () => []);
  for (const [u, v] of edges) {
    graph[u].push(v);
    graph[v].push(u); // omit this line for a DIRECTED graph
  }
  return graph;
}

Weighted, directed and labelled graphs

The adjacency list above is the shape you want to work in, but problems rarely hand it to you that way. Three inputs turn up again and again, and the first move in every solution is to convert the input into a form where "give me the neighbours of x" is one lookup.

You are givenExampleConvert toCost
an edge listn = 4, edges = [[0, 1], [1, 2], [2, 3]]adjacency listO(V + E)
an adjacency matrix[[0, 1, 0], [1, 0, 1], [0, 1, 0]]usually nothing: scan a row for neighboursO(V) per node, O(V²) for the whole graph
an adjacency list alreadygraph[i] = neighbours of inothing; read it as isO(1)
labels that are not 0 to n - 1[["a", "b"], ["b", "c"]]a Map from label to neighbour arrayO(V + E)

Weights

If an edge carries a number, a distance, cost or probability, store that number next to the neighbour. In a list each entry becomes a pair [neighbour, weight]. In a matrix each cell holds the weight instead of 1, and "no edge" needs a value that cannot be a real weight. 0 is a poor choice, because a free edge is legal in many problems; Infinity works with Math.min.

function buildWeighted(n, edges, directed = false) {
  const graph = Array.from({ length: n }, () => []);
  for (const [u, v, w] of edges) {
    graph[u].push([v, w]); // every entry is [neighbour, weight]
    if (!directed && u !== v) graph[v].push([u, w]); // an undirected edge is stored from both ends
  }
  return graph;
}

A directed graph stores the edge once, at u. An undirected graph stores it at both ends, so every edge appears twice in the lists: the total list length is 2E for E undirected edges, while "E" in O(V + E) still counts each edge once. A self-loop [u, u] would be pushed twice onto the same list if we did not guard, which is the reason for the u !== v test.

Converting back and forth

Sometimes the algorithm wants a different shape than the input. Edge-list algorithms (Kruskal's minimum spanning tree, for example) want the list of edges, and Floyd–Warshall wants a matrix. These conversions are short and worth being able to write without thinking.

function listToEdges(graph, directed = false) {
  const edges = [];
  graph.forEach((neighbours, u) => {
    for (const [v, w] of neighbours) {
      if (directed || u <= v) edges.push([u, v, w]); // undirected: report each edge once
    }
  });
  return edges;
}
function listToMatrix(graph) {
  const n = graph.length;
  const matrix = Array.from({ length: n }, () => new Array(n).fill(Infinity)); // Infinity = no edge
  graph.forEach((neighbours, u) => {
    for (const [v, w] of neighbours) matrix[u][v] = Math.min(matrix[u][v], w); // a matrix holds one weight per pair
  });
  return matrix;
}

function matrixToList(matrix) {
  return matrix.map((row) => {
    const neighbours = [];
    row.forEach((w, v) => {
      if (w !== Infinity) neighbours.push([v, w]);
    });
    return neighbours;
  });
}

listToEdges keeps an undirected edge only from its smaller end (u <= v), so each one comes out once. listToMatrix is O(V²) in space whatever the edge count, and a matrix has one cell per pair, so two parallel edges between the same pair are squeezed into one cell, keeping the cheaper. Going list to edges and back to a list gives the same edges. We checked these four functions by converting thousands of random graphs, with parallel edges and self-loops, back and forth and comparing the edges.

Labels that are not numbers

When nodes are names (words, city codes, variables in an equation) use a Map from the label to its neighbour array. Do not use a plain object: object keys are always strings, so a node labelled with the number 1 is stored under "1", and the check visited.has(1) is then false for a visited set that holds "1". We tried it: new Set([1]).has("1") is false.

function buildLabelled(nodes, edges) {
  const graph = new Map();
  for (const node of nodes) graph.set(node, []); // isolated nodes only exist because we list them here
  for (const [u, v] of edges) {
    graph.get(u).push(v);
    graph.get(v).push(u);
  }
  return graph;
}

The first loop is not decoration. An edge list can only mention nodes that have at least one edge, so a node with no edges would never appear in the map and its component would be missed. If the statement gives the node list or a count n, build from that, not from the edges.

⚠ Five ways the build step goes wrong 1. new Array(n).fill([]) puts the same array in every slot, so pushing into one neighbour list pushes into all of them. We ran const g = Array(3).fill([]); g[0].push(9) and got [[9], [9], [9]]. Use Array.from({ length: n }, () => []). 2. Nodes numbered from 1: allocate n + 1 slots (or subtract one everywhere). Reading graph[n] of an array of length n gives undefined, and the next .push throws a TypeError. 3. Forgetting to add the reverse edge for an undirected graph, or adding it for a directed one. Read the statement for the word "directed". 4. Inferring n from the largest label in the edges and losing isolated nodes at the end. 5. Matrix input where matrix[i][i] is 1: that is a self-loop, not an edge to a neighbour, and it can look like an unvisited neighbour if you do not mark visited first.
Say it like this → "I'll turn the edge list into an adjacency list so a node's neighbours are one lookup. That's O(V + E) space and time, and each undirected edge goes in both lists. Do the weights matter? If they do, each entry becomes a neighbour-and-weight pair."

DFS — go deep, backtrack when stuck

1st 2nd 3rd dead end — backtrack to find any unvisited neighbor
DFS commits to one path fully before ever considering an alternative.
// recursive DFS — the call stack IS the "backtrack" mechanism, for free
function dfs(graph, start, visited = new Set()) {
  visited.add(start);
  console.log(start);
  for (const neighbor of graph[start]) {
    if (!visited.has(neighbor)) dfs(graph, neighbor, visited);
  }
  return visited;
}

// iterative DFS — an explicit stack instead of recursion; reaches the same nodes, but not always in the same order
function dfsIterative(graph, start) {
  const visited = new Set([start]);
  const stack = [start];
  while (stack.length) {
    const node = stack.pop();
    console.log(node);
    for (const neighbor of graph[node]) {
      if (!visited.has(neighbor)) {
        visited.add(neighbor);
        stack.push(neighbor);
      }
    }
  }
  return visited;
}

Both visit exactly the same set of nodes, but the order can differ. On the diamond 0:[1,2], 1:[3], 2:[3] the recursive version prints 0, 1, 3, 2, while the iterative one pushes 1 then 2 and pops 2 first, so it prints 0, 2, 3, 1. Both are valid depth-first orders. To get the recursive order exactly, mark a node visited when you pop it (skipping it if it is already visited) and push its neighbours in reverse, as the demo below does.

DFS and the call stack: when recursion runs out

Recursive DFS is short and easy to read, and it has one hidden cost: every active call holds a frame on the call stack until it returns. When DFS dives down a path, the number of frames alive at once equals the length of the path it is on. On a graph that is one long chain, a linked list that is also a graph, a one-row grid, or a tree that has leaned to one side, that path has V nodes. The work is still O(V + E), but the stack is V frames deep, and the stack is a small fixed-size area, not the whole heap.

The failure is easy to see. We ran the chapter's recursive dfs in Node 24 over a chain 0 → 1 → 2 → .... A chain of 3,000 nodes finished, and a chain of 10,000 nodes threw RangeError: Maximum call stack size exceeded. The point where it breaks depends on the engine, the size of each frame and the stack that is available (it also moves with how warm the code is), so do not memorise a number: remember that it is thousands, not hundreds of thousands.

⚠ Check the constraints before you pick recursion If the statement says up to 10⁵ nodes, or a grid with a million cells, a recursive DFS can fail on the single input an interviewer will try: a chain, or a grid full of land. Say it aloud. "Recursion depth could reach V, so I'll use an explicit stack" is a sentence that earns points.

Turn the recursion into data

A recursive call remembers two things: which node it is working on, and how far through its neighbour loop it has got. If you store exactly that, a pair [node, index of the next neighbour] per call, on your own array, you have the call stack as data, and the array lives on the heap, so its size is limited by memory, not by the stack. The single-stack-of-nodes version earlier in the chapter loses the loop position, and with it the exact recursive order and any chance to act when a call returns. The frame version keeps all three.

function dfsIterative(graph, start) {
  const visited = new Set([start]);
  const order = [start];
  const stack = [[start, 0]]; // [node, index of the next neighbour to try]
  while (stack.length > 0) {
    const frame = stack[stack.length - 1];
    const [node, i] = frame;
    if (i === graph[node].length) {
      stack.pop(); // every neighbour tried: this is where the recursive call would return
      continue;
    }
    frame[1]++;
    const next = graph[node][i];
    if (!visited.has(next)) {
      visited.add(next);
      order.push(next);
      stack.push([next, 0]); // this is the recursive call
    }
  }
  return order;
}

Dry run: dfsIterative on 0:[1, 2], 1:[3], 2:[3], 3:[]

StepTop frame (node, next index)What happensStack after, bottom to topOrder
start—push the start(0,0)[0]
1(0,0)neighbour 1 is new: visit it, push a frame(0,1) (1,0)[0, 1]
2(1,0)neighbour 3 is new: visit it, push a frame(0,1) (1,1) (3,0)[0, 1, 3]
3(3,0)no neighbours: pop, the call returns(0,1) (1,1)[0, 1, 3]
4(1,1)all neighbours tried: pop(0,1)[0, 1, 3]
5(0,1)neighbour 2 is new: visit it, push a frame(0,2) (2,0)[0, 1, 3, 2]
6(2,0)neighbour 3 already seen: skip(0,2) (2,1)[0, 1, 3, 2]
7(2,1)all neighbours tried: pop(0,2)[0, 1, 3, 2]
8(0,2)all neighbours tried: popempty[0, 1, 3, 2]

The order 0, 1, 3, 2 is the one the recursive version gave in the earlier example, where the one-stack version printed 0, 2, 3, 1. Steps 3, 4, 7 and 8 are the moments a recursive call would return. If a problem needs something done after all of a node's neighbours (a post-order action, such as computing a subtree size or marking a node "done" in cycle detection), that is where it goes. Cost: O(V + E) time, and the stack of frames holds at most V entries. We compared its order with the recursive one on thousands of random graphs, and ran it on a chain of 200,000 nodes, which the recursive version cannot do.

Two other ways out. If the visiting order does not matter and you only need "what is reachable", BFS never recurses and has no such limit. And in an interview you rarely need to code the frame version unless the constraints force it: naming the limit and saying which of these two you would switch to is usually enough.

BFS — spread outward, layer by layer

start layer 1 layer 1 layer 1 layer 1 L2 L2 every layer-1 node visited BEFORE any layer-2 node — this is why BFS finds shortest paths
The queue enforces "finish this ring before starting the next" — the source of BFS's shortest-path guarantee.
function bfs(graph, start) {
  const visited = new Set([start]);
  const queue = [start];
  const order = [];
  while (queue.length) {
    const node = queue.shift();
    order.push(node);
    for (const neighbor of graph[node]) {
      if (!visited.has(neighbor)) {
        visited.add(neighbor);   // mark visited when ENQUEUED, not when dequeued
        queue.push(neighbor);
      }
    }
  }
  return order;
}
⚠ Mark visited on enqueue, not on dequeue If you wait to mark a node visited until you dequeue it, the same node can be pushed onto the queue multiple times by different neighbors before it's ever processed — wasted work, and on a graph with cycles it can blow up badly. Mark it the instant it's added to the queue.

Dry run: bfs(graph, "A") on the five-node graph above

StepDequeueNeighbors checkedNewly enqueuedQueue after
1AB, DB, D[B, D]
2BA, C, EC, E[D, C, E]
3DA, E(none — both already seen)[C, E]
4CB, E(none — both already seen)[E]
5EB, C, D(none — all already seen)[]

Visit order: A, B, D, C, E. D gets enqueued at step 1 even though it's processed after B and before C — that's the queue's FIFO order at work, not the order neighbors were listed in.

BFS with distances, and the path itself

The BFS above only lists nodes in visiting order. Most problems ask for something more: how many edges to reach a node, or which edges to take. Both come from a small change to the same loop, because of one fact: the node that first reaches next is node, which sits one layer closer to the source. So the distance of next is the distance of node plus one, and the parent of next is node. Set both at the moment you enqueue, the same moment you would mark it visited.

function bfsDistances(graph, source) {
  const dist = new Array(graph.length).fill(-1); // -1 means "not reached yet"
  dist[source] = 0;
  const queue = [source];
  for (let head = 0; head < queue.length; head++) {
    const node = queue[head];
    for (const next of graph[node]) {
      if (dist[next] === -1) {
        dist[next] = dist[node] + 1; // one edge further than the node that found it
        queue.push(next);
      }
    }
  }
  return dist;
}

The distance array does double duty. -1 means "not seen", so it replaces the visited set, and anything still -1 at the end is unreachable from the source. Time O(V + E), space O(V). The queue uses a moving head index in place of shift(): an array used as a queue with shift() can cost O(n) per removal on a long array, which would make the loop quadratic, whereas the head index makes each removal O(1) and keeps the array as a log of visited nodes.

To get the path, record who found each node. Following those parent links from the target back to the source gives the path in reverse.

function shortestPath(graph, source, target) {
  const parent = new Array(graph.length).fill(-2); // -2 = unseen, -1 = the source has no parent
  parent[source] = -1;
  const queue = [source];
  for (let head = 0; head < queue.length && parent[target] === -2; head++) {
    const node = queue[head];
    for (const next of graph[node]) {
      if (parent[next] === -2) {
        parent[next] = node; // remember who discovered it
        queue.push(next);
      }
    }
  }
  if (parent[target] === -2) return null; // never reached
  const path = [];
  for (let at = target; at !== -1; at = parent[at]) path.push(at); // walk back from the target
  return path.reverse();
}

Dry run: shortestPath(graph, A, C) on the five-node graph, with A to E as 0 to 4

The adjacency lists are A:[B, D], B:[A, C, E], C:[B, E], D:[A, E], E:[B, C, D]. The loop stops as soon as the target has a parent.

DequeueNewly found (distance, parent)Queue after
AB (1, A), D (1, A)B, D
BC (2, B), E (2, B)D, C, E — C now has a parent, so the loop ends

Walking back from C: C's parent is B, B's parent is A, A's parent is -1, so the reversed list is [A, B, C], which has two edges, matching distance 2. Notice that E also has distance 2 (through B) even though the loop stopped before processing it.

⚠ What this does and does not promise The path is a shortest one in number of edges. Several can tie (here A to E is [A, B, E], and A, D, E is just as short), and which one you get depends on the order of the neighbour lists. If the problem asks for all shortest paths, or the lexicographically smallest, you need more than one parent per node. And BFS counts edges, not weights. On a weighted graph the fewest-edge path is not the cheapest one; that is the job of Dijkstra's algorithm in Advanced graph algorithms. Stopping as soon as the target is discovered, as the loop does, is safe because a node's parent and distance never change once they are set. And when the source equals the target the answer is the one-node path [source], which the code above returns.

We checked bfsDistances and shortestPath against a plain relaxation search on thousands of random directed and undirected graphs: the distances matched, and every returned path starts at the source, ends at the target, uses real edges and has exactly the shortest length.

Watch both traversals

BFS and DFS on the same graph, starting from A

Queue
Visit order

Disconnected graphs: the outer loop

Everything above grows from one start node, so it can only reach that node's component, the set of nodes joined to it by some path. If the graph falls into several pieces, one traversal sees one piece. Any problem about the whole graph ("visit everything", "count the pieces", "is there a cycle anywhere") therefore needs a loop around the traversal: try every node as a start, and skip the ones already reached.

function componentLabels(graph) {
  const label = new Array(graph.length).fill(-1);
  let count = 0;
  for (let start = 0; start < graph.length; start++) {
    if (label[start] !== -1) continue; // an earlier start already reached it
    label[start] = count;
    const queue = [start];
    for (let head = 0; head < queue.length; head++) {
      for (const next of graph[queue[head]]) {
        if (label[next] === -1) {
          label[next] = count;
          queue.push(next);
        }
      }
    }
    count++;
  }
  return { label, count };
}

Dry run: 6 nodes, edges 0-1, 1-2, 3-4, node 5 alone

StartAlready labelled?ReachesLabel givencount after
0no0, 1, 201
1yes (0)——1
2yes (0)——1
3no3, 412
4yes (1)——2
5no523

The result is label = [0, 0, 0, 1, 1, 2] and count = 3. The traversal inside the loop starts at most once per node and looks at each edge a constant number of times in total, so the loop does not multiply the cost: the whole function is O(V + E), with O(V) space. A graph is connected exactly when count is 1. Checked on thousands of random graphs against a union-find count, and every edge joins two nodes with the same label.

⚠ Where the outer loop is forgotten, and where it lies Running one BFS from node 0 and reporting "visited everything" or "no cycle" is correct only if the graph is connected. Nodes with no edges are components too, which is why the loop runs over graph.length and not over the edge list. And on a directed graph the loop does not find "components" in the connected sense: starting from nodes in index order, a later start can reach nodes that an earlier start already labelled. For directed graphs the useful questions are reachability, cycles and ordering, which the next chapters cover. Counting pieces with this loop is the first problem in Graph problems.

DFS vs BFS — the complexity is identical, the use case isn't

DFSBFS
TimeO(V + E)O(V + E)
SpaceO(V) — recursion stack or explicit stackO(V) — the queue, can hold a whole "ring"
Finds shortest path (unweighted)?noyes — guaranteed
Natural for"does a path exist," cycle detection, backtracking-style explorationshortest path, "closest," level-by-level problems
Say it like this → "Both visit every node and edge once, so they're both O(V + E) — the choice isn't about speed, it's about the guarantee I need. BFS explores in strict distance order, so it's the only one of the two that guarantees the first time I reach a node is via a shortest path."

Clone graph: copying a structure that has cycles

Given one node of a connected undirected graph, build a deep copy: new node objects with the same values and the same connections, sharing nothing with the original. Copying a tree is a simple recursion: copy the node, copy each child. On a graph that same recursion never ends, because a node's neighbour has that node as its neighbour, so the copy of A asks for a copy of B, which asks for a copy of A, and so on.

The fix is the same one that makes DFS finish: remember what you have already done. But here "done" is not enough, because you must also be able to get the copy back when you meet the node again, so the Set of visited nodes becomes a Map from each original to its copy. The one rule that matters is when you write to it: before you recurse into the neighbours, not after.

class GraphNode {
  constructor(val) {
    this.val = val;
    this.neighbors = [];
  }
}

function cloneGraph(node) {
  if (node === null) return null;
  const copies = new Map(); // original node -> its copy
  function copy(original) {
    if (copies.has(original)) return copies.get(original);
    const clone = new GraphNode(original.val);
    copies.set(original, clone); // register BEFORE visiting neighbours: this is what stops a cycle
    for (const neighbor of original.neighbors) clone.neighbors.push(copy(neighbor));
    return clone;
  }
  return copy(node);
}

Dry run: the four-node cycle 1-2-3-4-1, called on node 1

CallIn the map?ActionMap after
copy(1)nomake 1', register it, go through neighbours 2 and 41→1'
copy(2)nomake 2', register it, go through neighbours 1 and 31→1', 2→2'
copy(1) againyesreturn 1' at once, no recursionunchanged
copy(3)nomake 3', register, neighbours 2 and 4…, 3→3'
copy(2) againyesreturn 2'unchanged
copy(4)nomake 4', register, go through neighbours 3 and 1…, 4→4'
copy(3) again, copy(1) again (from 4)yes, bothreturn 3' and 1' at once, so 4' gets neighbours [3', 1']unchanged
copy(4) again (from 1)yesreturn 4', so 1' gets neighbours [2', 4']unchanged

Every node is created once, and every edge is followed once from each end, so the time is O(V + E) and the map and the recursion use O(V) space. The recursion depth is up to V, so the warning about call stacks applies here too: with a very long chain the same algorithm needs a BFS or an explicit stack. We tested it on thousands of random graphs, including self-loops, checking that the copy has the same shape, that no node object is shared with the original, and that every neighbour of a copy is itself a copy.

⚠ Four ways clone graph goes wrong 1. Registering the copy after the neighbour loop: the recursion goes round the cycle before anything is in the map, and ends in a stack overflow. 2. Keying the map by node.val when values might repeat; key by the node object itself, since a Map can use objects as keys. 3. Sharing the neighbour array instead of building a new one: clone.neighbors = original.neighbors would point the copy back into the original graph. 4. Forgetting the empty input: cloneGraph(null) must return null.

The idea behind it is wider than this one problem. When a traversal must produce a result for every node (a copy, a label, a distance), the visited set grows into a map from node to result, and you fill the entry before going deeper. You have just seen it three times: the distance array, the label array, and the copy map.

A grid is a graph you never build

A grid problem hands you no edge list, but it is a graph all the same: each cell is a node, and two cells are joined when you can step from one to the other. You do not need to build an adjacency list. A cell's neighbours are computed on the spot from its row and column, and a cell is recorded as visited in a second grid, a set of strings or a set of numbers such as row * columns + column. A graph whose edges are computed rather than stored is an implicit graph, and everything in this chapter (the queue, the distances, the visited marking on enqueue) works on it unchanged.

The details of grids, the four-direction arrays, the boundary checks, flood fill, several sources at once and "reach from the border" problems, as well as searching over states that are not cells, such as word ladder, have their own chapter: BFS on grids and implicit graphs. The component-counting and cycle problems that sit on top of plain graphs are in Graph problems.

Recognizing it in an unseen problem

  • Input described as nodes/edges, a grid (adjacent cells = edges), or "connections between X and Y"
  • "Shortest path," "fewest steps," "minimum number of moves" on an unweighted graph → BFS
  • "Does a path exist," "all paths," "explore every option" → DFS
  • A 2D grid where you move up/down/left/right is a graph in disguise — each cell is a node, each valid move is an edge
  • "How many edges," "what is the route," "print the path": BFS with a distance and a parent per node
  • "How many groups," "is everything connected," "does a cycle exist anywhere": a traversal inside a loop over every node
  • "Copy this structure" when nodes can point back at each other: a map from each original to its copy, filled before you recurse
  • A chain, a deep tree or a huge all-land grid under a recursive DFS: say that the call stack may run out, and switch to an explicit stack or BFS
Before you move on
  • Build an adjacency list from an edge list, and say when you'd reach for a matrix instead.
  • Write both recursive and iterative DFS, and explain what the call stack was doing that the explicit stack now does.
  • Write BFS, and explain out loud why marking a node visited on enqueue (not dequeue) matters.
  • State which of DFS or BFS guarantees the shortest path on an unweighted graph, and why the other one doesn't.
  • Recognize a 2D grid as a graph in disguise, and say what a "node" and an "edge" are in that disguise.
  • Convert an edge list to a weighted adjacency list, an adjacency matrix and back, and name the five build-step slips (shared fill([]), 1-indexed nodes, one-way edges, lost isolated nodes, self-loops).
  • Explain why a recursive DFS can throw a RangeError on a long chain, and write the iterative version that keeps a node and a neighbour index per frame.
  • Write BFS that returns the distance to every node and the actual path, using a parent per node, and say what it does not promise on a weighted graph.
  • Wrap a traversal in a loop over all nodes to handle a disconnected graph, and say why a directed graph needs different questions.
  • Write clone graph and say why the copy goes into the map before the recursion.

13Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 8 testsNumber of Islandsintermediate
  • 8 testsMax Area of Islandintermediate
  • 8 testsClone Graphintermediate
  • 8 testsRotting Orangesintermediate
  • 8 testsSurrounded Regionsintermediate
  • 10 testsEdge List to Adjacency Listbeginner
  • 8 testsIs There a Path Between Two Nodesbeginner
  • 9 testsShortest Path in an Unweighted Graphintermediate
  • 9 testsLabel Connected Componentsintermediate
  • 9 testsDepth-First Order Without Recursionintermediate
  • 9 testsAll Nodes at Distance Kintermediate
  • 8 testsFind the Town Judgebeginner
  • 8 testsNumber of Connected Components in an Undirected Graphintermediate
All 318 for this level →

✓Check yourself

5 questions, pass at 4. The check runs in your browser and the result stays on this device, so it is a self-check and not a certificate.

I5

Finished reading?

0 of 48 chapters read in this series

Mark as read

→Next stop

  1. I4Previous · I4Heaps & priority queues
  2. I5you are here
  3. Next · I6BFS on grids and implicit graphsA grid, a word list and a lock are all the same graph: states, a rule for the next states, and a queue that finishes one distance before starting the next.42 min read

01On this page

13 sections · 0%

  1. 01A graph is nodes plus connections, nothing more
  2. 02Two ways to store one, and when each wins
  3. 03Weighted, directed and labelled graphs
  4. 04DFS — go deep, backtrack when stuck
  5. 05DFS and the call stack: when recursion runs out
  6. 06BFS — spread outward, layer by layer
  7. 07BFS with distances, and the path itself
  8. 08Watch both traversals
  9. 09Disconnected graphs: the outer loop
  10. 10DFS vs BFS — the complexity is identical, the use case isn't
  11. 11Clone graph: copying a structure that has cycles
  12. 12A grid is a graph you never build
  13. 13Recognizing it in an unseen problem
Mark as read

[] chapters · t top