Nodes, edges and a visited set
Think of a city map: places connected by roads. In a graph, the places are nodes (or vertices) and the roads are edges. Roads can be one-way (a directed graph, like “course A is required before B”) or two-way (undirected, like friendships), and they can have lengths (weighted, like travel time).
Exploring a graph works like exploring a tree with DFS or BFS, with one important difference: a graph can have cycles (A → B → C → A). Without care, you’d walk in circles forever. So every graph search keeps a visited set: before going to a node, check whether you’ve been there.
Graph problems reduce to a handful of algorithms once the modeling is right: what are the nodes (cells, words, states), what are the edges (moves, one-letter changes, prerequisites), are they directed or weighted? BFS gives shortest paths in unweighted graphs, DFS explores components and detects cycles, topological sort orders DAGs, Dijkstra handles non-negative weights, and union-find answers connectivity incrementally. Everything is O(V + E) or close, provided each node is processed once, which is where the bugs live: visited sets marked at the wrong moment, and Dijkstra variants that finalize too early. The Detailed version covers the algorithms and those traps.
// Undirected graph: each person maps to their friends
let friends: [String: [String]] = [
"Ana": ["Ben", "Cleo"], "Ben": ["Ana", "Dan"], "Cleo": ["Ana", "Dan"],
"Dan": ["Ben", "Cleo", "Eve"], "Eve": ["Dan"],
]
/// Fewest hops from start to every reachable person.
func distances(from start: String, in graph: [String: [String]]) -> [String: Int] {
var distance = [start: 0] // doubles as the visited set
var queue = [start], head = 0
while head < queue.count {
let person = queue[head]; head += 1
for friend in graph[person, default: []] where distance[friend] == nil {
distance[friend] = distance[person]! + 1 // mark when discovered
queue.append(friend)
}
}
return distance
}
print(distances(from: "Ana", in: friends)["Eve"]!) // 3: Ana → Ben → Dan → Eve// Undirected graph: each person maps to their friends
val friends = mapOf(
"Ana" to listOf("Ben", "Cleo"), "Ben" to listOf("Ana", "Dan"), "Cleo" to listOf("Ana", "Dan"),
"Dan" to listOf("Ben", "Cleo", "Eve"), "Eve" to listOf("Dan"),
)
/** Fewest hops from start to every reachable person. */
fun distances(start: String, graph: Map<String, List<String>>): Map<String, Int> {
val distance = mutableMapOf(start to 0) // doubles as the visited set
val queue = ArrayDeque(listOf(start))
while (queue.isNotEmpty()) {
val person = queue.removeFirst()
for (friend in graph[person].orEmpty()) {
if (friend in distance) continue
distance[friend] = distance.getValue(person) + 1 // mark when discovered
queue.addLast(friend)
}
}
return distance
}
println(distances("Ana", friends)["Eve"]) // 3: Ana → Ben → Dan → Eve// Undirected graph: each person maps to their friends
Map<String, List<String>> friends = Map.of(
"Ana", List.of("Ben", "Cleo"), "Ben", List.of("Ana", "Dan"), "Cleo", List.of("Ana", "Dan"),
"Dan", List.of("Ben", "Cleo", "Eve"), "Eve", List.of("Dan"));
/** Fewest hops from start to every reachable person. */
static Map<String, Integer> distances(String start, Map<String, List<String>> graph) {
Map<String, Integer> distance = new HashMap<>(Map.of(start, 0)); // doubles as the visited set
Deque<String> queue = new ArrayDeque<>(List.of(start));
while (!queue.isEmpty()) {
String person = queue.pollFirst();
for (String friend : graph.getOrDefault(person, List.of())) {
if (distance.containsKey(friend)) continue;
distance.put(friend, distance.get(person) + 1); // mark when discovered
queue.addLast(friend);
}
}
return distance;
}
System.out.println(distances("Ana", friends).get("Eve")); // 3: Ana → Ben → Dan → Eve// Undirected graph: each person maps to their friends
const friends = new Map<string, string[]>([
["Ana", ["Ben", "Cleo"]], ["Ben", ["Ana", "Dan"]], ["Cleo", ["Ana", "Dan"]],
["Dan", ["Ben", "Cleo", "Eve"]], ["Eve", ["Dan"]],
]);
/** Fewest hops from start to every reachable person. */
function distances(start: string, graph: Map<string, string[]>): Map<string, number> {
const distance = new Map([[start, 0]]); // doubles as the visited set
const queue = [start];
for (let head = 0; head < queue.length; head++) {
const person = queue[head];
for (const friend of graph.get(person) ?? []) {
if (distance.has(friend)) continue;
distance.set(friend, distance.get(person)! + 1); // mark when discovered
queue.push(friend);
}
}
return distance;
}
console.log(distances("Ana", friends).get("Eve")); // 3: Ana → Ben → Dan → EveTwo habits in that code prevent most graph bugs:
- Mark a node as visited when you add it to the queue, not when you take it out. Otherwise the same node can be added many times by different neighbors before it’s processed.
- BFS finds shortest paths (fewest edges) because it explores everything one hop away, then two hops, and so on: the first time it reaches a node is the shortest way there.
Ways to store a graph
| Representation | Looks like | Good for |
|---|---|---|
| Adjacency list | Map<Node, List<Node>> | most problems: sparse graphs, iterating neighbors |
| Adjacency matrix | Boolean[n][n] | dense graphs, “is there an edge between a and b?” in O(1) |
| Edge list | List<(from, to, weight)> | algorithms that process edges (Kruskal, Bellman-Ford), input format |
| Implicit | neighbors computed on the fly | grids (4 or 8 directions), puzzles, word ladders |
Interview inputs often come as an edge list or a grid; the first step is usually building an adjacency list.
Predict the output
In the friends graph (Ana–Ben, Ana–Cleo, Ben–Dan, Cleo–Dan, Dan–Eve), BFS from Ana visits people in which order, if neighbors are visited in the listed order?
BFS visits by distance: Ana (0), then everyone one hop away in listed order, Ben and Cleo (1), then Dan (2, discovered from Ben; when Cleo is processed, Dan is already visited), then Eve (3). The first option is a depth-first order, which follows Ben → Dan → Eve before backtracking to Cleo.
The algorithms behind most graph questions
| Problem | Algorithm | Complexity |
|---|---|---|
| Shortest path, unweighted | BFS | O(V + E) |
| Shortest path, weights 0 or 1 | 0-1 BFS with a deque | O(V + E) |
| Shortest path, non-negative weights | Dijkstra with a binary heap | O((V + E) log V) |
| Negative weights, negative cycles | Bellman-Ford | O(V · E) |
| Order with prerequisites, cycle check in a DAG | Topological sortTopological sortAn order of a directed graph's nodes where every edge points forward, used for dependencies.Open in glossary → (Kahn’s in-degrees, or DFS post-order) | O(V + E) |
| Connectivity under added edges, cycles in undirected graphs | Union-find with path compression and union by rank | ~O(α(n)) per operation |
| Minimum spanning tree | Kruskal (sort edges + union-find) or Prim | O(E log E) |
Cycle detection differs by graph type: in an undirected graph, an edge to a visited node that isn’t the parent is a cycle; in a directed graph, you need three states (unvisited, in progress, done), and an edge to an in-progress node is a cycle.
| Need | Tool |
|---|---|
| Explore / components / islands | DFS or BFS + visited (mark on discovery) |
| Fewest edges | BFS; multi-source BFS for “nearest of many” |
| Weighted, non-negative | Dijkstra: finalize on pop, skip stale entries |
| Ordering with dependencies, DAG check | Kahn’s topological sort (in-degrees) |
| Directed cycle detection | DFS with 3 states (unvisited, in progress, done) |
| Dynamic connectivity | Union-find (path compression + union by rank) |
Complexities: BFS/DFS/topo O(V + E), Dijkstra O((V + E) log V). Gotchas: no visited set (infinite loops), marking visited on dequeue in BFS (duplicates), marking visited on push in Dijkstra (wrong distances), BFS on weighted graphs, recursion depth on big grids, Swift arrays with removeFirst() (or TypeScript shift()) as queues.
Check this part
Why does every graph search need a visited set?
Which search finds the path with the fewest edges?
In BFS, when should a node be marked visited?
How do you detect a cycle in a directed graph with DFS?