565. Array Nesting
Problem Description
You have an integer array nums
of length n
that contains a permutation of all numbers from 0
to n-1
(each number appears exactly once).
For each starting index k
, you need to build a set s[k]
following these rules:
- Start with the element at index
k
: addnums[k]
to the set - Next, use the value you just added as the new index: add
nums[nums[k]]
to the set - Continue this pattern: add
nums[nums[nums[k]]]
, and so on - Stop when you would add a duplicate element (an element already in the set)
Essentially, you're following a chain of indices where each element points to the next index to visit, creating a cycle since the array is a permutation.
For example, if nums = [5,4,0,3,1,6,2]
:
- Starting at index
0
:s[0] = {5, 6, 2, 0}
(follow the chain: 0β5β6β2β0, stop before adding 0 again) - Starting at index
1
:s[1] = {4, 1}
(follow the chain: 1β4β1, stop before adding 1 again)
Your task is to find the maximum length among all possible sets s[k]
for k
from 0
to n-1
.
The solution uses a visited array to track which elements have already been part of a cycle. Since the array is a permutation, each element belongs to exactly one cycle. Once we've explored a cycle starting from any element in it, we don't need to explore it again from other starting points in the same cycle. The algorithm iterates through each unvisited index, follows its cycle while marking elements as visited, counts the cycle length, and keeps track of the maximum length found.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: Even though we have an array, we can model this as a graph problem. Each index
i
has a directed edge to indexnums[i]
. Since the array is a permutation, each node has exactly one outgoing edge and one incoming edge, forming one or more cycles.
Is it a tree?
- No: Trees don't have cycles, but our graph structure has cycles. When we follow the chain
nums[k] β nums[nums[k]] β ...
, we eventually return to a previously visited element, forming a cycle.
Is the problem related to directed acyclic graphs?
- No: Our graph has cycles (it's not acyclic). The permutation property guarantees that following the indices will create cycles.
Is the problem related to shortest paths?
- No: We're not looking for shortest paths between nodes. We want to find the longest cycle length.
Does the problem involve connectivity?
- Yes: We need to explore connected components (cycles) in the graph. Each cycle is a connected component where we follow edges until we return to the starting point.
Does the problem have small constraints?
- Yes: While the array can be up to length
10^5
, each element is visited at most once when exploring cycles, making it tractable.
DFS/backtracking?
- Yes: We use DFS to traverse each cycle. Starting from an unvisited node, we follow the edges (using DFS) until we complete the cycle, tracking the length of each cycle.
Conclusion: The flowchart suggests using a DFS approach. We treat the array as a graph where each index points to another index, and use DFS to explore cycles, keeping track of visited nodes to avoid reprocessing the same cycle. The maximum cycle length found during this traversal is our answer.
Intuition
The key insight is recognizing that this array forms a special graph structure. Since nums
is a permutation of [0, n-1]
, each index appears exactly once as a value. This means:
- Every index has exactly one outgoing edge (to
nums[index]
) - Every index has exactly one incoming edge (from some other index whose value is this index)
This creates a graph where every node has degree 2 (one in, one out), which means the graph must consist of one or more disjoint cycles. There are no dead ends or branching paths - just pure cycles.
When we start at any index k
and follow the chain k β nums[k] β nums[nums[k]] β ...
, we're essentially walking along one of these cycles. Since it's a cycle, we'll eventually return to where we started. The length of set s[k]
is simply the length of the cycle that contains index k
.
Here's the crucial realization: all indices in the same cycle will produce the same set length. If indices i
and j
are in the same cycle, then s[i]
and s[j]
will have the same length - they're just different starting points on the same circular path.
This means we don't need to check every possible starting index redundantly. Once we've explored a cycle from any starting point, we've found the length for all indices in that cycle. We can mark all nodes in that cycle as "visited" and skip them in future iterations.
The algorithm naturally follows:
- For each unvisited index, perform a DFS to trace through its cycle
- Mark all nodes in the cycle as visited while counting the cycle length
- Keep track of the maximum cycle length encountered
The time complexity is O(n)
because each element is visited exactly once across all DFS traversals, even though we might start multiple DFS calls. The space complexity is O(n)
for the visited array.
Learn more about Depth-First Search patterns.
Solution Approach
The implementation uses a visited array to efficiently track which elements have already been processed as part of a cycle exploration.
Data Structures Used:
vis
: A boolean array of sizen
to mark visited indicesres
: Variable to track the maximum cycle length found
Algorithm Steps:
-
Initialize tracking structures:
- Create a
vis
array of sizen
, initially allFalse
- Set
res = 0
to track the maximum cycle length
- Create a
-
Iterate through each index:
for i in range(n): if vis[i]: continue # Skip if already part of a processed cycle
-
For each unvisited index, trace its cycle:
- Start with
cur = nums[i]
(first element in the cycle) - Initialize cycle length
m = 1
- Mark
vis[cur] = True
- Start with
-
Follow the cycle until we return to the starting point:
while nums[cur] != nums[i]: cur = nums[cur] # Move to next element in cycle m += 1 # Increment cycle length vis[cur] = True # Mark as visited
The condition
nums[cur] != nums[i]
checks if we've completed the cycle. Whennums[cur] == nums[i]
, it means the next element would benums[i]
again (back to where we started), so we stop. -
Update the maximum cycle length:
res = max(res, m)
Why this works:
- Each element belongs to exactly one cycle due to the permutation property
- Once we've measured a cycle from any starting point within it, all elements in that cycle are marked as visited
- We never process the same cycle twice, ensuring
O(n)
time complexity - The visited array prevents redundant work while allowing us to find all distinct cycles
Example walkthrough with nums = [5,4,0,3,1,6,2]
:
- Start at index 0: Follow 0β5β6β2β0, cycle length = 4
- Start at index 1: Follow 1β4β1, cycle length = 2
- Start at index 3: Follow 3β3, cycle length = 1
- Indices 2, 4, 5, 6 are already visited, so skip them
- Maximum cycle length = 4
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with nums = [2,1,0,4,3]
:
Initial State:
vis = [False, False, False, False, False]
res = 0
Iteration 1: i = 0
vis[0]
is False, so we explore this cycle- Start with
cur = nums[0] = 2
, markvis[2] = True
,m = 1
- Follow the cycle:
nums[2] = 0
βnums[0] = 2
, so continuecur = nums[2] = 0
, markvis[0] = True
,m = 2
nums[0] = 2
=nums[0] = 2
, so stop
- Cycle: 0β2β0, length = 2
- Update
res = max(0, 2) = 2
vis = [True, False, True, False, False]
Iteration 2: i = 1
vis[1]
is False, so we explore this cycle- Start with
cur = nums[1] = 1
, markvis[1] = True
,m = 1
- Check:
nums[1] = 1
=nums[1] = 1
, so stop immediately - Cycle: 1β1 (self-loop), length = 1
res = max(2, 1) = 2
vis = [True, True, True, False, False]
Iteration 3: i = 2
vis[2]
is True (already visited in iteration 1), skip
Iteration 4: i = 3
vis[3]
is False, so we explore this cycle- Start with
cur = nums[3] = 4
, markvis[4] = True
,m = 1
- Follow the cycle:
nums[4] = 3
βnums[3] = 4
, so continuecur = nums[4] = 3
, markvis[3] = True
,m = 2
nums[3] = 4
=nums[3] = 4
, so stop
- Cycle: 3β4β3, length = 2
res = max(2, 2) = 2
vis = [True, True, True, True, True]
Iteration 5: i = 4
vis[4]
is True (already visited in iteration 4), skip
Final Result: Maximum cycle length = 2
The algorithm efficiently found three cycles:
- Cycle 1: {0, 2} with length 2
- Cycle 2: {1} with length 1
- Cycle 3: {3, 4} with length 2
Each element was visited exactly once, and we correctly identified the maximum cycle length as 2.
Solution Implementation
1class Solution:
2 def arrayNesting(self, nums: List[int]) -> int:
3 """
4 Find the longest length of a set S[k] where S[k] = {nums[k], nums[nums[k]], nums[nums[nums[k]]], ...}
5 subjected to the constraint that the set stops when a duplicate element is encountered.
6
7 Args:
8 nums: A list of integers where each element is an index pointing to another element
9
10 Returns:
11 The maximum length of any set S[k]
12 """
13 n = len(nums)
14 visited = [False] * n # Track which indices have been visited
15 max_length = 0
16
17 # Try starting from each unvisited index
18 for start_index in range(n):
19 if visited[start_index]:
20 continue
21
22 # Explore the cycle starting from current index
23 current_index = nums[start_index]
24 cycle_length = 1
25 visited[current_index] = True
26
27 # Follow the chain until we return to the starting element
28 while nums[current_index] != nums[start_index]:
29 current_index = nums[current_index]
30 cycle_length += 1
31 visited[current_index] = True
32
33 # Update maximum length found so far
34 max_length = max(max_length, cycle_length)
35
36 return max_length
37
1class Solution {
2 public int arrayNesting(int[] nums) {
3 int arrayLength = nums.length;
4 boolean[] visited = new boolean[arrayLength];
5 int maxCycleLength = 0;
6
7 // Check each index as a potential starting point for a cycle
8 for (int startIndex = 0; startIndex < arrayLength; startIndex++) {
9 // Skip if this index is already part of a previously found cycle
10 if (visited[startIndex]) {
11 continue;
12 }
13
14 // Start exploring the cycle from current index
15 int currentIndex = nums[startIndex];
16 int currentCycleLength = 1;
17 visited[currentIndex] = true;
18
19 // Follow the chain until we return to the starting element
20 while (nums[currentIndex] != nums[startIndex]) {
21 currentIndex = nums[currentIndex]; // Move to next index in the cycle
22 currentCycleLength++;
23 visited[currentIndex] = true; // Mark as visited
24 }
25
26 // Update maximum cycle length found so far
27 maxCycleLength = Math.max(maxCycleLength, currentCycleLength);
28 }
29
30 return maxCycleLength;
31 }
32}
33
1class Solution {
2public:
3 int arrayNesting(vector<int>& nums) {
4 int arraySize = nums.size();
5 vector<bool> visited(arraySize, false);
6 int maxCycleLength = 0;
7
8 // Iterate through each element as a potential starting point
9 for (int startIndex = 0; startIndex < arraySize; ++startIndex) {
10 // Skip if this element is already part of a previously found cycle
11 if (visited[startIndex]) {
12 continue;
13 }
14
15 // Start exploring the cycle from current index
16 int currentIndex = nums[startIndex];
17 int currentCycleLength = 1;
18 visited[currentIndex] = true;
19
20 // Follow the chain until we return to the starting element
21 // The cycle ends when nums[currentIndex] equals nums[startIndex]
22 while (nums[currentIndex] != nums[startIndex]) {
23 currentIndex = nums[currentIndex]; // Jump to next index in the cycle
24 ++currentCycleLength;
25 visited[currentIndex] = true;
26 }
27
28 // Update the maximum cycle length found so far
29 maxCycleLength = max(maxCycleLength, currentCycleLength);
30 }
31
32 return maxCycleLength;
33 }
34};
35
1function arrayNesting(nums: number[]): number {
2 const arraySize: number = nums.length;
3 const visited: boolean[] = new Array(arraySize).fill(false);
4 let maxCycleLength: number = 0;
5
6 // Iterate through each element as a potential starting point
7 for (let startIndex = 0; startIndex < arraySize; startIndex++) {
8 // Skip if this element is already part of a previously found cycle
9 if (visited[startIndex]) {
10 continue;
11 }
12
13 // Start exploring the cycle from current index
14 let currentIndex: number = nums[startIndex];
15 let currentCycleLength: number = 1;
16 visited[currentIndex] = true;
17
18 // Follow the chain until we return to the starting element
19 // The cycle ends when nums[currentIndex] equals nums[startIndex]
20 while (nums[currentIndex] !== nums[startIndex]) {
21 currentIndex = nums[currentIndex]; // Jump to next index in the cycle
22 currentCycleLength++;
23 visited[currentIndex] = true;
24 }
25
26 // Update the maximum cycle length found so far
27 maxCycleLength = Math.max(maxCycleLength, currentCycleLength);
28 }
29
30 return maxCycleLength;
31}
32
Time and Space Complexity
Time Complexity: O(n)
where n
is the length of the input array nums
.
The algorithm visits each element in the array exactly once. Although there's a nested while loop inside the for loop, each element is only processed once due to the vis
array marking visited elements. Once an element is marked as visited (vis[i] = True
), it will be skipped in future iterations of the outer loop. Therefore, the total number of operations across all iterations is bounded by n
, resulting in linear time complexity.
Space Complexity: O(n)
where n
is the length of the input array nums
.
The algorithm uses an additional boolean array vis
of size n
to track visited elements. This auxiliary array requires O(n)
space. The remaining variables (cur
, m
, res
, i
) use constant space O(1)
. Therefore, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Incorrectly marking the starting index as visited
The Problem:
A common mistake is marking visited[i] = True
(the starting index) instead of visited[nums[i]] = True
(the first element in the cycle). This causes an off-by-one error where you miss counting one element in each cycle.
Incorrect Code:
for i in range(n):
if visited[i]:
continue
visited[i] = True # WRONG: marking the starting index
current = nums[i]
cycle_length = 1
while nums[current] != nums[i]:
current = nums[current]
cycle_length += 1
visited[current] = True
Why it's wrong:
- Index
i
might not be part of the cycle that starts fromnums[i]
- For example, if
nums = [1, 0]
and we start at index 0:- The cycle from index 0 is: 0β1β0 (contains indices 0 and 1)
- But if we mark
visited[0] = True
initially and then only mark indices during traversal, we'd mark 0 and 1 - When we later try index 1, it's already marked visited, so we skip it
- However, this creates inconsistency in what "visited" means
Correct Approach:
for i in range(n):
if visited[i]:
continue
current = nums[i]
cycle_length = 1
visited[current] = True # Mark the first element in the cycle
while nums[current] != nums[i]:
current = nums[current]
cycle_length += 1
visited[current] = True
Pitfall 2: Using the wrong termination condition
The Problem:
Using while current != i
instead of while nums[current] != nums[i]
as the loop condition.
Incorrect Code:
while current != i: # WRONG: comparing index with value current = nums[current] cycle_length += 1 visited[current] = True
Why it's wrong:
current
holds a value from the array (which represents an index to jump to)i
is the starting index position- These two might never be equal if index
i
is not part of its own cycle - For example, with
nums = [1, 2, 0]
starting at index 0:- current starts as
nums[0] = 1
- We'd be checking if
1 != 0
, which is true - Next, current becomes
nums[1] = 2
- We'd check if
2 != 0
, which is true - Next, current becomes
nums[2] = 0
- We'd check if
0 != 0
, which is false, so we stop - This happens to work here, but it's conceptually wrong
- current starts as
Correct Approach: The condition should check if the next element in the sequence would be the same as the first element we added:
while nums[current] != nums[i]: # Correct: checking if we'd revisit the first element current = nums[current] cycle_length += 1 visited[current] = True
Pitfall 3: Not handling single-element cycles correctly
The Problem:
Failing to account for self-loops where nums[i] == i
.
Why it matters:
- If
nums[3] = 3
, this forms a cycle of length 1 - The standard traversal logic should handle this, but some implementations might accidentally skip or miscount these cases
Verification: The correct implementation naturally handles this case:
- Start at index 3 where
nums[3] = 3
- Set
current = nums[3] = 3
,cycle_length = 1
- Check condition:
nums[3] != nums[3]
isFalse
- Loop doesn't execute, returns
cycle_length = 1
β
Best Practice: Always test edge cases including:
- Single-element cycles (
nums[i] = i
) - Two-element cycles (
nums[i] = j
andnums[j] = i
) - Full array as one cycle
How many times is a tree node visited in a depth first search?
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!