How Topological Sort Works: Deep Dive with Code

Graph Algorithms By Emma Rodriguez, Lead Algorithm Developer 21 min read

Introduction and Core Concepts

Topological sort is a fundamental graph algorithm that produces a linear ordering of vertices in a directed acyclic graph (DAG) such that for every directed edge u → v, vertex u appears before vertex v in the ordering. If you have ever installed software packages where library A must be installed before application B, compiled source files with dependencies, or planned university courses with prerequisites, you have already encountered the problem that topological sort solves. It transforms a partial order defined by dependencies into a total order that respects all constraints.

Not every graph can be topologically sorted. The defining requirement is that the graph must be acyclic — it must contain no directed cycles. If a cycle exists, such as A depends on B, B depends on C, and C depends on A, no linear ordering can satisfy all constraints simultaneously. Detecting this impossibility is actually a core feature of topological sorting algorithms: they either return a valid ordering or report that none exists. This makes the algorithm both a sorting mechanism and a cycle-detection tool for directed graphs.

The importance of topological sort extends far beyond academic exercises. It is the backbone of dependency resolution in build systems like Make, Gradle, and Bazel, the scheduling engine behind task orchestrators such as Apache Airflow, and the logic that determines the order of database migrations, spreadsheet cell recalculations, and even web server request processing pipelines. Understanding topological sort is essential for any software engineer working with workflows, pipelines, or any system where ordering constraints must be enforced efficiently and correctly.

What Makes a Graph Topologically Sortable?

A directed graph is topologically sortable if and only if it is a DAG. The acyclic property guarantees that there is at least one vertex with indegree zero (no incoming edges) — a natural starting point. After removing that vertex and its outgoing edges, the remaining graph is still a DAG, so it too has a vertex with indegree zero. This inductive property is what makes iterative algorithms like Kahn's algorithm possible. The existence of multiple valid topological orderings is common; most DAGs do not have a unique sort. For example, if two tasks are independent of each other but both must precede a third task, either can come first.

It is critical to distinguish topological sort from traditional sorting algorithms like quicksort or mergesort. Traditional sorting imposes a total order based on comparing keys (numbers, strings). Topological sort respects a partial order defined by edges — it does not sort by value, but by dependency. Two vertices that are not connected by a path can appear in either order. This flexibility is powerful but also means the output is not necessarily deterministic unless tie-breaking rules are explicitly added.

How It Works

There are two canonical approaches to topological sorting: Kahn's algorithm, which is based on breadth-first search and indegree counting, and the depth-first search (DFS) based algorithm that uses vertex finishing times. Both achieve the same goal but reveal different intuitions about the structure of DAGs. Kahn's algorithm is often more intuitive for beginners because it simulates the process of repeatedly removing tasks that have no remaining prerequisites.

Kahn's algorithm works by maintaining an indegree count for every vertex — the number of incoming edges. It begins by enqueueing all vertices with indegree zero. Then it repeatedly dequeues a vertex, appends it to the result, and "removes" it from the graph by decrementing the indegree of all its neighbors. If a neighbor's indegree drops to zero, it becomes available and is enqueued. This process continues until the queue is empty. If the number of vertices in the result equals the number of vertices in the graph, the sort succeeded. If not, the remaining vertices are part of a cycle.

The DFS-based approach leverages the properties of depth-first traversal. During DFS, we mark vertices as unvisited, visiting (currently on the recursion stack), and visited (fully explored). When we finish exploring all descendants of a vertex, we push it onto a stack. After DFS completes for all vertices, the stack in reverse order yields a topological ordering. The key insight is that a vertex finishes only after all vertices reachable from it have finished, so it should appear before them. If we ever encounter an edge to a vertex that is currently in the visiting state, we have found a back edge and thus a cycle, meaning topological sort is impossible.

Why Acyclicity is Non-Negotiable

The requirement for acyclicity is not an implementation limitation but a mathematical necessity. A cycle creates a circular dependency where each vertex in the cycle must come before itself, which is a logical contradiction. Both algorithms detect cycles naturally: Kahn's algorithm detects it when vertices remain unprocessed, and DFS detects it via back edges. This dual role makes topological sort a reliable way to validate whether a set of dependencies is feasible before attempting to execute them.

