Graph Foundations, Structural Variants & BFS/DFS Traversals
A graph is a mathematical structure consisting of a finite set of vertices (nodes) and a set of edges (links) connecting pairs of vertices.
formal graph definitions & metrics.
Unlike linear containers where input size is governed by a single parameter , graph algorithms depend on two independent structural parameters: the vertex count and the edge count . Formally, in set-builder notation:
In an undirected graph, the edge relation is a symmetric binary relation (), whereas in a directed graph (digraph), is non-symmetric and edges are ordered pairs representing one-way arcs.
| CONCEPT | NOTATION | FORMAL DEFINITION |
|---|---|---|
| Order of a Graph | The cardinality of the vertex set ; the total number of nodes in . | |
| Size of a Graph | The cardinality of the edge set ; the total number of links in . | |
| Vertex Degree (Undirected) | The number of edges incident to vertex . In a simple graph, . By the Handshaking Lemma, . | |
| In-Degree & Out-Degree | In a directed graph, counts incoming directed edges (head at ) and counts outgoing edges (tail at ). | |
| Distance & Diameter | Distance is the shortest path length between and . The diameter is the maximum shortest-path distance over all vertex pairs: . If is disconnected, its diameter is . | |
| Transitive Closure | The reachability graph where edge exists if and only if there is a directed path of length from to in . |
graph variants, density & trees.
The structural constraints placed on determine the maximum possible number of edges, the formal Graph Density (), and which representation (Adjacency List vs Adjacency Matrix) is asymptotically superior.
- Simple Graph: Contains no self-loops and no parallel edges between the same pair of vertices. Maximum degree per node is .
- Multigraph: Permits multiple parallel edges between the same pair of vertices (and optionally self-loops).
- Complete Graph (): A simple undirected graph in which every pair of distinct vertices is connected by a unique edge (, ).
- Connected Graph: An undirected graph with a path between every pair of vertices ().
- Bridge (Cut-Edge): An edge whose removal increases the number of connected components in . An edge is a bridge iff it does not lie on any simple cycle.
- DAG (Directed Acyclic Graph): A directed graph containing no directed cycles. Used to model task dependencies, prerequisite chains, and version histories.
- Sparse Graph (): Approaches a tree (, typically ). Best stored as an Adjacency List using space.
- Dense Graph (): Approaches a complete graph (). Best stored as an Adjacency Matrix ( table) using space with edge lookup.
- Tree: A connected, acyclic undirected graph. Any tree with vertices has exactly edges, and every edge is a bridge.
- Spanning Tree: A subgraph of a connected graph that includes all vertices and forms a tree ( with ).
VCE Pseudocode Conventions & The 3 Core Control Structures
Before writing graph traversal algorithms, the HESS notes standardise VCE Algorithmics Pseudocode around three fundamental control structures, using <- for variable assignment, Algorithm for the main procedure, Function for helper subroutines, and Display / Return for output:
- 1. Sequence: Linear top-to-bottom execution of statements and assignments (
x <- 0). - 2. Selection: Conditional branching via
If ... Then ... Else if ... Else ... End if. - 3. Iteration: Unbounded loops (
While <cond> do ... End do/Do ... While <cond>) and bounded loops (For each x in S do ... End do).
Algorithm Mean(list):
sum <- 0
For each element in list do:
sum <- sum + element
End do
mean <- sum / length(list)
Return mean
End Algorithm breadth-first search (bfs).
Breadth-First Search (BFS) traverses a graph layer-by-layer outward from a source vertex using a FIFO Queue. Because it explores all vertices at distance before discovering any vertex at distance , BFS is guaranteed to find the shortest path in an unweighted graph (minimum number of edges).
- 3 Ways to Implement Node Marking (Visited State): To prevent infinite loops on cycles, visited vertices are marked using (1) a node attribute (e.g.,
v.visited = True), (2) a Boolean array indexed by vertex ID (visited[v] = True), or (3) a Hash Table / Set ADT (visited.add(v)). - BFS Distance-Labelling Procedure ( Shortest Unweighted Path): Set level counter and label source with . In each pass, find all unlabelled vertices adjacent to any vertex labelled , mark them with , and increment until target is marked.
- BFS Tree Edges & Back-Edges Reconstruct : The edges traversed to discover unvisited vertices form a spanning tree (the BFS Tree, or a BFS Forest if repeated across an outer loop
For each Node start_node in Gon a disconnected graph). Edges leading to already-visited vertices are back-edges (or cross-edges); adding all non-tree back-edges back to the BFS/DFS spanning tree recovers the exact original graph !
Algorithm BFS(G, root):
let Q be a queue
let T be a new Graph // BFS Tree
mark root as visited
Q.enqueue(root)
While Q is not empty do:
v <- Q.dequeue()
For each w in G.adjacent(v) do:
If w is not visited then:
mark w as visited
add edge (v, w) to T
Q.enqueue(w)
End If
End For
End While
Return T
End Algorithm import networkx as nx
from collections import deque
def bfs_tree(G: nx.Graph, startNode):
visited = set([startNode])
queue = deque([startNode])
T = nx.Graph()
T.add_node(startNode)
while queue:
v = queue.popleft()
for w in G.neighbors(v):
if w not in visited:
visited.add(w)
T.add_edge(v, w)
queue.append(w)
return T With an Adjacency List, every vertex is enqueued and dequeued at most once (), and every edge is inspected across the adjacency lists (), yielding time complexity and auxiliary space for the Queue and visited set.
depth-first search (dfs): iterative vs recursive.
Depth-First Search (DFS) plunges as deep as possible along each branch before backtracking. It can be written iteratively using an explicit LIFO Stack or recursively using the program call stack. In Python iterative DFS, calling neighbours.reverse() before pushing onto the stack ensures neighbours are popped and visited in the exact same natural order as recursive DFS. If the graph is disconnected, wrapping DFS in an outer loop For each Node start_node in G restarts traversal from unvisited vertices to produce a DFS Forest. Beyond basic traversal, DFS powers Topological Sorting (on DAGs), finding Strongly Connected Components, and testing -partiteness (such as 2-colouring bipartite graphs).
Algorithm DFS_Iterative(G, start):
let S be a stack
S.push(start)
While S is not empty do:
v <- S.pop()
If v is not visited then:
mark v as visited
For each w in G.adjacent(v) do:
If w is not visited then:
S.push(w)
End If
End For
End If
End While
End Algorithm Algorithm DFS_Recursive(G, v):
mark v as visited
For each u in G.adjacent(v) do:
If u is not visited then:
DFS_Recursive(G, u)
End If
End For
End Algorithm DFS Time Complexity: Best, Average & Worst Case (Adjacency List vs Matrix)
In VCE Algorithmics exams, deriving the exact complexity of DFS requires accounting for both how the graph is stored and its edge density:
| REPRESENTATION | BEST CASE (TREE: ) | AVERAGE CASE | WORST CASE (COMPLETE: ) |
|---|---|---|---|
| Adjacency List | (since ) | (when ) | |
| Adjacency Matrix |
- Why Adjacency List is : Initialising vertices takes . Iterating through
G.adjacent(v)across all visited vertices sums to . For a tree (), this simplifies to ; for a complete graph, . - Why Adjacency Matrix is Always : Finding the neighbours of each vertex requires scanning an entire matrix row of length regardless of how few edges actually has, costing in Best, Average, and Worst cases.
connectedness check & cycle detection.
Two essential applications of DFS in the HESS syllabus are testing whether an undirected graph is connected and detecting whether an undirected graph contains a cycle.
1. Graph Connectedness Check (Counting Visited Nodes)
Initialise a counter count <- 0 and run DFS (or BFS) starting from a single arbitrary vertex , incrementing count each time a vertex is marked visited. After that single pass terminates, check whether count == |V| (or equivalently, whether the outer loop For each Node start_node in G executes only once). If count == |V|, is connected; otherwise, is disconnected.
2. Undirected Cycle Detection (The ancestor Parameter)
In an undirected graph, every edge appears in both 's adjacency list and 's adjacency list. When DFS moves from parent to child , examining 's neighbours will immediately see as already visited! However, going immediately back to the parent is not a cycle.
Therefore, undirected DFS cycle detection must pass an ancestor (parent) parameter: a cycle exists if and only if DFS encounters an adjacent vertex that is already visited AND .
Algorithm HasCycle(G):
mark all vertices in G as unvisited
Function DFS_Cycle(v, ancestor):
mark v as visited
For each u in G.adjacent(v) do:
If u is not visited then:
If DFS_Cycle(u, v) == True then:
Return True
End If
Else If u != ancestor then:
// Visited neighbour that is NOT our immediate parent -> back-edge cycle!
Return True
End If
End For
Return False
End Function
For each v in G.vertices do:
If v is not visited then:
If DFS_Cycle(v, null) == True then:
Return True
End If
End If
End For
Return False
End Algorithm best-first search & review.
Best-First Search replaces the FIFO Queue of BFS and the LIFO Stack of DFS with a Priority Queue (open list) ordered by a heuristic evaluation function that estimates the desirability of node , alongside a closed list tracking visited nodes. In the HESS notes, typical heuristic factors combined into include Proximity (estimated distance to goal), Cost (resource expenditure), and Danger (risk penalty). While fast in practice, pure Greedy Best-First Search is not guaranteed to find the shortest path because it ignores the path cost already incurred to reach (which is corrected by Dijkstra's and A* Search in Lesson 03).
Algorithm Best_First_Search(G, start, f):
open <- PQueue(min or max) ADT
closed <- List
enqueue(open, start, f(start))
While open is not empty do:
current <- peek(open)
dequeue(open)
append(closed, current)
For each neighbour in Adjacent(G, current) do:
If neighbour not in closed and neighbour not in open then:
enqueue(open, neighbour, f(neighbour))
Else if neighbour in open then:
// Update priority if a better heuristic path is found
requeue(open, neighbour, f(neighbour))
End if
End do
End do
Return closed
End Algorithm Self-Check 1: Why does DFS cycle detection on an undirected graph fail if we omit the u != ancestor check?
In an undirected graph with even a single edge , starting DFS at marks visited and recurses on neighbour . When scans its adjacency list, it sees is already visited. Without checking A != ancestor, the algorithm falsely reports that the trivial two-node tree contains a cycle.
Self-Check 2: A simple undirected graph has 10 vertices and is disconnected into at least two components. What is the maximum number of edges it could possibly have?
To maximise edge count across two disconnected components of sizes and , make one component an isolated vertex (, 0 edges) and the other component a complete graph with edges. Adding a 37th edge would force the 10th vertex to connect, making the graph connected.