Last modified: December 05, 2024

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

Input & Output
Containers and Data Structures
Algorithm Steps

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

II. For each vertex v in the graph:

Step by Step Example

Imagine we have the same graph with vertices A, B, C, D, and E. The edges and weights are:

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

The adjacency matrix for the graph is:

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

Dijkstra's algorithm starting from A would proceed as follows:

I. Initialize the shortest paths from A to all other nodes as infinite (∞) and A to A as 0.

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

II. Start with A. Update all its neighbors:

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

III. Pick the smallest unvisited vertex, which is C. Update its neighbors:

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

IV. Next smallest unvisited vertex is B. Update its neighbors:

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

V. Next smallest unvisited vertex is D. Update its neighbors:

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

VI. E remains, but no update is possible.

Final shortest paths from A:

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

In the adjacency matrix, the shortest path distances from A can be represented as:

A [0]
B [3]
C [2]
D [8]
E [10]

Optimizing Time Complexity

While the basic implementation of Dijkstra's algorithm runs in O(n^2) time, its time complexity can be significantly reduced using a priority queue. By leveraging the queue to extract the vertex with the minimum distance, the complexity becomes O((V+E) log V) for a graph with V vertices and E edges.

Applications
Implementation

Bellman-Ford Algorithm

Input & Output
Containers and Data Structures
Algorithm Steps

I. Initialize distances[A] = 0 for the starting vertex and distances[v] = ∞ for all other vertices.

II. Repeat V-1 times (where V is the number of vertices):

III. For each edge (u, v) with weight w:

Step by Step Example

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

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

The adjacency matrix for the graph would be:

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

Now, let's run Bellman-Ford algorithm starting from vertex A:

Initialization:

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

For each vertex, update the distance to every other vertex.

I. Iteration 1:

Based on A's neighbors:

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

II. Iteration 2:

Based on B's neighbors:

dist[C] = 7 (No change)
dist[D] = 6 - 4 = 2
dist[E] = 11

Based on C's neighbors:

dist[E] = 7 - 3 = 4

III. Iteration 3:

Based on D's neighbors:

dist[A] = 2 + 2 = 4
dist[C] = 2 + 7 = 9 (But C's distance is already 7, so no change)

IV. Iteration 4:

No changes 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

Input & Output
Algorithm 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.

Used Data Structures
Step by Step Example

Suppose we have a graph with vertices A, B, C, D, and E. The edges and weights are:

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

Additionally, let's assume we have the following heuristic values estimating the distance from each node to the target node E:

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

The adjacency matrix for the graph would look like:

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

Now, let's run the A* algorithm starting from vertex A with the goal of reaching vertex E:

Initialization:

g(A) = 0
f(A) = g(A) + h(A) = 0 + 3 = 3

OpenList = [A]
ClosedList = []

While OpenList is not empty:

    Choose the node n in the OpenList with the lowest f(n).
    If n is the goal node, we're done.
    Otherwise, move n from the OpenList to the ClosedList.

I. Expand Node A:

A -> B : f(B) = g(A) + cost(A, B) + h(B) = 0 + 1 + 2 = 3
A -> C : f(C) = g(A) + cost(A, C) + h(C) = 0 + 2 + 2 = 4
The node with the lowest f value is B. So, expand B next.

II. Expand Node B:

B -> D : f(D) = g(B) + cost(B, D) + h(D) = 1 + 3 + 1 = 5
Now, C has the lowest f value. So, expand C next.

III. Expand Node C:

C -> D : f(D) = g(C) + cost(C, D) + h(D) = 2 + 2 + 1 = 5 (No improvement on the path to D)

IV. Expand Node D:

D -> E : f(E) = g(D) + cost(D, E) + h(E) = 5 + 1 + 0 = 6
E is the goal node. The algorithm stops here.

The path found by A* is: A -> B -> D -> E with a total cost of 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

Input & Output
Containers and Data Structures
Algorithm Steps

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

II. While there are nodes not yet included in the MST:

III.. The MST is formed using the parent[] array.

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 would look like:

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

Now, let's run Prim's algorithm starting from vertex A:

Initialization:

Chosen vertex: A
Vertices not included: B, C, D, E

Step-by-Step Execution:

I. Starting from vertex A, the closest vertex is B with a weight of 2.

Chosen vertices: A, B
Vertices not included: C, D, E

II. From the chosen vertices A and B, the closest vertex is D (from B) with a weight of 1.

Chosen vertices: A, B, D
Vertices not included: C, E

III. Continuing from the chosen vertices, the closest vertex is E (from D) with a weight of 2.

Chosen vertices: A, B, D, E
Vertices not included: C

IV. From the chosen vertices, the closest remaining vertex is C (from A) with a weight of 3.

Chosen vertices: A, B, D, E, C
And with that, all vertices have been included in the Minimum Spanning Tree (MST) using Prim's algorithm.

The edges selected by Prim's algorithm in this case are: A-B, B-D, D-E, and A-C, with a total weight of 8.

Special Characteristics
Applications
Implementation

Kruskal's Algorithm

Input & Output
Containers and Data Structures
Algorithm Steps

I. Sort all the edges in increasing order based on their weights.

II. Initialize an empty forest (a set of trees).

III. Iterate through the sorted edges. For each edge (u, v):

IV. The forest formed after processing all edges is the MST.

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

Here is the adjacency matrix for the graph:

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

Kruskal's algorithm sorts all the edges in ascending order and starts picking them from the smallest, ensuring that a cycle isn't formed.

Step-by-Step Execution:

I. Sort all edges:

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

II. Pick the smallest edge, B-D with a weight of 1.

Included edges: B-D
Total weight: 1

III. The next edge, A-B with a weight of 2, does not form a cycle. Include it.

Included edges: B-D, A-B
Total weight: 3

IV. The edge D-E with a weight of 2 is chosen next and does not form a cycle.

Included edges: B-D, A-B, D-E
Total weight: 5

V. The edge A-C with a weight of 3 does not form a cycle. Include it.

Included edges: B-D, A-B, D-E, A-C
Total weight: 8

VI. The next edge, B-E would form a cycle with the previously chosen edges, so it's skipped.

VII. Continuing, C-D would also form a cycle, so it's skipped.

VIII. The edge C-E would also form a cycle. At this point, all the vertices are connected, so the algorithm terminates.

The final Minimum Spanning Tree formed by Kruskal's algorithm includes the edges: B-D, A-B, D-E, and A-C with a total weight of 8.

Special Characteristics
Applications
Implementation

Table of Contents

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