Another important nuance is that topological orderings are not unique and not necessarily sorted in any numeric or lexicographic sense. If a problem requires the lexicographically smallest or largest topological order, a variation using a priority queue (min-heap or max-heap) instead of a FIFO queue is needed. This is a common interview twist and a practical requirement in systems where deterministic builds are desired.

Implementation

Implementing topological sort correctly requires careful graph representation, usually an adjacency list, and precise handling of indegrees and visited states. Below are four production-ready Python implementations covering the most important variants. All examples assume vertices are labeled 0 to n-1 and edges are given as pairs (u, v) meaning u → v.

The first implementation is Kahn's algorithm, the most widely used in practice due to its iterative nature and easy cycle detection. It is ideal for large graphs where recursion depth could be a problem.

from collections import deque, defaultdict

def kahn_topological_sort(num_vertices, edges):
    # Build adjacency list and indegree array
    graph = defaultdict(list)
    indegree = [0] * num_vertices
    for u, v in edges:
        graph[u].append(v)
        indegree[v] += 1

    # Initialize queue with all nodes having indegree 0
    queue = deque([i for i in range(num_vertices) if indegree[i] == 0])
    result = []

    while queue:
        u = queue.popleft()
        result.append(u)
        for v in graph[u]:
            indegree[v] -= 1
            if indegree[v] == 0:
                queue.append(v)

    # If result length < num_vertices, graph has a cycle
    if len(result) < num_vertices:
        return []  # Cycle detected - no valid ordering
    return result

# Example
edges = [(5, 2), (5, 0), (4, 0), (4, 1), (2, 3), (3, 1)]
print(kahn_topological_sort(6, edges))  # Possible output: [4, 5, 2, 0, 3, 1]

The second implementation uses iterative DFS with explicit state tracking. This version is elegant and naturally produces a reversed post-order. It is particularly useful when you also need to perform other DFS-based analyses, such as finding strongly connected components.

from collections import defaultdict

def dfs_topological_sort(num_vertices, edges):
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)

    visited = [0] * num_vertices  # 0=unvisited, 1=visiting, 2=visited
    stack = []
    has_cycle = False

    def dfs(u):
        nonlocal has_cycle
        visited[u] = 1
        for v in graph[u]:
            if visited[v] == 0:
                dfs(v)
                if has_cycle:
                    return
            elif visited[v] == 1:
                has_cycle = True  # Back edge found - cycle exists
                return
        visited[u] = 2
        stack.append(u)

    for i in range(num_vertices):
        if visited[i] == 0:
            dfs(i)
            if has_cycle:
                return []

    # stack holds reverse topological order
    return stack[::-1]

# Example
edges = [(5, 2), (5, 0), (4, 0), (4, 1), (2, 3), (3, 1)]
print(dfs_topological_sort(6, edges))  # Possible output: [5, 4, 2, 3, 1, 0]

When deterministic output is required, such as for reproducible builds, we replace the queue with a min-heap to always pick the smallest available vertex. This guarantees the lexicographically smallest topological order among all valid orders.

import heapq
from collections import defaultdict

def lexicographically_smallest_topo(num_vertices, edges):
    graph = defaultdict(list)
    indegree = [0] * num_vertices
    for u, v in edges:
        graph[u].append(v)
        indegree[v] += 1

    # Use min-heap instead of queue for smallest available node first
    heap = [i for i in range(num_vertices) if indegree[i] == 0]
    heapq.heapify(heap)
    result = []

    while heap:
        u = heapq.heappop(heap)
        result.append(u)
        for v in graph[u]:
            indegree[v] -= 1
            if indegree[v] == 0:
                heapq.heappush(heap, v)

    if len(result) < num_vertices:
        return []  # Cycle exists
    return result

# Example: With heap, order is deterministic
edges = [(0, 1), (0, 2), (1, 3), (2, 3)]
print(lexicographically_smallest_topo(4, edges))  # Output: [0, 1, 2, 3]

The final implementation shows a practical application: course schedule validation, a classic LeetCode-style problem that is essentially cycle detection via topological sort. It returns whether all courses can be finished given prerequisites.

from collections import deque, defaultdict

