Basic MathGuides

Graph Theory Explained: The Backbone of Modern Networks

Graph Theory: Comprehensive Notes

1. Introduction to Graph Theory

Graph theory is a branch of mathematics that studies the properties of graphs, which are mathematical structures used to model pairwise relations between objects. A graph consists of a set of vertices (also called nodes) and a set of edges that connect these vertices.

Historical Note: Graph theory began in 1736 when Leonhard Euler solved the famous Seven Bridges of Königsberg problem, which involved finding a path that would cross each of the seven bridges of the city exactly once.

Graphs are used to model many types of relations and processes in physical, biological, social, and information systems, making graph theory one of the most versatile areas of mathematics with applications in computer science, electrical engineering, operations research, linguistics, and more.

Real-life Examples of Graphs:
  • Social networks (people as vertices, friendships as edges)
  • Road systems (intersections as vertices, roads as edges)
  • Computer networks (devices as vertices, connections as edges)
  • Molecular structures (atoms as vertices, bonds as edges)

2. Basic Concepts

Vertex (Node)

A vertex (or node) is the fundamental unit of a graph. It represents an object or entity in the model.

Edge

An edge is a connection between two vertices, representing a relationship or link between the corresponding objects.

Degree of a Vertex

The degree of a vertex is the number of edges connected to it. For directed graphs, we distinguish between in-degree (number of incoming edges) and out-degree (number of outgoing edges).

Example: In the graph below, vertex A has degree 2, vertex B has degree 3, vertex C has degree 2, and vertex D has degree 1.

Path

A path in a graph is a sequence of vertices where each adjacent pair is connected by an edge. The length of a path is the number of edges in it.

Cycle

A cycle is a path that starts and ends at the same vertex, with no repeated edges or vertices (except the starting/ending vertex).

Connected Graph

A graph is connected if there is a path between every pair of vertices. Otherwise, it is disconnected.

Example: The graph below on the left is connected, while the one on the right is disconnected.

3. Types of Graphs

Undirected Graph

In an undirected graph, edges have no direction. If vertex A is connected to vertex B, then vertex B is also connected to vertex A.

Directed Graph (Digraph)

In a directed graph, edges have a direction. If there is an edge from vertex A to vertex B, it doesn't necessarily mean there is an edge from B to A.

Weighted Graph

In a weighted graph, each edge has a weight or cost associated with it. These weights could represent distances, costs, capacities, etc.

Complete Graph

A complete graph is an undirected graph where every pair of vertices is connected by an edge. A complete graph with n vertices is denoted by Kn.

Bipartite Graph

A bipartite graph is a graph whose vertices can be divided into two disjoint sets such that every edge connects a vertex in the first set to a vertex in the second set.

Tree

A tree is a connected undirected graph with no cycles. A tree with n vertices has exactly n-1 edges.

Planar Graph

A planar graph is a graph that can be drawn on a plane without any edges crossing.

Special Properties: Euler's Formula states that for a connected planar graph, V - E + F = 2, where V is the number of vertices, E is the number of edges, and F is the number of faces (including the outer face).

4. Graph Representation

There are several ways to represent a graph in a computer:

Adjacency Matrix

An adjacency matrix is a 2D array of size V×V where V is the number of vertices in the graph. The entry at position (i, j) is 1 if there is an edge from vertex i to vertex j, and 0 otherwise. For weighted graphs, the entry can be the weight of the edge.

Example: Consider the following graph and its adjacency matrix representation:
A B C D A 0 1 1 0 B 1 0 1 1 C 1 1 0 0 D 0 1 0 0

Adjacency List

An adjacency list represents a graph as an array of linked lists. The index of the array represents a vertex, and each element in its linked list represents the vertices that are adjacent to it.

Example: For the same graph, the adjacency list representation would be:
A -> B -> C
B -> A -> C -> D
C -> A -> B
D -> B
      

Edge List

An edge list is a collection of all the edges in the graph. Each edge is represented by a pair of vertices (or a triplet if the graph is weighted).

Example: The edge list for our example graph would be:
(A, B)
(A, C)
(B, C)
(B, D)
      

Comparison of Representations

Representation Space Complexity Check if there's an edge Find all neighbors Add Edge Remove Edge
Adjacency Matrix O(V²) O(1) O(V) O(1) O(1)
Adjacency List O(V+E) O(degree(V)) O(degree(V)) O(1) O(degree(V))
Edge List O(E) O(E) O(E) O(1) O(E)

5. Graph Traversal Algorithms

Graph traversal means visiting all the vertices of a graph in a particular order.

