Last modified: January 19, 2025

This article is written in: 🇺🇸

Graphs

In many areas of life, we come across systems where elements are deeply interconnected—whether through physical routes, digital networks, or abstract relationships. Graphs offer a flexible way to represent and make sense of these connections.

Some real-world examples include:

Viewing these systems as graphs allows for deeper analysis, better optimization, and more accurate predictions of their behavior. In computer science and mathematics, graphs serve as a powerful data structure that provides the tools needed for this kind of modeling.

Graph Terminology

Graph theory has its own language, full of terms that make it easier to talk about and define different types of graphs and their features. Here's an explanation of some important terms:

Representation of Graphs in Computer Memory

Graphs, with their versatile applications in numerous domains, necessitate efficient storage and manipulation mechanisms in computer memory. The choice of representation often depends on the graph's characteristics, such as sparsity, and the specific operations to be performed. Among the various methods available, the adjacency matrix and the adjacency list are the most prevalent.

Adjacency Matrix

An adjacency matrix represents a graph $G$ as a two-dimensional matrix. Given $V$ vertices, it utilizes a $V \times V$ matrix $A$. The rows and columns correspond to the graph's vertices, and each cell $A_{ij}$ holds:

For graphs with edge weights, $A_{ij}$ contains the weight of the edge between vertices $i$ and $j$.

Example:

A B C D
A 0 1 0 1
B 1 0 1 0
C 0 1 0 1
D 1 0 1 0

Here, the matrix indicates a graph with vertices A to D. For instance, vertex A connects with vertices B and D, hence the respective 1s in the matrix.

Benefits:

Drawbacks:

Adjacency List

An adjacency list uses a collection (often an array or a linked list) to catalog the neighbors of each vertex. Each vertex points to its own list, enumerating its direct neighbors.

Example:

A -> [B, D]
B -> [A, C]
C -> [B, D]
D -> [A, C]

This list reflects the same graph as our matrix example. Vertex A's neighbors, for instance, are B and D.

Benefits:

Drawbacks:

In practice, the choice between these (and other) representations often hinges on the graph's characteristics and the specific tasks or operations envisioned.

Planarity

Planarity is a fundamental concept in graph theory that examines whether a graph can be drawn on a flat surface without any of its edges overlapping. This idea holds significant importance in areas such as circuit design, urban planning, and geography.

What is a Planar Graph?

A graph is considered "planar" if there exists a representation of it on a two-dimensional plane where its edges intersect only at their vertices and nowhere in between. Being able to redraw a graph without any edges crossing, even if it starts out messy with overlaps, is what shows if it’s planar or not.

Planar Embedding

A "planar embedding" refers to a way of drawing a graph on a plane so that none of its edges cross each other. If a graph is first drawn with overlapping edges, it’s considered planar if you can rearrange it so no edges cross.

Examples

To understand planarity better, let's explore some examples:

  1. Cycle Graphs: Graphs like triangles, squares, or hexagons, which are simple loops or cycles, are intrinsically planar. These shapes can always be illustrated without intersecting edges.

A-----B
|     |
C-----D

  1. Complete Graph with Four Vertices ($K_4$): This graph has every vertex connected to all other vertices. Despite its complexity, $K_4$ remains planar and resembles a tetrahedron.

#
   A--------
  / \      |
 B---C     |
  \ /      |
   D--------

  1. Complete Graph with Five Vertices ($K_5$): Unlike $K_4$, $K_5$ cannot be sketched without crossing edges, thus classifying it as non-planar.

#
      -  A  -
    /  /   \  \
  /   |     |   \
 B----+-----+---C
  \   |     |  /
    \ D-----E /

In the $K_5$ graph, edges like AD and AE overlap with BC.

Strategies for Assessing Planarity

Traversals

Breadth-First Search (BFS)

Breadth-First Search (BFS) is a fundamental graph traversal algorithm that explores the vertices of a graph in layers, starting from a specified source vertex. It progresses by visiting all immediate neighbors of the starting point, then the neighbors of those neighbors, and so on.

To efficiently keep track of the traversal, BFS employs two primary data structures:

Algorithm Steps
  1. Begin from a starting vertex, $i$.
  2. Mark the vertex $i$ as visited.
  3. Explore each of its neighbors. If the neighbor hasn't been visited yet, mark it as visited and enqueue it in unexplored.
  4. Dequeue the front vertex from unexplored and repeat step 3.
  5. Continue this process until the unexplored queue becomes empty.

To ensure the algorithm doesn't fall into an infinite loop due to cycles in the graph, it could be useful to mark nodes as visited as soon as they are enqueued. This prevents them from being added to the queue multiple times.

Example

Queue: Empty          Visited: A, B, C, D, E

   A
  / \
 B   C
 |   |
 D   E

In this example, BFS started at the top of the graph and worked its way down, visiting nodes in order of their distance from the starting node. The ASCII representation provides a step-by-step visualization of BFS using a queue and a list of visited nodes.

Applications

BFS is not only used for simple graph traversal. Its applications span multiple domains:

  1. BFS can determine the shortest path in an unweighted graph from a source to all other nodes.
  2. To find all connected components in an undirected graph, you can run BFS on every unvisited node.
  3. BFS mirrors the propagation in broadcasting networks, where a message is forwarded to neighboring nodes, and they subsequently forward it to their neighbors.
  4. If during BFS traversal, an already visited node is encountered (and it's not the parent of the current node in traversal), then there exists a cycle in the graph.
Implementation

Depth-First Search (DFS)

Depth-First Search (DFS) is another fundamental graph traversal algorithm, but unlike BFS which traverses level by level, DFS dives deep into the graph, exploring as far as possible along each branch before backtracking.

To implement DFS, we use two main data structures:

Algorithm Steps
  1. Begin from a starting vertex, $i$.
  2. Mark vertex $i$ as visited.
  3. Visit an unvisited neighbor of $i$, mark it as visited, and move to that vertex.
  4. Repeat the above step until the current vertex has no unvisited neighbors.
  5. Backtrack to the previous vertex and explore other unvisited neighbors.
  6. Continue this process until you've visited all vertices connected to the initial start vertex.

Marking nodes as visited as soon as you encounter them is important to avoid infinite loops, particularly in graphs with cycles.

Example

Stack: Empty          Visited: A, B, D, C, E

   A
  / \
 B   C
 |   |
 D   E

In this example, DFS explored as deep as possible along the left side (branch with B and D) of the graph before backtracking and moving to the right side (branch with C and E). The ASCII representation provides a step-by-step visualization of DFS using a stack and a list of visited nodes.

Applications

DFS, with its inherent nature of diving deep, has several intriguing applications:

  1. Topological Sorting is used in scheduling tasks, where one task should be completed before another starts.
  2. To find all strongly connected components in a directed graph.
  3. DFS can be employed to find a path between two nodes, though it might not guarantee the shortest path.
  4. If during DFS traversal, an already visited node is encountered (and it's not the direct parent of the current node in traversal), then there's a cycle in the graph.
Implementation

Shortest paths

A common task when dealing with weighted graphs is to find the shortest route between two vertices, such as from vertex $A$ to vertex $B$. Note that there might not be a unique shortest path, since several paths could have the same length.

Dijkstra's Algorithm

Algorithm Steps

Input

Output

Containers and Data Structures

Steps

I. Initialize distances[A] to 0

II. Initialize distances[v] to for every other vertex v

III. While not all vertices are marked as finished

Step by Step Example

Consider a graph with vertices A, B, C, D, and E, and edges:

A-B: 4
A-C: 2
C-B: 1
B-D: 5
C-D: 8
C-E: 10
D-E: 2

The adjacency matrix looks like this (∞ means no direct edge):

A B C D E
A 0 4 2
B 4 0 1 5
C 2 1 0 8 10
D 5 8 0 2
E 10 2 0

Starting from A, here’s how Dijkstra’s algorithm proceeds:

I. Initialize all distances with ∞ except A=0:

A: 0
B: ∞
C: ∞
D: ∞
E: ∞

II. From A (distance 0), update neighbors:

A: 0
B: 4  (via A)
C: 2  (via A)
D: ∞
E: ∞

III. Pick the smallest unvisited vertex (C with distance 2). Update its neighbors:

A: 0
B: 3  (via C)
C: 2
D: 10 (via C)
E: 12 (via C)

IV. Pick the next smallest unvisited vertex (B with distance 3). Update its neighbors:

A: 0
B: 3
C: 2
D: 8  (via B)
E: 12

V. Pick the next smallest unvisited vertex (D with distance 8). Update its neighbors:

A: 0
B: 3
C: 2
D: 8
E: 10 (via D)

VI. The only remaining vertex is E (distance 10). No further updates are possible.

Final shortest paths from A:

A: 0
B: 3
C: 2
D: 8
E: 10

Optimizing Time Complexity
Applications
Implementation

Bellman-Ford Algorithm

Algorithm Steps

Input

Output

Containers and Data Structures

Steps

I. Initialize distances[A] to 0 and distances[v] to for all other vertices v

II. Repeat V - 1 times

III. Check for negative cycles by iterating over all edges (u, v) again

Step by Step Example

We have vertices A, B, C, D, and E. The edges and weights (including a self-loop on E):

A-B: 6
A-C: 7
B-C: 8
B-D: -4
B-E: 5
C-E: -3
D-A: 2
D-C: 7
E-E: 9

Adjacency matrix (∞ means no direct edge):

A B C D E
A 0 6 7
B 0 8 -4 5
C 0 -3
D 2 7 0
E 9

Initialization:

dist[A] = 0
dist[B] = ∞
dist[C] = ∞
dist[D] = ∞
dist[E] = ∞

Iteration 1 (relax edges from A):

dist[B] = 6
dist[C] = 7

Iteration 2 (relax edges from B, then C):

dist[D] = 2        (6 + (-4))
dist[E] = 11       (6 + 5)
dist[E] = 4        (7 + (-3))  // C → E is better

Iteration 3 (relax edges from D):

dist[A] = 4        (2 + 2)
(No update for C since dist[C]=7 is already < 9)

Iteration 4:

No changes in this round

Final distances from A:

dist[A] = 0
dist[B] = 6
dist[C] = 7
dist[D] = 2
dist[E] = 4

Special Characteristics
Applications
Implementation

A* (A-Star) Algorithm

Algorithm Steps

Input

Output

Used Data Structures

I. g(n): The best-known cost from the start vertex to vertex n

II. h(n): The heuristic estimate from vertex n to the goal

III. f(n) = g(n) + h(n): The estimated total cost from start to goal via n

IV. openSet: Starting with the initial node, contains nodes to be evaluated

V. closedSet: Contains nodes already fully evaluated

VI. cameFrom: Structure to record the path taken

Steps

I. Add the starting node to the openSet

II. While the openSet is not empty

III. If the algorithm terminates without finding the goal, no path exists

Step by Step Example

We have a graph with vertices A, B, C, D, and E:

A-B: 1
A-C: 2
B-D: 3
C-D: 2
D-E: 1

Heuristic estimates to reach E:

h(A) = 3
h(B) = 2
h(C) = 2
h(D) = 1
h(E) = 0

Adjacency matrix (∞ = no direct path):

A B C D E
A 0 1 2
B 0 3
C 0 2
D 0 1
E 0

Initialization:

g(A) = 0
f(A) = g(A) + h(A) = 0 + 3 = 3
openSet = [A]
closedSet = []

Expand A:

f(B) = 0 + 1 + 2 = 3
f(C) = 0 + 2 + 2 = 4

Expand B next (lowest f=3):

f(D) = g(B) + cost(B,D) + h(D) = 1 + 3 + 1 = 5

Next lowest is C (f=4):

f(D) = g(C) + cost(C,D) + h(D) = 2 + 2 + 1 = 5 (no improvement)

Expand D (f=5):

f(E) = g(D) + cost(D,E) + h(E) = 5 + 1 + 0 = 6
E is the goal; algorithm stops.

Resulting path: A -> B -> D -> E with total cost 5.

Special Characteristics
Applications
Implementation

Minimal Spanning Trees

Suppose we have a graph that represents a network of houses. Weights represent the distances between vertices, which each represent a single house. All houses must have water, electricity, and internet, but we want the cost of installation to be as low as possible. We need to identify a subgraph of our graph with the following properties:

Such a subgraph is called a minimal spanning tree.

Prim's Algorithm

Algorithm Steps

Input

Output

Containers and Data Structures

Steps

I. Start with an arbitrary node as the initial MST node

II. While there are vertices not yet included in the MST

III. The MST is formed using the parent[] array once all vertices are included

Step by Step Example

Consider a simple graph with vertices A, B, C, D, and E. The edges with weights are:

A-B: 2
A-C: 3
B-D: 1
B-E: 3
C-D: 4
C-E: 5
D-E: 2

The adjacency matrix for the graph (using ∞ where no direct edge exists) is:

A B C D E
A 0 2 3
B 2 0 1 3
C 3 0 4 5
D 1 4 0 2
E 3 5 2 0

Run Prim's algorithm starting from vertex A:

I. Initialization

Chosen vertex: A  
Not in MST: B, C, D, E

II. Pick the smallest edge from A

Closest vertex is B with a weight of 2.  
MST now has: A, B  
Not in MST: C, D, E

III. From A and B, pick the smallest edge

Closest vertex is D (from B) with a weight of 1.  
MST now has: A, B, D  
Not in MST: C, E

IV. Next smallest edge from A, B, or D

Closest vertex is E (from D) with a weight of 2.  
MST now has: A, B, D, E  
Not in MST: C

V. Pick the final vertex

The closest remaining vertex is C (from A) with a weight of 3.  
MST now has: A, B, D, E, C

The MST includes the edges: A-B (2), B-D (1), D-E (2), and A-C (3), with a total weight of 8.

Special Characteristics
Applications
Implementation

Kruskal's Algorithm

Algorithm Steps

Input

Output

Containers and Data Structures

Steps

I. Sort all edges in increasing order of their weights

II. Initialize a forest where each vertex is its own tree

III. Iterate through the sorted edges

IV. Once V-1 edges have been added, the MST is complete

Step by Step Example

Consider a graph with vertices A, B, C, D, and E. The weighted edges are:

A-B: 2
A-C: 3
B-D: 1
B-E: 3
C-D: 4
C-E: 5
D-E: 2

The adjacency matrix (∞ indicates no direct edge):

A B C D E
A 0 2 3
B 2 0 1 3
C 3 0 4 5
D 1 4 0 2
E 3 5 2 0

Sort edges by weight:

B-D: 1
A-B: 2
D-E: 2
A-C: 3
B-E: 3
C-D: 4
C-E: 5

  1. Pick B-D (1): Include it. MST has {B-D}, weight = 1.
  2. Pick A-B (2): Include it. MST has {B-D, A-B}, weight = 3.
  3. Pick D-E (2): Include it. MST has {B-D, A-B, D-E}, weight = 5.
  4. Pick A-C (3): Include it. MST has {B-D, A-B, D-E, A-C}, weight = 8.
  5. Pick B-E (3): Would form a cycle (B, D, E already connected), skip.
  6. Pick C-D (4): Would form a cycle (C, D already connected), skip.
  7. Pick C-E (5): Would form a cycle as well, skip.

The MST edges are B-D, A-B, D-E, and A-C, total weight = 8.

Special Characteristics
Applications
Implementation

Table of Contents

    Graphs
    1. Graph Terminology
    2. Representation of Graphs in Computer Memory
      1. Adjacency Matrix
      2. Adjacency List
    3. Planarity
      1. What is a Planar Graph?
      2. Planar Embedding
      3. Examples
      4. Strategies for Assessing Planarity
    4. Traversals
      1. Breadth-First Search (BFS)
      2. Depth-First Search (DFS)
    5. Shortest paths
      1. Dijkstra's Algorithm
      2. Bellman-Ford Algorithm
      3. A* (A-Star) Algorithm
    6. Minimal Spanning Trees
      1. Prim's Algorithm
      2. Kruskal's Algorithm