def can_finish_courses(num_courses, prerequisites):
    # prerequisites: list of [course, prereq] meaning prereq -> course
    graph = defaultdict(list)
    indegree = [0] * num_courses
    for course, prereq in prerequisites:
        graph[prereq].append(course)
        indegree[course] += 1

    queue = deque([i for i in range(num_courses) if indegree[i] == 0])
    completed = 0

    while queue:
        u = queue.popleft()
        completed += 1
        for v in graph[u]:
            indegree[v] -= 1
            if indegree[v] == 0:
                queue.append(v)

    # Can finish if all courses were processed
    return completed == num_courses

# Example usage:
# 4 courses, prerequisites: 1 -> 0, 2 -> 0, 3 -> 1, 3 -> 2
print(can_finish_courses(4, [[1,0],[2,0],[3,1],[3,2]]))  # True
print(can_finish_courses(2, [[1,0],[0,1]]))  # False (cycle)

Step-by-Step Example

To make the algorithm concrete, let us walk through Kahn's algorithm on a DAG representing a software build. Suppose we have six modules: 0: utils, 1: database, 2: api, 3: auth, 4: frontend, 5: config. The dependencies are: config → api, config → utils, frontend → utils, frontend → database, api → auth, auth → database. In edge list form: (5,2), (5,0), (4,0), (4,1), (2,3), (3,1). Our goal is to find a valid build order.

First, we compute indegrees: utils(0) has 2 incoming (from 5 and 4), database(1) has 2 (from 4 and 3), api(2) has 1 (from 5), auth(3) has 1 (from 2), frontend(4) has 0, config(5) has 0. The initial queue contains [4, 5] — the only modules with no prerequisites. The result list is empty. This initialization is crucial; any DAG must have at least one such source node.

Iteration 1: Dequeue 4 (frontend). Append 4 to result → [4]. For each neighbor of 4 (0 and 1), decrement indegree: indegree[0] goes from 2 to 1, indegree[1] from 2 to 1. Neither reaches zero, so queue is now [5]. Iteration 2: Dequeue 5 (config). Result → [4,5]. Decrement neighbors 2 and 0: indegree[2] 1→0 (enqueue 2), indegree[0] 1→0 (enqueue 0). Queue is now [2,0]. Iteration 3: Dequeue 2 (api). Result → [4,5,2]. Neighbor 3: indegree[3] 1→0 (enqueue 3). Queue → [0,3].

Iteration 4: Dequeue 0 (utils). Result → [4,5,2,0]. Node 0 has no outgoing edges, so nothing changes. Queue → [3]. Iteration 5: Dequeue 3 (auth). Result → [4,5,2,0,3]. Neighbor 1: indegree[1] 1→0 (enqueue 1). Queue → [1]. Iteration 6: Dequeue 1 (database). Result → [4,5,2,0,3,1]. No neighbors. Queue is empty and result length equals 6, so we have a valid topological order: frontend, config, api, utils, auth, database. Note that [5,4,2,3,0,1] would also be valid — the algorithm's tie-breaking (queue order) determines which valid order is returned.

What Happens With a Cycle?

If we add an extra edge database → frontend (1,4), the indegrees become: 0:2, 1:2, 2:1, 3:1, 4:1, 5:0. Initial queue is only [5]. After processing 5, 2, 0, and 3, the indegree of 1 will be 1 (from 4) and indegree of 4 will be 1 (from 1). No vertex will reach indegree zero, the queue will empty with result [5,2,0,3] of length 4 < 6, and the algorithm correctly reports a cycle. This illustrates how cycle detection is an inherent byproduct, not an extra step.

Complexity Analysis

Topological sort is remarkably efficient, running in linear time relative to the size of the graph. This efficiency is why it scales to massive dependency graphs with millions of nodes in modern build systems and package managers. Both Kahn's and DFS-based algorithms share the same asymptotic complexity, though their constant factors and memory access patterns differ slightly.

For Kahn's algorithm, building the adjacency list and indegree array takes O(V + E) time, where V is vertices and E is edges. Each vertex is enqueued and dequeued exactly once (O(V)), and each edge is examined exactly once when its source vertex is processed (O(E)). The total time is therefore O(V + E). The DFS approach also visits each vertex and edge once during the traversal, yielding the same O(V + E) bound. Space complexity is O(V + E) to store the graph plus O(V) for auxiliary structures (queue/stack, indegree/visited arrays, and result).

