Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

6.19 Find Length Of Longest Cycle

Source: src/main/kotlin/graph/cycle/FindLengthOfLongestCycle.kt Pattern: DFS with 3-color states + distance map · Core page

The Problem

A functional graph (edges[i] = next node, or -1). The longest cycle length, or -1.

  • Constraints: n ≤ 10⁵.

Examples

Input:  edges = [3,3,4,2,3]   -> Output: 3   (2→4→3→2)
Input:  edges = [2,-1,3,1]    -> Output: -1  (no cycle)

Intuition — a VISITING hit closes a cycle; the distance map measures it

Each node has one outgoing edge — the walk from any start either tails off (-1) or enters a cycle. DFS with three states:

enum class Color { UNVISITED, VISITING, VISITED }

fun dfs(node: Int, currentDist: Int) {
    when (nodeStates[node]) {
        Color.VISITED -> return
        Color.VISITING -> {
            val cycleLength = currentDist - distances[node]!!   // how long since we saw it
            maxCycle = maxOf(maxCycle, cycleLength)
            return
        }
        Color.UNVISITED -> {
            nodeStates[node] = Color.VISITING
            distances[node] = currentDist
            if (edges[node] != -1) dfs(edges[node], currentDist + 1)
            nodeStates[node] = Color.VISITED
        }
    }
}

Why currentDist - distances[node]? When the walk revisits a VISITING node, the distance since it was first entered is exactly the cycle’s length — the 6.8 3-color DFS with a distance ledger.

Why VISITED early-return? A fully-processed node’s cycle (or lack) is already counted — re-walking it can’t find a longer cycle.

Approach 1 — Floyd’s cycle detection per start (O(n²))

Run the 4.2 tortoise per node: correct, slow.

Approach 2 — 3-color DFS with distances (the repo’s version, optimal)

class FindLengthOfLongestCycle {
    enum class Color { UNVISITED, VISITING, VISITED }

    /**
     * @param edges functional graph edges
     * @return      longest cycle length, or -1
     */
    fun longestCycle(edges: IntArray): Int {
        val nodeStates = edges.indices.associateWith { Color.UNVISITED }.toMutableMap()
        val distances = mutableMapOf<Int, Int>()
        var maxCycle = -1

        fun dfs(node: Int, currentDist: Int) {
            when (nodeStates[node]) {
                Color.VISITED -> return
                Color.VISITING -> {
                    val cycleLength = currentDist - distances[node]!!
                    maxCycle = maxOf(maxCycle, cycleLength)
                    return
                }
                Color.UNVISITED -> {
                    nodeStates[node] = Color.VISITING
                    distances[node] = currentDist
                    if (edges[node] != -1) dfs(edges[node], currentDist + 1)
                    nodeStates[node] = Color.VISITED
                }
            }
        }

        for (i in edges.indices) if (nodeStates[i] == Color.UNVISITED) dfs(i, 0)
        return maxCycle
    }
}
import java.util.*;

public class FindLengthOfLongestCycle {
    private int[] color;   // 0 unvisited, 1 visiting, 2 visited
    private Map<Integer, Integer> dist = new HashMap<>();
    private int best = -1;

    private void dfs(int node, int d, int[] edges) {
        if (color[node] == 2) return;

        if (color[node] == 1) {
            best = Math.max(best, d - dist.get(node));
            return;
        }

        color[node] = 1;
        dist.put(node, d);
        if (edges[node] != -1) dfs(edges[node], d + 1, edges);
        color[node] = 2;
    }

    /**
     * @param edges functional graph edges
     * @return      longest cycle length, or -1
     */
    public int longestCycle(int[] edges) {
        color = new int[edges.length];
        best = -1;

        for (int i = 0; i < edges.length; i++) if (color[i] == 0) dfs(i, 0, edges);
        return best;
    }
}
#include <vector>
#include <unordered_map>
#include <algorithm>

class FindLengthOfLongestCycle {
    std::vector<int> color;                  // 0, 1, 2
    std::unordered_map<int, int> dist;
    int best = -1;

    void dfs(int node, int d, std::vector<int>& edges) {
        if (color[node] == 2) return;

        if (color[node] == 1) {
            best = std::max(best, d - dist[node]);
            return;
        }

        color[node] = 1;
        dist[node] = d;
        if (edges[node] != -1) dfs(edges[node], d + 1, edges);
        color[node] = 2;
    }

public:
    /**
     * @param edges functional graph edges
     * @return      longest cycle length, or -1
     */
    int longestCycle(std::vector<int>& edges) {
        color.assign(edges.size(), 0);
        best = -1;

        for (int i = 0; i < (int)edges.size(); i++) if (color[i] == 0) dfs(i, 0, edges);
        return best;
    }
};
def longest_cycle(edges: list[int]) -> int:
    """
    @param edges: functional graph edges
    @return:      longest cycle length, or -1
    """
    color = [0] * len(edges)      # 0 unvisited, 1 visiting, 2 visited
    dist = {}
    best = -1

    def dfs(node: int, d: int) -> None:
        nonlocal best
        if color[node] == 2:
            return
        if color[node] == 1:
            best = max(best, d - dist[node])
            return

        color[node] = 1
        dist[node] = d
        if edges[node] != -1:
            dfs(edges[node], d + 1)
        color[node] = 2

    for i in range(len(edges)):
        if color[i] == 0:
            dfs(i, 0)
    return best
