Loading content...
So far, we've implicitly assumed graphs with "well-behaved" edges: each edge connects two different vertices, and at most one edge exists between any pair. But reality is messier.
What if a webpage links to itself? What if there are multiple flights between two cities on the same day? What if a state machine can loop back to its current state?
These scenarios require us to extend our graph vocabulary to include self-loops (edges from a vertex to itself) and multi-edges (multiple edges between the same pair of vertices). Understanding when and how to use these structures—and when to avoid them—is essential for accurate graph modeling.
By the end of this page, you will understand self-loops and their applications, recognize when multi-edges are necessary for accurate modeling, distinguish between simple graphs, multigraphs, and pseudographs, and know how self-loops affect degree calculations and graph algorithms.
A self-loop (also called a loop) is an edge that connects a vertex to itself. If we denote this edge in directed notation, it's (v, v); in undirected notation, it's {v, v} (which simplifies to just {v}).
Formal Definition:
In a graph G = (V, E), an edge e ∈ E is a self-loop if both endpoints are the same vertex:
Visual Representation:
Self-loops are typically drawn as small circles emanating from a vertex and returning to it—like a vertex with an earring or loop attached.
In the diagram above, vertex A has a self-loop (A → A) and vertex C has a self-loop (C → C).
Degree Counting with Self-Loops:
A subtle but important detail: a self-loop contributes 2 to the degree of its vertex (in undirected graphs). Why? Because degree counts edge endpoints incident to the vertex, and a self-loop has both endpoints at the same vertex.
This convention preserves the Handshaking Lemma: $$\sum_{v \in V} \deg(v) = 2|E|$$
If a self-loop counted as 1, the sum would be odd when |E| is odd, breaking the lemma.
In directed graphs, a self-loop (v, v) contributes 1 to both in-degree AND out-degree of v. This is different from undirected where it adds 2 to deg(v). The directed handshaking lemma Σdeg⁺ = Σdeg⁻ = |E| still holds because the self-loop adds 1 to each sum.
Self-loops arise naturally in many domains. Recognizing them helps you model systems accurately:
State Machines:
In finite automata and state diagrams, self-loops represent state transitions that don't change the state. For example, in a vending machine state diagram, inserting more coins while in the "collecting money" state might loop back to the same state (increasing the balance).
Markov Chains:
Probability transitions where a system can stay in its current state with some probability are self-loops. A weather model might have a 70% chance of "sunny" remaining "sunny" the next day.
Web Graphs:
A webpage linking to itself (same URL) creates a self-loop. While unusual, this can happen through navigation elements or anchor links.
Dependency Graphs:
A module that references itself (perhaps through conditional imports or plugins) creates a self-loop—often indicating a design smell.
| Domain | Self-Loop Meaning | Example |
|---|---|---|
| State Machine | Action that stays in same state | Retry button in error state |
| Markov Chain | Probability of staying in state | 70% chance rain continues |
| Call Graph | Recursive function call | fibonacci(n) calls fibonacci(n-1) |
| Web Graph | Page links to itself | Anchor link to same page |
| Game Graph | Pass or skip turn | Chess: repetition, skip move |
| Network | Loopback address | 127.0.0.1 → same machine |
A self-loop is the simplest possible cycle—a cycle of length 1. If you're detecting cycles in a graph, remember that a single self-loop edge creates a cycle. Many cycle detection algorithms handle self-loops as a special case.
Multi-edges (also called parallel edges or multiple edges) occur when two or more distinct edges connect the same pair of vertices.
Formal Definition:
In a graph G = (V, E), multi-edges exist when E contains multiple copies of the same edge:
Since sets cannot contain duplicates, we need a modified definition. We can either:
When Multi-Edges Matter:
Multi-edges are necessary when you need to distinguish between different connections with the same endpoints—such as multiple flights, parallel roads, or different types of relationships.
In the diagram above, there are three distinct edges from NYC to LAX, representing three different flights. These are multi-edges—multiple edges with the same endpoints but different identities.
Contrast with Simple Graphs:
In a simple graph, at most one edge exists between any pair of vertices. This simplifies many algorithms because you only need to track "connected or not," not "how many connections." However, simple graphs lose information when the number of connections matters.
Multi-edges and weighted edges solve related but different problems. A weighted edge (u, v, weight=3) says 'this connection has magnitude 3.' Three multi-edges between u and v say 'there are three distinct connections.' Choose based on your domain: do you need to track individual connections, or just aggregate magnitude?
Multi-edges appear whenever multiple distinct relationships exist between the same entities:
Transportation Networks:
Between two cities, there might be multiple flights (with different airlines, times, or prices), multiple train routes (express vs. local), or multiple roads (highway vs. local streets). Each is a distinct edge.
Communication Networks:
Two servers might have multiple network links for redundancy. Each link is a separate edge with its own capacity, latency, and failure characteristics.
Social Networks (Multi-Relational):
Two people might be connected through multiple relationship types: friends on Facebook, colleagues on LinkedIn, and co-authors on papers. These are logically distinct edges.
Knowledge Graphs:
Entities can have multiple relationships. (Einstein, Germany) might have edges labeled "birthplace," "citizenship," and "fled from"—three distinct connections.
| Domain | Multi-Edge Meaning | Why Not Use Weights? |
|---|---|---|
| Airline Routes | Different flights (times, airlines) | Each flight has its own properties (time, price) |
| Redundant Links | Parallel network connections | Each link can fail independently |
| Social Relations | Friend AND colleague AND family | Different relationship types, not quantities |
| Knowledge Graph | Multiple relationship types | Each edge has different semantics |
| Circuit Design | Parallel wires | Each wire has its own properties |
When deciding between multi-edges and weighted edges, ask: 'Do I need to reason about individual connections, or just their aggregate effect?' For flow networks, weights often suffice. For scheduling, routing, or relationship analysis, multi-edges may be necessary.
Graph theory classifies graphs based on whether they allow self-loops and multi-edges:
Simple Graph:
Multigraph:
Pseudograph:
Simple Digraph:
Multidigraph:
| Graph Type | Self-Loops | Multi-Edges | Example Use Case |
|---|---|---|---|
| Simple Graph | No | No | Social network (friend/not friend) |
| Multigraph | No | Yes | Transportation network (multiple routes) |
| Pseudograph | Yes | Yes | General modeling, state machines |
| Simple Digraph | No | No | Dependency graph (A depends on B) |
| Multidigraph | Optional | Yes | Multi-relational knowledge graph |
In most algorithm contexts, 'graph' means 'simple graph' unless otherwise specified. Algorithms for multigraphs or pseudographs may require modifications. Always clarify assumptions when self-loops or multi-edges might be present.
Self-loops and multi-edges affect how algorithms behave. Here are key considerations:
Cycle Detection:
Connectivity:
Shortest Path:
Minimum Spanning Tree:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
from typing import List, Tuple, Set, Dictfrom collections import deque def has_cycle_with_self_loops(n: int, edges: List[Tuple[int, int]]) -> bool: """ Detect if a directed graph has a cycle, including self-loops. Returns True if any cycle exists (including self-loops). """ # Check for self-loops first (trivial cycles) for u, v in edges: if u == v: return True # Self-loop found! # Build adjacency list (ignoring multi-edges for cycle detection) adj: Dict[int, Set[int]] = {i: set() for i in range(n)} for u, v in edges: adj[u].add(v) # Standard DFS cycle detection with colors WHITE, GRAY, BLACK = 0, 1, 2 color = [WHITE] * n def dfs(node: int) -> bool: color[node] = GRAY # Currently visiting for neighbor in adj[node]: if color[neighbor] == GRAY: return True # Back edge → cycle if color[neighbor] == WHITE and dfs(neighbor): return True color[node] = BLACK # Finished return False for i in range(n): if color[i] == WHITE and dfs(i): return True return False def minimum_edge_for_multi_edges(edges: List[Tuple[int, int, float]]) -> Dict[Tuple[int, int], float]: """ Given edges with weights (possibly multi-edges), return the minimum weight edge for each pair. Useful for shortest path and MST preprocessing. """ min_edge: Dict[Tuple[int, int], float] = {} for u, v, weight in edges: key = (u, v) if key not in min_edge or weight < min_edge[key]: min_edge[key] = weight return min_edge # Example: Graph with self-loopedges_with_self_loop = [(0, 1), (1, 2), (2, 2)] # 2→2 is self-loopprint(f"Has cycle (with self-loop): {has_cycle_with_self_loops(3, edges_with_self_loop)}")# True (because of self-loop at vertex 2) # Example: Multi-edges - keep minimummulti_edges = [ (0, 1, 5.0), # NYC → LA, $500 (0, 1, 3.0), # NYC → LA, $300 (cheaper!) (0, 1, 7.0), # NYC → LA, $700 (1, 2, 2.0), # LA → SF, $200]print("Minimum edges:", minimum_edge_for_multi_edges(multi_edges))# {(0, 1): 3.0, (1, 2): 2.0}When implementing graph algorithms, explicitly decide how to handle self-loops and multi-edges. Either filter them out in preprocessing, or ensure your algorithm handles them correctly. Silent bugs often arise from unexpected self-loops in input data.
How you represent graphs affects how naturally self-loops and multi-edges are handled:
Adjacency Matrix:
matrix[v][v] = true (or weight for weighted graphs)Adjacency List (Set-based):
Adjacency List (List-based):
Edge List:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
from typing import List, Tuple, Dict, Setfrom collections import defaultdict class SimpleGraphMatrix: """Adjacency matrix - supports self-loops, NOT multi-edges.""" def __init__(self, n: int): self.n = n self.matrix = [[False] * n for _ in range(n)] def add_edge(self, u: int, v: int): self.matrix[u][v] = True self.matrix[v][u] = True # Undirected def add_self_loop(self, v: int): self.matrix[v][v] = True # Works naturally def edge_count(self, u: int, v: int) -> int: # Can only return 0 or 1 - no multi-edge support return 1 if self.matrix[u][v] else 0 class MultiGraphAdjList: """Adjacency list (list-based) - supports self-loops AND multi-edges.""" def __init__(self): self.adj: Dict[int, List[int]] = defaultdict(list) def add_edge(self, u: int, v: int): self.adj[u].append(v) if u != v: # Don't double-add for self-loops self.adj[v].append(u) def edge_count(self, u: int, v: int) -> int: """Count edges between u and v (including multi-edges).""" if u == v: # Self-loops appear once in adjacency list return self.adj[u].count(v) else: # For undirected, count in one direction return self.adj[u].count(v) def neighbors(self, v: int) -> List[int]: return self.adj[v] # May include v itself (self-loop) class EdgeListGraph: """Edge list - most flexible, supports everything.""" def __init__(self): self.edges: List[Tuple[int, int]] = [] self.vertices: Set[int] = set() def add_edge(self, u: int, v: int): self.edges.append((u, v)) self.vertices.add(u) self.vertices.add(v) def has_self_loop(self, v: int) -> bool: return any(u == v and w == v for u, w in self.edges) def count_multi_edges(self, u: int, v: int) -> int: """Count all edges between u and v (both directions for undirected).""" count = 0 for a, b in self.edges: if (a == u and b == v) or (a == v and b == u): count += 1 return count # Demonstrationprint("=== Simple Graph (Matrix) ===")sg = SimpleGraphMatrix(3)sg.add_edge(0, 1)sg.add_edge(0, 1) # Second edge ignored!sg.add_self_loop(2)print(f"Edge count 0-1: {sg.edge_count(0, 1)}") # 1 (ignores multi)print(f"Self-loop at 2: {sg.matrix[2][2]}") # True print("=== Multigraph (List) ===")mg = MultiGraphAdjList()mg.add_edge(0, 1)mg.add_edge(0, 1) # Second edge stored!mg.add_edge(0, 1) # Third edge stored!mg.add_edge(2, 2) # Self-loopprint(f"Edge count 0-1: {mg.edge_count(0, 1)}") # 3 (counts multi)print(f"Neighbors of 0: {mg.neighbors(0)}") # [1, 1, 1]print(f"Neighbors of 2: {mg.neighbors(2)}") # [2] (self-loop)If your problem might have multi-edges, use list-based adjacency or edge lists. If you want to prevent multi-edges, use set-based adjacency or matrices. The representation enforces constraints automatically.
Should you allow self-loops and multi-edges in your graph model? The answer depends on your problem:
Embrace Self-Loops When:
Avoid Self-Loops When:
Embrace Multi-Edges When:
Avoid Multi-Edges When:
When in doubt, start with a simple graph. You can always extend to multigraphs if needed, but unnecessary complexity makes algorithms harder to implement and debug. Most graph algorithm resources assume simple graphs.
We've explored the special cases that extend simple graphs. Let's consolidate:
Module Complete:
Congratulations! You've completed the foundational vocabulary of graph theory. You now understand:
With this vocabulary, you're ready to explore graph types (directed, undirected, weighted), graph representations (adjacency matrix, adjacency list), and eventually graph algorithms (BFS, DFS, shortest paths, and more).
You've mastered the fundamental vocabulary of graph theory. From vertices and edges to formal definitions, adjacency relationships, and special edge types, you now speak the language of graphs fluently. This foundation will support every graph algorithm and application you encounter.