Breadth-First Search (BFS)

BFS explores all vertices at the present depth before moving on to vertices at the next depth level. It uses a queue data structure to keep track of vertices to visit next.

BFS Algorithm:
function BFS(graph, startVertex):
    let queue = new Queue()
    let visited = new Set()
    
    queue.enqueue(startVertex)
    visited.add(startVertex)
    
    while queue is not empty:
        let currentVertex = queue.dequeue()
        // Process currentVertex
        
        for each neighbor of currentVertex:
            if neighbor is not in visited:
                queue.enqueue(neighbor)
                visited.add(neighbor)
      
BFS Traversal Example:

For the graph below, starting from vertex A, BFS traversal would visit vertices in the order: A, B, C, D, E, F.

Depth-First Search (DFS)

DFS explores as far as possible along each branch before backtracking. It can be implemented using recursion or a stack data structure.

DFS Algorithm (Recursive):
function DFS(graph, startVertex, visited = new Set()):
    visited.add(startVertex)
    // Process startVertex
    
    for each neighbor of startVertex:
        if neighbor is not in visited:
            DFS(graph, neighbor, visited)
      
DFS Traversal Example:

For the same graph, starting from vertex A, DFS traversal might visit vertices in the order: A, B, D, E, C, F (though the exact order depends on how neighbors are processed).

Applications of Graph Traversal

  • Finding connected components in a graph
  • Detecting cycles in a graph
  • Solving puzzles like mazes
  • Topological sorting of vertices in a directed acyclic graph (DAG)
  • Finding paths between two vertices

6. Shortest Path Algorithms

Shortest path algorithms find the path with minimum total weight between two vertices in a weighted graph.

Dijkstra's Algorithm

Dijkstra's algorithm finds the shortest path from a source vertex to all other vertices in a weighted graph with non-negative weights.

Dijkstra's Algorithm:
function dijkstra(graph, startVertex):
    let distances = {}  // Stores shortest distance from startVertex to every other vertex
    let previous = {}   // Stores previous vertex in the optimal path
    let pq = new PriorityQueue()  // Vertices to visit next, ordered by distance
    
    // Initialize distances
    for each vertex v in graph:
        if v equals startVertex:
            distances[v] = 0
            pq.enqueue(v, 0)
        else:
            distances[v] = INFINITY
            pq.enqueue(v, INFINITY)
        previous[v] = null
    
    while pq is not empty:
        let currentVertex = pq.dequeue()
        
        for each neighbor of currentVertex:
            let distance = distances[currentVertex] + weight(currentVertex, neighbor)
            
            if distance < distances[neighbor]:
                distances[neighbor] = distance
                previous[neighbor] = currentVertex
                pq.decreaseKey(neighbor, distance)
    
    return { distances, previous }
      
Dijkstra Example:

Consider the following weighted graph:

Starting from vertex A, Dijkstra's algorithm would compute shortest paths to all other vertices.

Bellman-Ford Algorithm

Bellman-Ford algorithm finds the shortest paths from a source vertex to all other vertices in a weighted graph, even if the graph contains negative weight edges. It can also detect negative weight cycles.

Bellman-Ford Algorithm:
function bellmanFord(graph, startVertex):
    let distances = {}  // Stores shortest distance from startVertex to every other vertex
    let previous = {}   // Stores previous vertex in the optimal path
    
    // Initialize distances
    for each vertex v in graph:
        if v equals startVertex:
            distances[v] = 0
        else:
            distances[v] = INFINITY
        previous[v] = null
    
    // Relax edges |V| - 1 times
    for i from 1 to |V| - 1:
        for each edge (u, v) with weight w in graph:
            if distances[u] + w < distances[v]:
                distances[v] = distances[u] + w
                previous[v] = u
    
    // Check for negative weight cycles
    for each edge (u, v) with weight w in graph:
        if distances[u] + w < distances[v]:
            throw "Graph contains a negative weight cycle"
    
    return { distances, previous }
      

Floyd-Warshall Algorithm

Floyd-Warshall algorithm finds the shortest paths between all pairs of vertices in a weighted graph, even with negative weights (but no negative cycles).

Floyd-Warshall Algorithm:
function floydWarshall(graph):
    let dist = new 2D array of size |V| × |V|
    
    // Initialize distances
    for each vertex i in graph:
        for each vertex j in graph:
            if i equals j:
                dist[i][j] = 0
            else if there is an edge from i to j:
                dist[i][j] = weight(i, j)
            else:
                dist[i][j] = INFINITY
    
    // Update distances considering each vertex as an intermediate
    for k from 0 to |V| - 1:
        for i from 0 to |V| - 1:
            for j from 0 to |V| - 1:
                if dist[i][k] + dist[k][j] < dist[i][j]:
                    dist[i][j] = dist[i][k] + dist[k][j]
    
    return dist
      

