title: Array Nesting
slug: array-nesting
difficulty: medium
leetcode_id: 565
leetcode_url: https://leetcode.com/problems/array-nesting/
categories:
  - arrays
patterns:
  - dfs

description: |
  You are given an integer array `nums` of length `n` where `nums` is a permutation of the numbers in the range `[0, n - 1]`.

  You should build a set `s[k] = {nums[k], nums[nums[k]], nums[nums[nums[k]]], ... }` subjected to the following rule:

  - The first element in `s[k]` starts with the selection of the element `nums[k]` of `index = k`.
  - The next element in `s[k]` should be `nums[nums[k]]`, and then `nums[nums[nums[k]]]`, and so on.
  - We stop adding right before a duplicate element occurs in `s[k]`.

  Return *the longest length of a set* `s[k]`.

constraints: |
  - `1 <= nums.length <= 10^5`
  - `0 <= nums[i] < nums.length`
  - All the values of `nums` are **unique**.

examples:
  - input: "nums = [5,4,0,3,1,6,2]"
    output: "4"
    explanation: "nums[0] = 5, nums[1] = 4, nums[2] = 0, nums[3] = 3, nums[4] = 1, nums[5] = 6, nums[6] = 2. One of the longest sets s[k]: s[0] = {nums[0], nums[5], nums[6], nums[2]} = {5, 6, 2, 0}."
  - input: "nums = [0,1,2]"
    output: "1"
    explanation: "Each element points to itself (nums[0]=0, nums[1]=1, nums[2]=2), so each set contains only one element."

explanation:
  intuition: |
    Imagine the array as a directed graph where each index `i` has an edge pointing to index `nums[i]`. Since `nums` is a permutation of `[0, n-1]`, every index appears exactly once as a value. This means:

    - Every node has exactly one outgoing edge (to `nums[i]`)
    - Every node has exactly one incoming edge (from the index where it appears as a value)

    This structure guarantees that the graph consists of **disjoint cycles**. Think of it like a treasure hunt where each clue points to the next location, and eventually you end up back where you started.

    The problem asks us to find the longest cycle. The key insight is that if two indices belong to the same cycle, starting from either one will trace the same cycle. So once we've explored a cycle starting from any index, we never need to revisit any index in that cycle again.

    This is why marking visited indices works: we're essentially finding all disjoint cycles and tracking the longest one.

  approach: |
    We solve this using **Cycle Detection with Visited Tracking**:

    **Step 1: Initialise tracking variables**

    - `max_length`: Set to `0` to track the longest cycle found
    - `visited`: A set (or array) to mark indices we've already explored

    &nbsp;

    **Step 2: Iterate through each starting index**

    - For each index `i` from `0` to `n-1`:
    - If `i` is already visited, skip it (it's part of a cycle we've already counted)
    - If not visited, start exploring the cycle from `i`

    &nbsp;

    **Step 3: Explore each cycle**

    - Starting from index `i`, follow the chain: `i -> nums[i] -> nums[nums[i]] -> ...`
    - Count each step and mark each visited index
    - Stop when we return to an already-visited index (the cycle is complete)
    - Update `max_length` if this cycle is longer

    &nbsp;

    **Step 4: Return the result**

    - After checking all indices, return `max_length`

    &nbsp;

    The key optimisation is that we only visit each index once across all iterations. Once an index is marked visited, we skip it in future iterations because we already know its cycle length.

  common_pitfalls:
    - title: Recomputing Cycles for Each Index
      description: |
        A naive approach might trace the full cycle for every starting index, even if that index was already visited from a previous starting point.

        For example, if indices 0, 5, 6, 2 form a cycle, starting from index 5 would trace the same cycle as starting from 0. Without visited tracking, you'd count this cycle 4 times.

        This leads to **O(n^2) time complexity** in the worst case. With `n = 10^5`, this could mean 10 billion operations.
      wrong_approach: "Trace full cycle for every starting index"
      correct_approach: "Mark visited indices and skip them in future iterations"

    - title: Using Extra Space for Cycle Storage
      description: |
        You don't need to actually store the set `s[k]` for each starting index. We only care about the *length* of each cycle, not its contents.

        Storing all elements would use O(n) space per cycle exploration. Instead, just count steps while following the chain.
      wrong_approach: "Store each cycle's elements in a list or set"
      correct_approach: "Only count the cycle length, don't store elements"

    - title: Not Recognising the Permutation Property
      description: |
        The constraint that `nums` is a permutation is crucial. It guarantees:

        - No index points to an out-of-bounds location
        - Every index is pointed to by exactly one other index
        - The graph structure is guaranteed to be disjoint cycles

        If you miss this, you might add unnecessary bounds checking or fail to recognise why the visited optimisation works correctly.

  key_takeaways:
    - "**Cycle detection pattern**: When array values are indices into the same array, think of it as a graph where `nums[i]` defines edges"
    - "**Permutation implies disjoint cycles**: In a permutation, following indices always forms closed cycles with no branches"
    - "**Visit once optimisation**: When finding the longest cycle among disjoint cycles, each element only needs to be visited once total"
    - "**Similar problems**: This pattern appears in Find the Duplicate Number, Linked List Cycle, and problems involving functional graphs"

  time_complexity: "O(n). Each index is visited at most once across all cycle explorations, giving linear time."
  space_complexity: "O(n). We use a visited set/array of size `n` to track explored indices. Can be reduced to O(1) by modifying the input array in-place."