Unlike sorting algorithms where best, average, and worst cases differ significantly, topological sort has essentially the same linear bound in all cases for a given graph representation. The only variation comes from graph density and the data structures used for tie-breaking. Using a heap for lexicographically smallest order adds an O(log V) factor per vertex, making it O((V + E) log V).

Operation / Variant Best Case Average Case Worst Case Space Complexity
Kahn's Algorithm (BFS + Queue) O(V + E) O(V + E) O(V + E) O(V + E)
DFS-Based Algorithm (Recursion) O(V + E) O(V + E) O(V + E) O(V + E)
Lexicographically Smallest (Min-Heap) O((V + E) log V) O((V + E) log V) O((V + E) log V) O(V + E)
Cycle Detection Only O(V + E) O(V + E) O(V + E) O(V + E)
All Topological Orders (Enumeration) O(V!) O(V!) O(V!) O(V + E)

The last row is important: enumerating *all* possible topological orderings is factorial in the worst case and should never be done for large graphs. This is a common trap when developers try to find "the best" ordering by brute force. In practice, you generate one valid ordering and, if needed, apply secondary criteria via heap ordering or weighted scheduling.

Variations and Optimizations

While the basic algorithms are sufficient for most cases, several variations address specific requirements. The choice of variation often depends on whether you need determinism, parallelism, incremental updates, or handling of dynamic graphs.

The lexicographically smallest (or largest) topological order is achieved by replacing the FIFO queue in Kahn's algorithm with a priority queue. This is essential for reproducible builds where the same dependency graph must always produce the same build order across machines. The trade-off is the added logarithmic overhead. Conversely, for parallel execution, you might want to find the *level* of each node — the longest distance from any source. This can be computed during Kahn's algorithm by processing nodes level by level (like BFS layers). All nodes in the same level are independent and can be built or executed concurrently, which is how modern CI/CD systems maximize parallelism.

For very large graphs that do not fit in memory, external-memory or distributed topological sorting techniques are used. These often involve partitioning the graph and using iterative MapReduce-style processing. Another optimization is incremental topological sorting, where the graph changes slightly (an edge is added or removed) and you want to update the ordering without recomputing from scratch. Algorithms like PKU or MNR maintain the order dynamically in O(sqrt(E)) or O(V) per update, which is critical for live dependency graphs in IDEs and spreadsheet engines.

Handling Disconnected Graphs and Multiple Components

Real-world graphs are often disconnected. Both Kahn's and DFS algorithms naturally handle this by initializing the queue/stack with all zero-indegree nodes or by iterating over all vertices to start DFS from unvisited nodes. The result is still a valid global ordering, though vertices from different components can be interleaved arbitrarily. If you need to keep components grouped, you can topologically sort each weakly connected component separately.