7. Minimum Spanning Trees

A minimum spanning tree (MST) is a subset of the edges of a connected, undirected weighted graph that connects all the vertices together with the minimum possible total edge weight, without forming cycles.

Kruskal's Algorithm

Kruskal's algorithm finds the MST by adding edges in order of increasing weight, skipping edges that would create a cycle.

Kruskal's Algorithm:
function kruskal(graph):
    let result = new Graph()  // This will store the MST
    let edges = all edges in graph sorted by weight
    let disjointSet = new DisjointSet(all vertices in graph)
    
    for each edge (u, v) with weight w in edges:
        if disjointSet.find(u) != disjointSet.find(v):  // If adding this edge doesn't create a cycle
            result.addEdge(u, v, w)
            disjointSet.union(u, v)
    
    return result
      
Kruskal Example:

Consider the following weighted graph:

Kruskal's algorithm would construct the MST by adding edges in increasing order of weight (avoiding cycles).

Prim's Algorithm

Prim's algorithm starts with a single vertex and grows the MST one vertex at a time by adding the lowest weight edge that connects a vertex in the tree to a vertex outside the tree.

Prim's Algorithm:
function prim(graph, startVertex):
    let result = new Graph()  // This will store the MST
    let visited = new Set()  // Set of vertices included in MST
    let pq = new PriorityQueue()  // Edges to consider next, ordered by weight
    
    visited.add(startVertex)
    
    // Add all edges connected to startVertex to priority queue
    for each edge (startVertex, v) with weight w in graph:
        pq.enqueue(edge, w)
    
    while pq is not empty and visited.size < |V|:
        let currentEdge = pq.dequeue()
        let (u, v) = currentEdge
        
        // If v is not visited, add this edge to MST
        if v is not in visited:
            result.addEdge(u, v, weight(u, v))
            visited.add(v)
            
            // Add all edges connected to v to priority queue
            for each edge (v, x) with weight w in graph:
                if x is not in visited:
                    pq.enqueue(edge, w)
    
    return result
      

8. Graph Coloring

Graph coloring is the process of assigning labels (colors) to elements of a graph subject to certain constraints. In its simplest form, vertex coloring requires that no two adjacent vertices share the same color.

Applications of Graph Coloring

  • Map coloring
  • Scheduling problems
  • Register allocation in compilers
  • Frequency assignment in mobile networks

Greedy Coloring Algorithm

The greedy coloring algorithm assigns colors to vertices in a fixed order, always using the first available color that does not conflict with already colored adjacent vertices.

Greedy Coloring Algorithm:
function greedyColoring(graph):
    let result = {}  // Maps vertices to colors
    let colors = [1, 2, 3, ...]  // Available colors
    
    for each vertex v in graph:
        let usedColors = new Set()  
        
        // Collect colors of all adjacent vertices
        for each adjacent vertex u of v:
            if u is colored:
                usedColors.add(result[u])
        
        // Find the first color not used by adjacent vertices
        let chosenColor = first color in colors not in usedColors
        result[v] = chosenColor
    
    return result
      
Greedy Coloring Example:

Using the greedy algorithm, this graph can be colored using 3 colors.

Chromatic Number

The chromatic number of a graph is the minimum number of colors needed to color the vertices of a graph so that no two adjacent vertices share the same color.