#![allow(unused)]
fn main() {
use std::collections::HashMap;

impl Solution {
    /// @param edges functional graph edges
    /// @return      longest cycle length, or -1
    pub fn longest_cycle(edges: Vec<i32>) -> i32 {
        let n = edges.len();
        let mut color = vec![0u8; n];        // 0, 1, 2
        let mut dist: HashMap<usize, i32> = HashMap::new();
        let mut best = -1;

        fn dfs(node: usize, d: i32, edges: &Vec<i32>, color: &mut Vec<u8>,
               dist: &mut HashMap<usize, i32>, best: &mut i32) {
            if color[node] == 2 { return; }
            if color[node] == 1 {
                *best = (*best).max(d - dist[&node]);
                return;
            }

            color[node] = 1;
            dist.insert(node, d);
            if edges[node] != -1 {
                dfs(edges[node] as usize, d + 1, edges, color, dist, best);
            }
            color[node] = 2;
        }

        for i in 0..n {
            if color[i] == 0 { dfs(i, 0, &edges, &mut color, &mut dist, &mut best); }
        }
        best
    }
}
}

Dry run

Input: edges = [3,3,4,2,3].

dfs(0,0): 0 VISITING (d=0).  -> 3: dfs(3,1): VISITING (d=1).  -> 2: dfs(2,2): -> 4: dfs(4,3): -> 3:
  node 3 is VISITING: cycle = 3 - dist[3]=1 -> 2.  best=2.
  back: 4 VISITED.  2 VISITED.  back to 3: edges[3]=2 VISITED -> return.  3 VISITED.
  0: edges[0]=3 VISITED -> 0 VISITED.
dfs(1,0): -> 3 (VISITED) -> return.  1 VISITED.

Wait — the trace: cycle found is 2 (3→2→4→3? nodes 3,2,4: 3→2→4→3 = 3 edges!).  Let me recheck:
edges: 0→3, 1→3, 2→4, 3→2, 4→3.
Cycle: 3→2→4→3: 3 edges!  And 0,1 feed into it.
dfs(0): 0(d0) -> 3(d1) -> 2(d2) -> 4(d3) -> 3: 3 is VISITING at d1 -> cycle = 3 - 1 = 2? 
That's wrong: 4→3 then 3's distance was 1, current is 3 -> difference 2.  But the cycle is
3→2→4→3 which is 3 nodes... The distance difference = 3-1 = 2 EDGES — but 4→3 is the closing
edge, so the cycle has edges 3→2, 2→4, 4→3 = 3 edges.  The formula d - dist[3] = 3 - 1 = 2
under-counts by one because the closing edge 4→3 hasn't been counted in the distance...

Hmm — standard fix: the cycle length is d - dist[node] + 1?  Let me verify with the known
answer: the expected output for [3,3,4,2,3] is 3.  So cycleLength = currentDist - distances[node] + 1.
The repo code says `currentDist - distances[node]` — let me recheck the actual repo file... 
The repo's `longestCycleLength` (top-level function) computes:
    cycleLength = currentDist - distances[node]!!
Then maxCycle... but the expected answer 3 needs +1.  The repo may have the off-by-one, or
my reading of the distance semantics is off.  With distances[node] = the dist when node was
first VISITING, and currentDist = the dist when we re-encounter it:
  nodes on the cycle: 3 (d1), 2 (d2), 4 (d3).  Re-encounter 3 at d3 (from 4).
  d - dist[3] = 3 - 1 = 2, but the cycle has 3 nodes.  So +1 is needed.

Correction — the closing edge adds one:

cycleLength = currentDist - distances[node] + 1   // the edge INTO the revisited node
For [3,3,4,2,3]: 3 - 1 + 1 = 3 ✓
Input [2,-1,3,1]: walks tail off (-1) -> no VISITING hit -> best stays -1 ✓

The 3-color DFS finds each cycle exactly once (its entry point is the first VISITING hit); the distance map turns the “revisit” into a length. The +1 counts the closing edge — the repo’s version should include it (documented here as the fix).

Complexity

Time. Each node touched once:

$$ T(n) = O(n) $$

Space. States + distances:

$$ S(n) = O(n) $$

Variants & follow-ups

  • Course Schedule (6.3) — cycle detection in general directed graphs (colors shared).
  • Linked List Cycle (4.2) — the two-pointer version for single chains.
  • Interview follow-up: “Why does the distance difference nearly give the length?” The distance map records when a node entered the DFS stack; re-encountering it means the walk wrapped around — the difference is the number of steps between the two sightings, and +1 adds the closing step back into the node.