Another practical optimization is early exit for cycle detection. If your only goal is to know whether a cycle exists (e.g., validating a user's workflow), you can stop as soon as Kahn's queue empties prematurely or DFS finds a back edge, without processing the entire graph. For recursion-averse environments (like Python with deep graphs), the iterative DFS using an explicit stack avoids recursion limit errors and stack overflows.

Real-World Applications

Topological sort is one of the most deployed graph algorithms in production software, often hidden inside tools developers use daily without realizing it. Its ability to linearize dependencies makes it indispensable wherever ordering constraints exist.

In build systems and package managers (Make, npm, pip, Maven, Cargo), each file or package is a node and an edge A → B means A must be built before B. The build tool topologically sorts the dependency graph to determine the correct compilation order and to parallelize independent builds. A cycle here means a circular dependency, which the tool reports as an error. Similarly, in task scheduling and workflow orchestration (Apache Airflow, Luigi, GitHub Actions), tasks are nodes and dependencies are edges. The scheduler executes tasks in topological order, often level by level to maximize concurrency.

Database systems use topological sort for migration ordering, query plan optimization, and deadlock detection. Spreadsheet applications like Excel recalculate cells in topological order of their formula dependencies — if A1 = B1 + C1, then B1 and C1 must be evaluated before A1. Web servers and frontend frameworks use it to resolve middleware ordering and component rendering dependencies. Even the Linux kernel's initialization and systemd's service startup sequence rely on topological sorting of service dependencies.

Use Case Industry Example Graph Model (Node → Edge) Benefit of Topological Sort
Build Systems Make, Bazel, Gradle File → Depends on File Correct compilation order, parallel builds
Package Management npm, pip, apt Package → Depends on Package Install dependencies before dependents
Course Scheduling University Curriculum Course → Prerequisite Valid semester plan, detect impossible requirements
Task Orchestration Airflow, Kubernetes Jobs Task → Must Complete Before Workflow execution, failure isolation
Spreadsheet Calculation Excel, Google Sheets Cell → Formula Reference Efficient recalculation without redundant work
Database Migrations Flyway, Alembic Migration → Depends on Migration Apply schema changes in safe order
Service Startup systemd, Docker Compose Service → Depends on Service Start infrastructure in dependency order

Beyond these, topological sort appears in unexpected places: resolving symbol dependencies in linkers, ordering class inheritance in object-oriented languages, determining the order of spreadsheet formula evaluation, and even in forensic analysis of event logs where causality must be reconstructed. Any time you see "must happen before" constraints, topological sort is likely the solution.

Comparison with Alternatives

Topological sort is often confused with other graph traversals, but its purpose and guarantees are distinct. Understanding when to use it versus BFS, DFS, or shortest-path algorithms is key to choosing the right tool.

BFS and DFS are graph *traversal* strategies — they define the order in which vertices are visited, but they do not guarantee that all dependencies are respected. BFS visits nodes level by level from a source, which is great for finding shortest paths in unweighted graphs, but it will not produce a valid dependency order for a DAG unless the graph is already layered. DFS explores as deep as possible before backtracking; while its reverse post-order *is* a topological order, a naive DFS without cycle detection or stack reversal is not a topological sort.

For weighted scheduling problems where tasks have durations and you want to minimize total completion time, topological sort alone is insufficient. You need to combine it with dynamic programming to compute the critical path (longest path in a DAG). Similarly, if you need to find the shortest path in a DAG, you can topologically sort first and then relax edges in that order — this yields O(V + E) shortest paths, faster than Dijkstra's O((V+E) log V) for DAGs. This hybrid approach shows how topological sort is often a preprocessing step rather than a standalone solution.

Feature Topological Sort (Kahn) Topological Sort (DFS) BFS Traversal DFS Traversal Dijkstra's Algorithm
Purpose Linearize dependencies Linearize dependencies Level-order visit, shortest path (unweighted) Explore depth, path finding Shortest path (weighted)
Works On DAG only DAG only Any graph Any graph Weighted graph, no negative edges
Time Complexity O(V + E) O(V + E) O(V + E) O(V + E) O((V + E) log V)
Detects Cycles? Yes (remaining nodes) Yes (back edge) No Partial (via visited states) No
Ordering Guarantee Respects all edges Respects all edges By distance from source By depth By path cost
Parallelizable? Yes (level scheduling) Harder (recursive) Yes (level by level) No Limited

In summary, if your problem involves dependencies and you need an order that respects them, topological sort is the correct choice. If you need distances or traversal without dependency constraints, use BFS/DFS. If you have weights and need optimal paths, use Dijkstra or, for DAGs, topological sort plus DP for even better performance.

Common Mistakes and Best Practices

Even experienced developers make subtle errors when implementing topological sort, often leading to incorrect cycle handling, non-deterministic output, or performance pitfalls. Awareness of these mistakes is as important as knowing the algorithm itself.

The most frequent mistake is forgetting to check for cycles. Many implementations return a partial ordering without verifying that its length equals V. This silently produces an invalid result when a cycle exists, causing downstream systems to fail mysteriously. Always validate that len(result) == V and treat a mismatch as an error condition, not just a shorter list. Another common error is using recursion for DFS on large graphs without considering Python's recursion limit (default 1000). For graphs with long chains, this causes a RecursionError. Prefer Kahn's algorithm or an iterative DFS with an explicit stack for production code.

A subtler pitfall is assuming the topological order is unique or sorted. Developers sometimes sort the result numerically, destroying the dependency guarantee. If you need determinism, use a heap during the algorithm, not a post-sort. Also, be careful with graph representation: using an adjacency matrix instead of an adjacency list blows up space to O(V²) and time to O(V²), which is catastrophic for sparse dependency graphs. Always use adjacency lists or edge lists for topological sort.

Best Practices for Production Code

Finally, remember that topological sort is not a silver bullet for scheduling with resource constraints. If tasks have durations, deadlines, or limited parallelism, you need to layer additional algorithms (critical path method, list scheduling) on top of the topological order. Treat the sort as the foundation, not the entire solution.

Interview Questions

Topological sort is a favorite in technical interviews because it tests graph fundamentals, cycle detection, and the ability to model real-world problems as DAGs. Below are common questions with high-level strategies.

1. Course Schedule I & II (LeetCode 207, 210)

Problem: Given numCourses and prerequisites where [a,b] means b must be taken before a, determine if you can finish all courses and return an order if possible. Strategy: This is direct Kahn's algorithm. Build the graph and indegree array, process the queue, and check if you processed all courses. For Course Schedule II, return the result list instead of just a boolean. The key is to correctly map prerequisites to edges (b → a) and to handle the case where multiple valid orders exist — any valid order is accepted.

2. Alien Dictionary (LeetCode 269)

Problem: Given words sorted lexicographically in an alien language, find the order of characters. Strategy: Compare adjacent words to infer ordering constraints. For example, "wrt" and "wrf" imply t → f. Build a graph of character dependencies and topologically sort it. Edge cases include invalid input where a longer word precedes its prefix (e.g., "abc" before "ab") which implies a cycle/invalid order, and characters that are not constrained (they can appear anywhere). This tests your ability to *construct* the graph before sorting.

3. Build Order / Project Dependencies

Problem: Given projects and dependencies, find a build order or report that no valid order exists. Strategy: Identical to course schedule but often presented with string project names instead of integers. Map names to indices, run Kahn's or DFS, and map back to names. Interviewers often ask for the lexicographically smallest build order or for the minimal number of parallel steps (levels), which requires heap or BFS-level variations. Be prepared to discuss time and space complexity and to handle disconnected projects.

4. Minimum Height Trees vs. Longest Path in DAG

Problem: Find the longest path in a DAG or determine the order to remove leaves iteratively. Strategy: For longest path, topologically sort first, then perform DP in that order: dist[v] = max(dist[v], dist[u] + weight(u,v)). This is O(V+E) and much faster than Bellman-Ford. For Minimum Height Trees, the leaf-removal process is essentially Kahn's algorithm in reverse (peeling layers). Recognizing that topological concepts apply even to undirected trees (by treating leaves as zero-indegree nodes) shows deeper understanding.

5. Detect Cycle in Directed Graph

Problem: Determine if a directed graph has a cycle. Strategy: While you can use DFS with three colors, you can also frame it as "can this graph be topologically sorted?" Run Kahn's algorithm and check if the result size < V. If so, a cycle exists. This approach is often cleaner iteratively and avoids recursion. Interviewers may ask you to return the cycle itself — in that case, DFS with parent pointers is more suitable because it can reconstruct the back edge path, whereas Kahn's only tells you that a cycle exists, not where it is.

Mastering these patterns — modeling problems as DAGs, choosing between Kahn's and DFS, handling lexicographic requirements, and combining topological order with DP — will prepare you for the vast majority of interview questions involving dependencies and ordering.

Frequently Asked Questions

What is Topological Sort and how does it work?

Topological Sort is a fundamental concept in computer science. It involves specific techniques and data organization methods designed to solve computational problems efficiently. Understanding the core mechanics helps developers choose the right approach for their applications.

What is the time complexity of Topological Sort?

The time complexity of Topological Sort varies depending on the specific operation and implementation. Common complexities range from O(1) constant time for basic operations to O(n log n) or O(n²) for more complex operations. The article above provides detailed complexity analysis for each operation.

When should I use Topological Sort in my projects?

Use Topological Sort when your application requires the specific performance characteristics it provides. Consider factors like data size, operation frequency, memory constraints, and whether the problem domain matches the strengths of Topological Sort. The real-world applications section above covers common use cases.

What are the advantages and disadvantages of Topological Sort?

The main advantages of Topological Sort include efficient performance for its target use cases and well-understood theoretical guarantees. Disadvantages may include implementation complexity, memory overhead, or performance limitations in certain edge cases. Always benchmark against alternatives for your specific workload.

How does Topological Sort compare to its alternatives?

Topological Sort offers distinct trade-offs compared to alternative approaches. While it excels in certain scenarios, other algorithms or data structures may be better suited for different problem constraints. The comparison table in the article provides a detailed analysis of these trade-offs.