Important Results:
  • The chromatic number of a complete graph Kn is n.
  • The chromatic number of a bipartite graph is 2 (if it's not empty).
  • The chromatic number of a cycle of odd length is 3, and for a cycle of even length, it's 2.

9. Advanced Topics

Network flow problems involve finding the maximum amount of flow that can go from a source vertex to a sink vertex in a flow network (a directed graph where each edge has a capacity).

Ford-Fulkerson Algorithm

The Ford-Fulkerson algorithm finds the maximum flow in a flow network by repeatedly finding augmenting paths from source to sink and augmenting the flow along these paths.

Ford-Fulkerson Algorithm:
function fordFulkerson(graph, source, sink):
    let residualGraph = copy of graph
    let maxFlow = 0
    
    while there is an augmenting path p from source to sink in residualGraph:
        let residualCapacity = minimum capacity of edges along path p
        maxFlow += residualCapacity
        
        for each edge (u, v) in path p:
            residualGraph[u][v] -= residualCapacity  // Forward edge
            residualGraph[v][u] += residualCapacity  // Backward edge
    
    return maxFlow
        

A topological sort of a directed acyclic graph (DAG) is a linear ordering of its vertices such that for every directed edge (u, v), vertex u comes before vertex v in the ordering.

Topological Sort Algorithm (using DFS):
function topologicalSort(graph):
    let visited = new Set()
    let stack = new Stack()
    
    function dfs(vertex):
        visited.add(vertex)
        
        for each neighbor of vertex:
            if neighbor is not in visited:
                dfs(neighbor)
        
        stack.push(vertex)
    
    for each vertex v in graph:
        if v is not in visited:
            dfs(v)
    
    // Return vertices in reverse order of their finishing times
    let result = []
    while stack is not empty:
        result.push(stack.pop())
    
    return result
        

A strongly connected component (SCC) of a directed graph is a subgraph in which there is a path from each vertex to every other vertex.

Kosaraju's Algorithm

Kosaraju's algorithm finds all strongly connected components in a directed graph in linear time.

Kosaraju's Algorithm:
function kosaraju(graph):
    let visited = new Set()
    let stack = new Stack()
    
    // First DFS to fill stack with vertices in order of finishing time
    for each vertex v in graph:
        if v is not in visited:
            fillOrder(v, visited, stack)
    
    // Create the transpose graph (reverse all edges)
    let transposeGraph = reverse all edges in graph
    
    // Second DFS using vertices from stack
    visited.clear()
    let result = []
    
    while stack is not empty:
        let v = stack.pop()
        
        if v is not in visited:
            let component = new Set()
            dfs(transposeGraph, v, visited, component)
            result.push(component)
    
    return result
        

An Eulerian path is a path in a graph that visits every edge exactly once. An Eulerian circuit is an Eulerian path that starts and ends at the same vertex.

Conditions for Existence:
  • An undirected graph has an Eulerian circuit if and only if every vertex has an even degree and the graph is connected.
  • An undirected graph has an Eulerian path if and only if exactly zero or two vertices have odd degree, and the graph is connected.
Finding Eulerian Path/Circuit (Hierholzer's Algorithm):
function findEulerianPath(graph):
    let result = []
    let stack = new Stack()
    let startVertex = find a valid starting vertex
    
    stack.push(startVertex)
    
    while stack is not empty:
        let currentVertex = stack.peek()
        
        if degree(currentVertex) > 0:
            let nextVertex = any adjacent vertex of currentVertex
            
            // Remove the edge from the graph
            removeEdge(graph, currentVertex, nextVertex)
            
            stack.push(nextVertex)
        else:
            result.push(stack.pop())
    
    return reverse of result
        

10. Real-world Applications

Social Network Analysis

Graphs are used to model social networks where vertices represent people and edges represent relationships. Various graph algorithms help identify communities, influential individuals, and information flow patterns.

Transportation Networks

Road systems, flight routes, and public transit networks are modeled as graphs. Shortest path algorithms help find optimal routes, and network flow algorithms help optimize capacity.

Computer Networks

The internet and local networks are modeled as graphs. Routing algorithms determine how data moves through these networks.

Biology and Chemistry

Protein-protein interaction networks, metabolic pathways, and molecular structures are represented as graphs.

Computer Science

Graphs are used in various areas of computer science including:

  • Compiler optimization
  • Database query optimization
  • Image processing
  • Natural language processing
  • Artificial intelligence and machine learning

11. Interactive Quiz

1. Which traversal algorithm visits all vertices at the current depth before moving to vertices at the next depth?
A. Depth-First Search (DFS)
B. Breadth-First Search (BFS)
C. Dijkstra's Algorithm
D. Prim's Algorithm
2. What is the chromatic number of a complete graph with 5 vertices (K₅)?
A. 3
B. 4
C. 5
D. 6
3. Which algorithm can detect negative weight cycles in a graph?
A. Dijkstra's Algorithm
B. Prim's Algorithm
C. Bellman-Ford Algorithm
D. Kruskal's Algorithm
4. What is the maximum number of edges in an undirected graph with n vertices?
A. n
B. n-1
C. n(n-1)
D. n(n-1)/2
5. Which of the following is NOT a necessary condition for a graph to have an Eulerian circuit?
A. The graph must be connected
B. Every vertex must have even degree
C. The graph must have an equal number of vertices and edges
D. The graph must not have any bridges
Shares:

Leave a Reply

Your email address will not be published. Required fields are marked *