2501. Longest Square Streak in an Array
Problem Description
You need to find the longest "square streak" in an integer array nums
.
A square streak is a subsequence that meets two conditions:
- It contains at least 2 elements
- When sorted, each element (except the first) equals the square of the previous element
For example, [2, 4, 16]
is a valid square streak because 4 = 2²
and 16 = 4²
.
The task is to return the length of the longest square streak found in nums
. If no valid square streak exists, return -1
.
Key Points:
- A subsequence maintains the relative order of elements from the original array but doesn't need to be contiguous
- The subsequence must form a chain where each number is the square of the previous one
- The minimum valid length is 2
Solution Explanation:
The solution uses a hash table approach. First, all elements from nums
are stored in a set s
for O(1) lookup. Then, for each number x
in the array, it attempts to build a square streak starting from that number:
- Start with current number
x
and countert = 0
- While
x
exists in the set:- Square the current number:
x = x * x
- Increment the counter
t
- Square the current number:
- If
t > 1
(meaning we found at least 2 elements in the streak), update the maximum answer
The algorithm tracks the longest streak found across all starting points and returns it, or -1
if no valid streak exists.
Intuition
The key insight is that we need to find chains where each element is the square of the previous one. Since squaring grows numbers very quickly (e.g., 2 → 4 → 16 → 256 → 65536
), these chains tend to be relatively short.
We can think of this problem as finding the longest path in a directed graph where there's an edge from a
to b
if b = a²
and both exist in our array. Instead of building the entire graph, we can explore paths on-the-fly.
The natural approach is to try each number as a potential starting point of a streak. For each starting number, we keep squaring it and checking if the result exists in our array. This continues until we reach a number whose square isn't in the array.
Why use a hash set? Because we need to repeatedly check "does this squared value exist in the array?" A hash set gives us O(1) lookups, making the checking process efficient.
An important observation: we don't need to worry about finding the "optimal" starting point first. We can simply try every number as a potential start and keep track of the maximum streak length found. Even if we start from a middle element of a potential streak, we'll eventually try the actual first element and find the full length.
The squaring operation naturally ensures the sequence is increasing, so we don't need to explicitly sort or maintain order - we just follow the chain of squares until it breaks.
Learn more about Binary Search, Dynamic Programming and Sorting patterns.
Solution Approach
The implementation uses a hash table (set) combined with enumeration to find the longest square streak.
Step 1: Create a Hash Set
s = set(nums)
We convert the array into a set for O(1) lookup time. This allows us to quickly check if a squared value exists in the original array.
Step 2: Initialize the Answer
ans = -1
Start with -1
to handle the case where no valid square streak exists.
Step 3: Enumerate Each Starting Point
for x in nums:
Try each element in the array as a potential starting point for a square streak.
Step 4: Build the Square Streak
For each starting element x
:
t = 0 while x in s: x *= x t += 1
- Initialize a counter
t
to track the streak length - Keep squaring
x
(usingx *= x
which is equivalent tox = x * x
) - Check if the squared value exists in the set
- Increment the counter for each valid element found
- Continue until we reach a value whose square isn't in the set
Step 5: Update the Maximum
if t > 1:
ans = max(ans, t)
Only update the answer if we found a streak of length at least 2 (since a valid square streak requires minimum 2 elements).
Time Complexity: O(n × log(log(M))) where n is the array length and M is the maximum value. The log(log(M)) factor comes from the fact that squaring grows exponentially fast, so we can only square a number a very limited number of times before it exceeds any reasonable bound.
Space Complexity: O(n) for storing the hash set.
The algorithm efficiently finds all possible square streaks by trying each element as a starting point and following the chain of squares as far as possible.
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 walk through the algorithm with nums = [4, 3, 6, 16, 8, 2]
.
Step 1: Create hash set
s = {4, 3, 6, 16, 8, 2}
Step 2: Try each element as a starting point
Starting with x = 4
:
t = 0
- Is
4
in set? Yes →x = 4 * 4 = 16
,t = 1
- Is
16
in set? Yes →x = 16 * 16 = 256
,t = 2
- Is
256
in set? No → stop t = 2 > 1
, soans = 2
Starting with x = 3
:
t = 0
- Is
3
in set? Yes →x = 3 * 3 = 9
,t = 1
- Is
9
in set? No → stop t = 1
is not > 1, so no update
Starting with x = 6
:
t = 0
- Is
6
in set? Yes →x = 6 * 6 = 36
,t = 1
- Is
36
in set? No → stop t = 1
is not > 1, so no update
Starting with x = 16
:
t = 0
- Is
16
in set? Yes →x = 16 * 16 = 256
,t = 1
- Is
256
in set? No → stop t = 1
is not > 1, so no update
Starting with x = 8
:
t = 0
- Is
8
in set? Yes →x = 8 * 8 = 64
,t = 1
- Is
64
in set? No → stop t = 1
is not > 1, so no update
Starting with x = 2
:
t = 0
- Is
2
in set? Yes →x = 2 * 2 = 4
,t = 1
- Is
4
in set? Yes →x = 4 * 4 = 16
,t = 2
- Is
16
in set? Yes →x = 16 * 16 = 256
,t = 3
- Is
256
in set? No → stop t = 3 > 1
, soans = max(2, 3) = 3
Result: The longest square streak is [2, 4, 16]
with length 3
.
Notice how starting from different points in the same streak (like starting from 4
vs starting from 2
) gives different lengths. By trying all starting points, we ensure we find the maximum possible length.
Solution Implementation
1class Solution:
2 def longestSquareStreak(self, nums: List[int]) -> int:
3 # Create a set for O(1) lookup of numbers in the array
4 num_set = set(nums)
5
6 # Initialize the maximum streak length (-1 means no valid streak found)
7 max_streak_length = -1
8
9 # Check each number as a potential starting point of a square streak
10 for num in nums:
11 # Count the length of square streak starting from current number
12 current_streak_length = 0
13 current_value = num
14
15 # Keep squaring the number while it exists in the set
16 while current_value in num_set:
17 current_value = current_value * current_value
18 current_streak_length += 1
19
20 # A valid streak must have at least 2 elements
21 if current_streak_length > 1:
22 max_streak_length = max(max_streak_length, current_streak_length)
23
24 return max_streak_length
25
1class Solution {
2 public int longestSquareStreak(int[] nums) {
3 // Create a HashSet to store all numbers as Long type for efficient lookup
4 // Using Long to prevent overflow when squaring large integers
5 Set<Long> numberSet = new HashSet<>();
6 for (long num : nums) {
7 numberSet.add(num);
8 }
9
10 // Initialize the result to -1 (return value when no valid streak exists)
11 int maxStreakLength = -1;
12
13 // Iterate through each unique number in the set
14 for (long currentNumber : numberSet) {
15 // Count the length of square streak starting from currentNumber
16 int streakLength = 0;
17
18 // Keep squaring the number and checking if it exists in the set
19 // Continue until we find a square that doesn't exist in the set
20 while (numberSet.contains(currentNumber)) {
21 streakLength++;
22 currentNumber = currentNumber * currentNumber;
23 }
24
25 // A valid square streak must have length > 1
26 // Update the maximum streak length if current streak is valid
27 if (streakLength > 1) {
28 maxStreakLength = Math.max(maxStreakLength, streakLength);
29 }
30 }
31
32 // Return the length of the longest square streak, or -1 if none exists
33 return maxStreakLength;
34 }
35}
36
1class Solution {
2public:
3 int longestSquareStreak(vector<int>& nums) {
4 // Create a hash set for O(1) lookup of numbers in the array
5 // Using long long to prevent overflow when squaring
6 unordered_set<long long> numSet(nums.begin(), nums.end());
7
8 // Initialize the maximum streak length (-1 means no valid streak found)
9 int maxStreakLength = -1;
10
11 // Try to build a square streak starting from each number
12 for (long long currentNum : nums) {
13 int streakLength = 0;
14
15 // Keep squaring the current number and check if it exists in the set
16 // Continue until we find a number that's not in the set
17 while (numSet.count(currentNum)) {
18 streakLength++;
19 currentNum = currentNum * currentNum;
20
21 // Early termination if the number becomes too large
22 // (optional optimization to prevent overflow)
23 if (currentNum > 1e9) {
24 break;
25 }
26 }
27
28 // A valid square streak must have at least 2 numbers
29 if (streakLength > 1) {
30 maxStreakLength = max(maxStreakLength, streakLength);
31 }
32 }
33
34 return maxStreakLength;
35 }
36};
37
1/**
2 * Finds the length of the longest square streak in an array.
3 * A square streak is a sequence where each element is the square of the previous element.
4 * @param nums - The input array of positive integers
5 * @returns The length of the longest square streak, or -1 if no streak exists
6 */
7function longestSquareStreak(nums: number[]): number {
8 // Create a set for O(1) lookup of elements in the array
9 const numSet: Set<number> = new Set(nums);
10
11 // Initialize the maximum streak length to -1 (default when no valid streak exists)
12 let maxStreakLength: number = -1;
13
14 // Iterate through each number as a potential starting point of a square streak
15 for (const startNum of nums) {
16 // Current number in the streak sequence
17 let currentNum: number = startNum;
18
19 // Counter for the current streak length
20 let currentStreakLength: number = 0;
21
22 // Continue the streak as long as the current number exists in the set
23 while (numSet.has(currentNum)) {
24 // Move to the next number in the streak (square of current)
25 currentNum = currentNum * currentNum;
26
27 // Increment the streak length
28 currentStreakLength += 1;
29
30 // Break if number becomes too large to prevent overflow
31 if (currentNum > 1e9) {
32 break;
33 }
34 }
35
36 // A valid square streak must have at least 2 elements
37 if (currentStreakLength > 1) {
38 // Update the maximum streak length found so far
39 maxStreakLength = Math.max(maxStreakLength, currentStreakLength);
40 }
41 }
42
43 return maxStreakLength;
44}
45
Time and Space Complexity
Time Complexity: O(n × log log M)
The algorithm iterates through each element in nums
(contributing O(n)
). For each element x
, it repeatedly squares the value and checks if it exists in the set. The key insight is determining how many times we can square a number before it exceeds the maximum value M
.
Starting from a value x
, after k
squaring operations, we get x^(2^k)
. The loop continues while x^(2^k) ≤ M
. Taking logarithms: 2^k × log(x) ≤ log(M)
, which gives us k ≤ log(log(M)/log(x))
. In the worst case when x = 2
(the smallest base that can form a streak), we get approximately k ≤ log log M
iterations.
Since each element can trigger at most O(log log M)
iterations, and we process n
elements, the total time complexity is O(n × log log M)
.
Space Complexity: O(n)
The algorithm uses a set s
to store all elements from the input array nums
, which requires O(n)
space. The only other variables used are ans
, t
, and x
, which require constant space. Therefore, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Overflow When Squaring Large Numbers
The most critical pitfall in this solution is that squaring numbers can quickly lead to integer overflow. When we compute current_value = current_value * current_value
, the result can exceed the maximum integer limit, especially after just a few iterations.
Example of the Problem:
- Starting with 46340:
46340² = 2,147,395,600
(still within 32-bit int range) - Next iteration:
2,147,395,600² ≈ 4.6 × 10^18
(overflow!)
Solution: Add an overflow check before squaring:
class Solution:
def longestSquareStreak(self, nums: List[int]) -> int:
num_set = set(nums)
max_streak_length = -1
# Define a reasonable upper bound to prevent overflow
MAX_LIMIT = 10**5 # Adjust based on problem constraints
for num in nums:
current_streak_length = 0
current_value = num
while current_value in num_set:
# Check if squaring would exceed our limit
if current_value > MAX_LIMIT:
break
current_value = current_value * current_value
current_streak_length += 1
if current_streak_length > 1:
max_streak_length = max(max_streak_length, current_streak_length)
return max_streak_length
2. Redundant Computation for Elements in the Same Streak
The current solution may redundantly compute streaks for numbers that are part of another number's streak. For instance, if we have [2, 4, 16]
, we compute the streak starting from 2 (finding 2→4→16), then again from 4 (finding 4→16), and again from 16.
Solution: Track visited numbers or only start streaks from "root" elements (numbers that aren't squares of any other number in the set):
class Solution:
def longestSquareStreak(self, nums: List[int]) -> int:
num_set = set(nums)
max_streak_length = -1
visited = set()
for num in nums:
# Skip if this number was already part of another streak
if num in visited:
continue
current_streak_length = 0
current_value = num
streak_elements = []
while current_value in num_set and current_value <= 10**5:
streak_elements.append(current_value)
current_value = current_value * current_value
current_streak_length += 1
# Mark all elements in this streak as visited
visited.update(streak_elements)
if current_streak_length > 1:
max_streak_length = max(max_streak_length, current_streak_length)
return max_streak_length
3. Not Handling Edge Cases with Duplicates
If the input contains duplicate numbers, the set conversion removes them, which is correct for this problem. However, developers might mistakenly think they need to handle duplicates specially.
Key Understanding: Duplicates don't affect the solution because:
- A square streak is about unique values forming a sequence
[2, 2, 4, 16]
has the same longest streak as[2, 4, 16]
The set conversion naturally handles this correctly, so no additional logic is needed.
Problem: Given a list of tasks and a list of requirements, compute a sequence of tasks that can be performed, such that we complete every task once while satisfying all the requirements.
Which of the following method should we use to solve this problem?
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Want a Structured Path to Master System Design Too? Don’t Miss This!