solutions:
  - approach_name: Cycle Detection with Visited Set
    is_optimal: true
    code: |
      def array_nesting(nums: list[int]) -> int:
          n = len(nums)
          visited = set()
          max_length = 0

          for i in range(n):
              # Skip if this index is already part of a known cycle
              if i in visited:
                  continue

              # Explore the cycle starting from index i
              current = i
              cycle_length = 0

              while current not in visited:
                  # Mark this index as visited
                  visited.add(current)
                  # Move to the next index in the chain
                  current = nums[current]
                  cycle_length += 1

              # Update the maximum cycle length found
              max_length = max(max_length, cycle_length)

          return max_length
    explanation: |
      **Time Complexity:** O(n) — Each index is visited exactly once across all iterations.

      **Space Complexity:** O(n) — The visited set stores up to n indices.

      We iterate through each index and explore its cycle if not yet visited. The visited set ensures we never process the same index twice, making the total work linear regardless of cycle structure.

  - approach_name: In-Place Marking
    is_optimal: true
    code: |
      def array_nesting(nums: list[int]) -> int:
          max_length = 0
          n = len(nums)

          for i in range(n):
              # Skip if already visited (marked with -1)
              if nums[i] == -1:
                  continue

              # Explore the cycle starting from index i
              current = i
              cycle_length = 0

              while nums[current] != -1:
                  # Save the next index before marking
                  next_idx = nums[current]
                  # Mark as visited by setting to -1
                  nums[current] = -1
                  # Move to next index
                  current = next_idx
                  cycle_length += 1

              max_length = max(max_length, cycle_length)

          return max_length
    explanation: |
      **Time Complexity:** O(n) — Each index is visited exactly once.

      **Space Complexity:** O(1) — We modify the input array in-place instead of using a separate visited structure.

      This optimisation marks visited indices by setting them to `-1` (an invalid value). This eliminates the need for a separate visited set but modifies the input array. Use this when space is critical and input modification is acceptable.

  - approach_name: Brute Force
    is_optimal: false
    code: |
      def array_nesting(nums: list[int]) -> int:
          max_length = 0
          n = len(nums)

          for i in range(n):
              # Start a new cycle exploration from index i
              seen = set()
              current = i

              # Follow the chain until we see a repeat
              while current not in seen:
                  seen.add(current)
                  current = nums[current]

              # Update max with this cycle's length
              max_length = max(max_length, len(seen))

          return max_length
    explanation: |
      **Time Complexity:** O(n^2) — In the worst case, we might trace O(n) elements for each of n starting indices.

      **Space Complexity:** O(n) — The seen set for each iteration can grow to n elements.

      This approach creates a fresh `seen` set for each starting index, potentially retracing the same cycles multiple times. While correct, it's inefficient because it doesn't share visited information across iterations. For example, if all elements form one big cycle, we'd trace all n elements n times.
