{"id":4098,"date":"2024-10-17T16:29:43","date_gmt":"2024-10-17T16:29:43","guid":{"rendered":"https:\/\/algocademy.com\/blog\/mastering-the-longest-path-problem-in-graphs-a-leetcode-challenge\/"},"modified":"2026-10-15T10:34:49","modified_gmt":"2026-10-15T10:34:49","slug":"mastering-the-longest-path-problem-in-graphs-a-leetcode-challenge","status":"publish","type":"post","link":"https:\/\/algocademy.com\/blog\/mastering-the-longest-path-problem-in-graphs-a-leetcode-challenge\/","title":{"rendered":"Longest Path Problem in Graphs: DAG Solution and LeetCode Practice"},"content":{"rendered":"<p>The longest path problem asks for the path with the most edges (or the largest total weight) in a graph. On a directed acyclic graph (DAG) you solve it in O(V + E) with a topological order and a small DP array. On a general graph with cycles it is NP-hard, so no known algorithm is fast on every input. Most interview problems that look like &#8220;longest path&#8221; are the DAG case in disguise: a grid with increasing values, a course schedule, or a tree.<\/p>\n<h2>Why it is harder than shortest path<\/h2>\n<p>Shortest path has polynomial algorithms (BFS, Dijkstra, Bellman-Ford). Longest <em>simple<\/em> path, where no node repeats, is NP-hard on general graphs, because it contains the Hamiltonian path problem: a path through all <code>n<\/code> nodes exists exactly when the longest simple path has <code>n - 1<\/code> edges.<\/p>\n<p>The naive idea of &#8220;negate the weights and run shortest path&#8221; fails. With cycles you get negative cycles, and the walk can loop forever. Without cycles, the idea works, but you don&#8217;t need it: a DAG has a much simpler solution.<\/p>\n<h2>Longest path in a DAG<\/h2>\n<p>In a DAG every edge goes &#8220;forward&#8221; in some ordering of the nodes (a topological order). So if you process nodes in that order, by the time you reach a node all the paths that can end at it are already accounted for. Define <code>dist[v]<\/code> as the number of edges on the longest path that ends at <code>v<\/code>. Start every node at 0 and relax each edge <code>u -> v<\/code> with <code>dist[v] = max(dist[v], dist[u] + 1)<\/code>. The answer is the largest value in <code>dist<\/code>.<\/p>\n<p>Kahn&#8217;s algorithm (repeatedly remove nodes with in-degree 0) gives the topological order and does the DP in one pass. It has a bonus: if fewer than <code>n<\/code> nodes get processed, the graph has a cycle, so you can reject invalid input instead of returning a wrong answer.<\/p>\n<pre><code class=\"language-python\">from collections import deque\n\n\ndef longest_path_dag(n, edges):\n    \"\"\"Number of edges on the longest path in a DAG with nodes 0..n-1.\n    Returns -1 if the graph has a cycle.\"\"\"\n    if n == 0:\n        return 0\n    graph = [[] for _ in range(n)]\n    indegree = [0] * n\n    for u, v in edges:\n        graph[u].append(v)\n        indegree[v] += 1\n\n    queue = deque(i for i in range(n) if indegree[i] == 0)\n    dist = [0] * n          # dist[v] = longest path (in edges) ending at v\n    processed = 0\n    while queue:\n        u = queue.popleft()\n        processed += 1\n        for v in graph[u]:\n            dist[v] = max(dist[v], dist[u] + 1)\n            indegree[v] -= 1\n            if indegree[v] == 0:\n                queue.append(v)\n    if processed &lt; n:\n        return -1\n    return max(dist)\n\n\nprint(longest_path_dag(6, [(0, 1), (0, 2), (1, 3), (2, 3), (3, 4)]))  # 3\nprint(longest_path_dag(3, []))                                        # 0\nprint(longest_path_dag(3, [(0, 1), (1, 2), (2, 0)]))                  # -1\nprint(longest_path_dag(4, [(0, 1), (2, 3)]))                          # 1\n<\/code><\/pre>\n<p>These print 3, 0, -1 and 1. In the first graph the longest path is <code>0 -> 1 -> 3 -> 4<\/code>, with 3 edges. The last graph is disconnected, and the answer is still correct because every in-degree-0 node starts the queue.<\/p>\n<h3>Complexity<\/h3>\n<p>Time is O(V + E): each node enters the queue once and each edge is relaxed once. Space is O(V + E) for the adjacency list plus O(V) for the in-degree, distance and queue arrays.<\/p>\n<h3>Memoized DFS version<\/h3>\n<p>You can get the same result top-down: the longest path starting at <code>u<\/code> is 0 if <code>u<\/code> has no outgoing edges, otherwise 1 plus the best of its neighbors. Cache the result per node. This is the form to reach for when the graph is implicit, as in grid problems. Python&#8217;s default recursion limit is about 1000 frames, so a long chain of nodes needs an iterative version (like Kahn&#8217;s above) or a raised limit.<\/p>\n<pre><code class=\"language-python\">from functools import lru_cache\n\n\ndef longest_dfs(n, edges):\n    g = [[] for _ in range(n)]\n    for u, v in edges:\n        g[u].append(v)\n\n    @lru_cache(None)\n    def f(u):\n        return max((f(v) + 1 for v in g[u]), default=0)\n\n    return max((f(i) for i in range(n)), default=0)\n\n\nprint(longest_dfs(6, [(0, 1), (0, 2), (1, 3), (2, 3), (3, 4)]))  # 3\n<\/code><\/pre>\n<p>This version assumes the graph is acyclic. On a cycle it recurses without end, so check for cycles first if the input is not guaranteed to be a DAG (see <a href=\"https:\/\/algocademy.com\/blog\/algorithms-for-detecting-cycles-in-graphs-a-comprehensive-guide\/\">detecting cycles in graphs<\/a>).<\/p>\n<h2>Nodes or edges? Read the statement<\/h2>\n<p>The most common wrong answer is off by one. A path with <code>k<\/code> nodes has <code>k - 1<\/code> edges, and problems differ on which they ask for. A single node is a path of length 0 in edges and 1 in nodes. Check this against the examples before you code. Also decide up front what an empty graph returns; the code above returns 0.<\/p>\n<h2>The same idea with weights: Parallel Courses III<\/h2>\n<p>When nodes or edges carry weights, you add the weight instead of 1. LeetCode 2050, Parallel Courses III, is the standard example: each course has a duration in months, prerequisites form a DAG, you can take any number of courses in parallel, and you need the minimum total time. That time is the longest weighted path through the prerequisite graph. Here <code>finish[c]<\/code> is the earliest month by which course <code>c<\/code> can be finished.<\/p>\n<pre><code class=\"language-python\">from collections import deque\n\n\ndef minimum_time(n, relations, time):\n    graph = [[] for _ in range(n + 1)]\n    indegree = [0] * (n + 1)\n    for a, b in relations:          # a must finish before b starts\n        graph[a].append(b)\n        indegree[b] += 1\n    finish = [0] * (n + 1)\n    queue = deque()\n    for c in range(1, n + 1):\n        finish[c] = time[c - 1]\n        if indegree[c] == 0:\n            queue.append(c)\n    while queue:\n        u = queue.popleft()\n        for v in graph[u]:\n            finish[v] = max(finish[v], finish[u] + time[v - 1])\n            indegree[v] -= 1\n            if indegree[v] == 0:\n                queue.append(v)\n    return max(finish)\n\n\nprint(minimum_time(3, [[1, 3], [2, 3]], [3, 2, 5]))                              # 8\nprint(minimum_time(5, [[1, 5], [2, 5], [3, 5], [3, 4], [4, 5]], [1, 2, 3, 4, 5]))  # 12\n<\/code><\/pre>\n<p>This is also how the critical path in project scheduling is computed, and the same shape appears in build systems: the longest dependency chain sets the minimum build time even with unlimited parallel workers.<\/p>\n<h2>LeetCode problems that are longest path in disguise<\/h2>\n<ul>\n<li><strong>329. Longest Increasing Path in a Matrix.<\/strong> Cells are nodes, with an edge to a neighbor that has a larger value. Strictly increasing values mean no cycles, so it is a DAG and memoized DFS is O(m * n).<\/li>\n<li><strong>543. Diameter of Binary Tree.<\/strong> The longest path between any two nodes in a tree, counted in edges. It does not have to pass through the root.<\/li>\n<li><strong>2050. Parallel Courses III.<\/strong> Weighted longest path in a DAG, shown above.<\/li>\n<li><strong>1857. Largest Color Value in a Directed Graph.<\/strong> Topological order with a DP table per node and color, and a cycle check that returns -1.<\/li>\n<\/ul>\n<h3>Tree diameter<\/h3>\n<p>For each node, the longest path that bends at it is <code>left_depth + right_depth<\/code>. Return the depth upward and keep the best bend seen so far.<\/p>\n<pre><code class=\"language-python\">class TreeNode:\n    def __init__(self, val=0, left=None, right=None):\n        self.val, self.left, self.right = val, left, right\n\n\ndef diameter(root):\n    best = 0\n\n    def depth(node):\n        nonlocal best\n        if not node:\n            return 0\n        l, r = depth(node.left), depth(node.right)\n        best = max(best, l + r)\n        return 1 + max(l, r)\n\n    depth(root)\n    return best\n\n\nt = TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(3))\nprint(diameter(t))  # 3  (4 -> 2 -> 1 -> 3, or 5 -> 2 -> 1 -> 3)\n<\/code><\/pre>\n<p>To practice this one step by step, there is a <a href=\"https:\/\/algocademy.com\/link\/?lang=py&amp;problem=binary-tree-diameter&amp;solution=1\">Binary Tree Diameter lesson<\/a> on AlgoCademy.<\/p>\n<h3>Longest increasing path in a matrix<\/h3>\n<pre><code class=\"language-python\">def longest_increasing_path(matrix):\n    if not matrix or not matrix[0]:\n        return 0\n    m, n = len(matrix), len(matrix[0])\n    memo = {}\n\n    def dfs(i, j):\n        if (i, j) in memo:\n            return memo[(i, j)]\n        best = 1                      # the cell itself\n        for di, dj in ((1, 0), (-1, 0), (0, 1), (0, -1)):\n            x, y = i + di, j + dj\n            if 0 &lt;= x &lt; m and 0 &lt;= y &lt; n and matrix[x][y] &gt; matrix[i][j]:\n                best = max(best, 1 + dfs(x, y))\n        memo[(i, j)] = best\n        return best\n\n    return max(dfs(i, j) for i in range(m) for j in range(n))\n\n\nprint(longest_increasing_path([[9, 9, 4], [6, 6, 8], [2, 1, 1]]))  # 4\nprint(longest_increasing_path([[3, 4, 5], [3, 2, 6], [2, 2, 1]]))  # 4\n<\/code><\/pre>\n<p>Both grids give 4 (the first is <code>1 -> 2 -> 6 -> 9<\/code>). Without the memo the DFS re-explores the same cells exponentially often; with it, each cell is computed once. If memoization is new to you, see <a href=\"https:\/\/algocademy.com\/blog\/dynamic-programming-mastering-tabulation-and-memoization\/\">tabulation and memoization<\/a> or the <a href=\"https:\/\/algocademy.com\/link\/?lang=py&amp;problem=memoization&amp;solution=1\">Memoization lesson<\/a>. A closely related write-up is <a href=\"https:\/\/algocademy.com\/blog\/longest-path-in-a-dag-mastering-graph-algorithms-for-technical-interviews\/\">longest path in a DAG<\/a>.<\/p>\n<h2>Common mistakes<\/h2>\n<ul>\n<li><strong>Running the DAG algorithm on a graph with cycles.<\/strong> Kahn&#8217;s version detects it; plain DFS recursion does not.<\/li>\n<li><strong>Marking nodes visited and never revisiting them in a longest-path DFS.<\/strong> A visited set is right for reachability, not for longest path; use a memo of results instead, which is only valid on a DAG.<\/li>\n<li><strong>Mixing up nodes and edges<\/strong> in the final answer, as described above.<\/li>\n<li><strong>Forgetting disconnected parts.<\/strong> Start from every node with in-degree 0 (Kahn) or loop over all start nodes (DFS).<\/li>\n<li><strong>Recursion depth.<\/strong> A path graph with 100,000 nodes breaks recursive DFS in Python; use the iterative version.<\/li>\n<\/ul>\n<h2>If the graph has cycles<\/h2>\n<p>If the problem allows cycles and wants a simple path, you are in NP-hard territory. In an interview that means one of three things: the input is small enough for backtracking (exponential, with a visited set that you undo), the graph has extra structure (a tree, a DAG, a bitmask DP over at most about 20 nodes), or you misread the problem. Say so out loud, and ask about constraints before choosing an approach.<\/p>\n<p>For more graph practice, <a href=\"https:\/\/app.algocademy.com\/\">AlgoCademy<\/a> has a Graphs section with guided problems, including DFS and BFS exercises, in its Python course.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Find the longest path in a graph: why it is NP-hard in general, how topological sort plus DP solves it on a DAG, with tested Python and LeetCode problems.<\/p>\n","protected":false},"author":1,"featured_media":4097,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[23],"tags":[],"class_list":["post-4098","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-problem-solving"],"_links":{"self":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/4098"}],"collection":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/comments?post=4098"}],"version-history":[{"count":1,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/4098\/revisions"}],"predecessor-version":[{"id":8721,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/posts\/4098\/revisions\/8721"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/media\/4097"}],"wp:attachment":[{"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/media?parent=4098"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/categories?post=4098"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/algocademy.com\/blog\/wp-json\/wp\/v2\/tags?post=4098"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}