A is the source and B is the destination. The edge permits A → B, but does not permit B → A.
Graphs: Represent, Traverse, Analyse
A graph describes objects and the connections between them. Learn how to store those connections, explore them systematically, and use traversal to answer questions about reachability, dependencies and critical vertices.
Notation: V and E denote the vertex and edge sets; in complexity expressions, they mean the numbers of vertices and edges. An arrow u → v permits travel from u to v. In every trace, Next performs one operation, Back restores the previous state, and the slider jumps to a chosen step.
11.1Graph foundations
Where graphs appear
Webpages are vertices; hyperlinks are edges.
Cities are vertices; roads are edges.
People are vertices; friendships are edges.
Entities are vertices; relationships are edges.
Directed, undirected, weighted, and unweighted
A and B are adjacent to each other.
Each edge carries a cost, distance, time, or another numerical weight.
Every edge is treated equally; path length is measured by the number of edges.
Sparse and dense graphs
Has relatively few edges, commonly E = O(V).
Has close to the maximum possible number of edges, so E = Θ(V²).
Degree of a vertex
- In an undirected graph,
deg(v)counts incident edge ends; a self-loop contributes two. - In a directed graph, in-degree counts incoming edges and out-degree counts outgoing edges.
- Total directed degree is in-degree plus out-degree.
Self-loop, path, and cycle
An edge that connects a vertex to itself.
A sequence of adjacent vertices with no repeated vertex, such as G → C → B → A → D → E.
A closed path: the start is repeated at the end, but no other vertex is repeated.
11.2Graph representations
A representation must preserve vertex relationships in memory. The two standard choices are an adjacency matrix and an adjacency list.
None or ∞
for “no edge.”
Build both representations edge by edge
Graph: 0→1, 0→4, 1→2, 1→3, 1→4, 2→3, 3→4.
Python: adjacency matrix
class GraphMatrix:
def __init__(self, vertices):
self.vertices = vertices
self.adj = [[0] * vertices for _ in range(vertices)]
def add_edge(self, u, v):
self.adj[u][v] = 1
def display(self):
for row in self.adj:
print(row)
g = GraphMatrix(5)
for u, v in [(0,1), (0,4), (1,2), (1,3),
(1,4), (2,3), (3,4)]:
g.add_edge(u, v)
g.display()
Python: adjacency list
class GraphList:
def __init__(self, vertices):
self.vertices = vertices
self.adj = {i: [] for i in range(vertices)}
def add_edge(self, u, v):
self.adj[u].append(v)
def display(self):
for vertex, neighbors in self.adj.items():
print(vertex, ":", neighbors)
| Representation | Space | Edge lookup | Best for |
|---|---|---|---|
| Adjacency matrix | O(V²) | O(1) | Dense graphs |
| Adjacency list | O(V + E) | O(deg(u)); O(V) worst case | Sparse graphs |
11.3Traversal: visit each reachable vertex
Unlike trees, graphs can represent cyclic road and computer networks.
Graphs can offer several routes between two vertices, enabling shortest-path problems.
Mutual friendships do not fit a strict parent–child hierarchy.
Course prerequisites and other networks may have multiple parents and connections.
- Visited (discovered): the vertex has been seen.
- Explored: all adjacent vertices of that vertex have been considered.
Applications include pathfinding, web crawling, social-network analysis, and network routing.
11.4Breadth-first search (BFS)
BFS explores the nearest vertices first. A first-in, first-out queue holds discovered vertices whose neighbours still need to be examined.
- Start: mark the source visited and put it in the queue.
- Process: remove the front vertex. Mark and enqueue each of its unvisited neighbours.
- Repeat: continue until the queue is empty.
BFS: discover vertices level by level
Directed graph; start at 0 and scan neighbours in the displayed adjacency-list order.
After processing 0, the queue contains 1, 2 and 3. Vertex 4 is first discovered from 1. The later edge 2 → 4 must not enqueue it again.
| Vertex | Distance | Parent |
|---|
View the Python implementation
from collections import deque
def bfs(graph, start):
visited = {start}
queue = deque([start])
order = []
while queue:
u = queue.popleft()
order.append(u)
for v in graph[u]:
if v not in visited:
visited.add(v)
queue.append(v)
return orderWhy BFS gives shortest paths
The queue processes distance d before distance d + 1. When v is first discovered from u, set distance[v] = distance[u] + 1 and parent[v] = u. Mark v immediately when enqueuing it, so two incoming edges cannot enqueue it twice. Following parent pointers backwards reconstructs a shortest path; for example, 0 → 1 → 4 → 6 has three edges.
The traversal order depends on neighbour order, but shortest-path distances do not. In the animation, an undiscovered vertex has distance ∞; the source has distance 0 and no parent.
Analysis
Initializing visited takes O(V). Across the traversal, adjacency-list scanning examines all vertices and edges once, giving O(V + E).
11.5Depth-first search (DFS)
DFS follows one route as far as possible before trying another. Recursion remembers where to continue: returning from a call resumes the caller’s neighbour scan.
- Discover: mark the current vertex visited and record it.
- Explore: scan its outgoing neighbours in order. Recurse only on an unvisited neighbour.
- Return: once every neighbour is checked, pop the call and resume its parent.
DFS: follow calls and returns
Follow every call, neighbour check and return. Only the top frame executes.
Follow 0 → 1 → 4 → 6. When 6 has no outgoing neighbour, its frame is popped. Calls 4 and 1 then finish, allowing 0 to continue with neighbour 2.
View neighbour order for this graph
| Vertex | Neighbours, in order |
|---|
View the Python implementation
def dfs(graph, node, visited=None, order=None):
if visited is None:
visited = set()
if order is None:
order = []
visited.add(node)
order.append(node)
for neighbor in graph[node]:
if neighbor not in visited:
dfs(graph, neighbor, visited, order)
return orderHow to read the call stack
- Local to each call
- u is the vertex being explored; v is the neighbour currently being checked. The frame also remembers which neighbour comes next.
- Shared by all calls
- The visited set and discovery order belong to the whole traversal. Returning from a call does not erase them.
- Suspended versus active
- Only the top frame executes. A parent waits at step 4 while its child runs, then resumes at step 2.
- Discovery versus completion
- A vertex enters the traversal order when first visited. Its frame is removed only after every outgoing neighbour has been checked.
Example: by the time DFS reaches 2, vertex 4 has already finished. The edge 2 → 4 is checked and skipped; it does not create another call.
Analysis and comparison
DFS uses O(V) space for visited plus up to O(V) recursive calls or explicit stack entries. Its time is O(V + E) with adjacency lists and O(V²) with an adjacency matrix.
| Feature | BFS | DFS |
|---|---|---|
| Approach | Level by level | Deep first, then backtrack |
| Data structure | Queue (FIFO) | Stack (LIFO) or recursion |
| Time with lists | O(V + E) | O(V + E) |
| Auxiliary space | O(V) | O(V) |
| Typical use | Unweighted shortest path | Maze solving, cycle detection |
11.6Topological sort and cycle detection
In a DFS implementation, insert each vertex at the front of the result when it finishes (equivalently, push it to a stack and reverse the finish stack).
DFS finish times → topological order
Watch vertices enter the finish stack, then leave in reverse order.
A vertex enters the finish stack after its descendants finish. The displayed order is obtained by popping that stack, not by copying DFS discovery order.
Finish stack versus call stack: the call stack contains unfinished calls and shrinks on return. The finish stack records vertices only after all their outgoing neighbours are finished. Reversing that finish order places every prerequisite before the vertices that depend on it. For a disconnected DAG, start DFS from each still-unvisited vertex before reversing the complete finish order.
Clothing example and DFS times
Constraints include undershorts → pants, undershorts → shoes, pants → belt, pants → shoes, socks → shoes, shirt → belt, shirt → tie, belt → jacket, and tie → jacket. The watch is independent.
1/8 means discovery time 1 and finish time 8.
Cycle detection using DFS
For a directed graph, an edge to an already-visited vertex is not enough to prove a cycle. The important question is whether that vertex’s call is still active.
| State of neighbour v | Meaning | Action for edge u → v |
|---|---|---|
| Unvisited (white) | DFS has not reached v. | Explore v recursively. |
| Active (grey) | v is on the current call stack. | A back edge closes a directed cycle. |
| Finished (black) | v’s call has returned. | This edge alone does not indicate a cycle. |
Directed cycle trace
The red back edge closes 1 → 4 → 6 → 5 → 2 → 1.
Homework: detect a cycle in an undirected graph
Run DFS with a parent parameter. If an adjacent vertex is unvisited, recurse. If it is already visited and is not the current vertex’s parent, a cycle exists.
def has_cycle(graph):
visited = set()
def visit(u, parent):
visited.add(u)
for v in graph[u]:
if v not in visited:
if visit(v, u): return True
elif v != parent:
return True
return False
return any(visit(u, None) for u in graph if u not in visited)
11.7Articulation points (cut vertices)
Let disc[u] be u’s discovery time and
low[u] the smallest discovery time reachable from u’s DFS
subtree using tree edges and at most one back edge.
| Vertex | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| Discovery time | 1 | 6 | 3 | 2 | 4 | 5 |
| Low value | 1 | 1 | 1 | 1 | 3 | 3 |
Remove a vertex and count components
The slide graph has edges 1–4, 1–2, 4–3, 2–3, 3–5, 3–6, and 5–6.
How discovery and low values change
disc[u] records when u is first visited. low[u] records the earliest discovery time reachable from u’s subtree without using the edge back to its parent.
- On entry: set disc[u] = low[u] to the next discovery time.
- After a child v returns: set low[u] = min(low[u], low[v]).
- At a visited non-parent neighbour v: set low[u] = min(low[u], disc[v]).
- Test for separation: for a non-root u, low[v] ≥ disc[u] means that child’s subtree cannot reach an ancestor of u. For the root, count DFS-tree children: more than one makes it a cut vertex.
Articulation points: follow the DFS decisions
Discover → explore → propagate low values → test for separation.
On small screens, scroll the graph sideways to inspect every vertex.
Algorithm steps and full vertex table
| Vertex | Parent | disc | low | Cut vertex? |
|---|
Second slide example
For the graph with edges G–C, C–B, C–D, B–A, A–D, D–F, F–E, and D–E, starting DFS at A gives discovery/low labels A 1/1, B 2/1, C 3/1, D 4/1, E 5/4, F 6/4, and G 7/7.
A graph with two cut vertices
The highlighted vertices separate the two leaf-side regions.
11.8Questions, applications, and shortest paths
Which DFS(a) sequences are valid?
Select a sequence. The exact graph from the slide will trace the proposed DFS path.
DFS sequence check
Start at a; adjacency-list order may vary.
Valid sequences are 1, 3, and 4. Sequence 2 fails after a → b → f: vertex f still has unvisited neighbors h and g, so DFS cannot backtrack and visit e next.
What can be the maximum DFS stack length?
Follow the exact numbered path from the slide and watch the recursion stack grow.
Maximum-depth DFS path
The two unnumbered vertices are not on this active recursion path.
On a small screen, scroll the graph horizontally to see all vertices.
Best representation for testing whether a vertex is isolated in an undirected graph
An adjacency list is convenient: the vertex is isolated exactly when its list is empty. With an adjacency matrix, its complete row must be checked. In a directed graph, isolation additionally requires both in-degree and out-degree to be zero.
Count connected components of an undirected graph using DFS
Scan all vertices. Whenever an unvisited vertex is found, start DFS and increment the component count. Complexity is O(V + E) with adjacency lists and O(V²) with an adjacency matrix. Directed graphs require a chosen notion of connectivity, such as weak or strong connectivity.
Detect strongly connected components using DFS
Kosaraju’s algorithm runs DFS to record finish order, transposes every directed edge, then runs DFS on the transpose in decreasing finish order. Each DFS tree in the second pass is one strongly connected component. Time: O(V + E).
Traversing a disconnected graph
Keep one visited set for the whole graph. Scan vertices in order; each unvisited vertex starts a new DFS tree and increases the component count. Returning from one DFS tree finishes only that component. An isolated vertex also forms a component.
One DFS tree per connected component
Scan A through F. The triangle, the pair and the isolated vertex form three components.
A new component is counted only when the next vertex is still unvisited. F is isolated, but its single DFS call still counts as one component.
Shortest paths: weighted versus unweighted graphs
BFS explores level by level. The first arrival uses the fewest edges, so BFS gives shortest paths.
BFS minimizes edge count, not total weight. Use Dijkstra’s algorithm when all edge weights are non-negative.
Choose the algorithm by the question
| Question | Method | What to remember |
|---|---|---|
| Fewest edges from a source? | BFS | Queue + distance + parent; edges must have equal cost. |
| Explore paths and backtrack? | DFS | Visited set + call stack. |
| Order tasks with dependencies? | Topological sort | Reverse DFS finish order; requires a DAG. |
| Does a directed graph contain a cycle? | DFS with active-state tracking | An edge to an active ancestor proves a cycle. |
| Which vertices can disconnect an undirected graph? | DFS with discovery and low values | Check each child subtree; handle the root separately. |
These methods take O(V + E) time with adjacency lists. Before coding, identify whether the graph is directed, whether edge weights matter, and whether one traversal reaches every vertex.