Files
codetutor/backend/data/questions/array-nesting.yaml

219 lines
9.5 KiB
YAML

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.