3152. Special Array II
Problem Description
You are given an array of integers nums
and need to determine if certain subarrays are "special". An array is considered special if every pair of adjacent elements has different parity (one is odd and one is even).
For example:
[1, 2, 3, 4]
is special because each adjacent pair(1,2)
,(2,3)
,(3,4)
contains one odd and one even number[1, 3, 5]
is not special because the pair(1,3)
contains two odd numbers
You're also given a 2D array queries
where each query queries[i] = [from_i, to_i]
asks you to check if the subarray nums[from_i..to_i]
(inclusive) is special.
Your task is to return an array of booleans answer
where answer[i]
is true
if the subarray specified by the i-th query is special, and false
otherwise.
The solution uses a preprocessing technique where it tracks for each position i
the leftmost index from which a special subarray can start and extend to position i
. The array d
stores these leftmost positions. When processing the array:
- Initially,
d[i] = i
for all positions - If
nums[i]
andnums[i-1]
have different parities, thend[i] = d[i-1]
(the special subarray can extend further left) - For each query
[from, to]
, the subarray is special ifd[to] <= from
, meaning the special subarray starting from positionfrom
can reach positionto
Intuition
The key insight is that we need to efficiently answer multiple queries about whether subarrays are special. A naive approach would check each subarray for every query, but this would be inefficient.
Think about what makes a subarray special: it needs alternating parities throughout. If we have a special subarray and it breaks at some point (two consecutive elements with the same parity), then any subarray containing that breaking point cannot be special.
This leads us to think about "maximal special subarrays" - the longest possible special subarrays that cannot be extended further. For any position in the array, we can ask: "What's the furthest left I can go while maintaining the special property?"
For example, if we have nums = [1, 2, 3, 8, 5, 6]
:
- At index 0: we can only start from index 0
- At index 1: we can start from index 0 (since 1 and 2 have different parities)
- At index 2: we can start from index 0 (entire subarray
[1,2,3]
is special) - At index 3: we can only start from index 3 (since 3 and 8 both odd, breaks the pattern)
- At index 4: we can start from index 3 (since 8 and 5 have different parities)
- At index 5: we can start from index 3 (entire subarray
[8,5,6]
is special)
By preprocessing this information into array d
, where d[i]
stores the leftmost position from which we can form a special subarray ending at position i
, we can answer any query in O(1) time. For a query [from, to]
, if d[to] <= from
, it means the special subarray that includes position to
can extend all the way back to (or beyond) position from
, making the queried subarray special.
Learn more about Binary Search and Prefix Sum patterns.
Solution Approach
The solution uses a preprocessing approach with dynamic programming to efficiently answer multiple queries.
Step 1: Initialize the tracking array
We create an array d
where d[i]
represents the leftmost index from which we can form a special subarray that ends at position i
. Initially, we set d[i] = i
for all positions, meaning each position can at least form a special subarray of length 1 (a single element).
d = list(range(n)) # d[i] = i initially
Step 2: Build the leftmost positions
We iterate through the array from index 1 to n-1
. For each position i
, we check if nums[i]
and nums[i-1]
have different parities:
for i in range(1, n):
if nums[i] % 2 != nums[i - 1] % 2:
d[i] = d[i - 1]
- If they have different parities (one odd, one even), the special property is maintained, so we can extend the special subarray. We set
d[i] = d[i-1]
, meaning positioni
can be part of the same special subarray that positioni-1
belongs to. - If they have the same parity, the special property breaks. We keep
d[i] = i
(unchanged from initialization), meaning a new special subarray must start at positioni
.
Step 3: Answer queries
For each query [from, to]
, we check if the subarray nums[from..to]
is special:
return [d[t] <= f for f, t in queries]
The condition d[to] <= from
checks whether the special subarray that includes position to
can extend back to (or beyond) position from
. If true, it means all elements from from
to to
form a special subarray.
Time Complexity: O(n + q) where n is the length of the array and q is the number of queries
Space Complexity: O(n) for the preprocessing array d
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 a concrete example to understand how the solution works.
Given:
nums = [2, 3, 4, 5, 6]
queries = [[0, 2], [1, 3], [2, 4]]
Step 1: Initialize array d
We start with d = [0, 1, 2, 3, 4]
where each position initially points to itself.
Step 2: Build the leftmost positions
Let's process each position starting from index 1:
-
i = 1: Compare
nums[1] = 3
(odd) withnums[0] = 2
(even)- Different parities ✓
- Set
d[1] = d[0] = 0
- Now
d = [0, 0, 2, 3, 4]
-
i = 2: Compare
nums[2] = 4
(even) withnums[1] = 3
(odd)- Different parities ✓
- Set
d[2] = d[1] = 0
- Now
d = [0, 0, 0, 3, 4]
-
i = 3: Compare
nums[3] = 5
(odd) withnums[2] = 4
(even)- Different parities ✓
- Set
d[3] = d[2] = 0
- Now
d = [0, 0, 0, 0, 4]
-
i = 4: Compare
nums[4] = 6
(even) withnums[3] = 5
(odd)- Different parities ✓
- Set
d[4] = d[3] = 0
- Final
d = [0, 0, 0, 0, 0]
This means the entire array forms one special subarray, as each position can trace back to index 0.
Step 3: Process queries
-
Query [0, 2]: Check if
d[2] <= 0
d[2] = 0
, and0 <= 0
is true ✓- Subarray
[2, 3, 4]
is special
-
Query [1, 3]: Check if
d[3] <= 1
d[3] = 0
, and0 <= 1
is true ✓- Subarray
[3, 4, 5]
is special
-
Query [2, 4]: Check if
d[4] <= 2
d[4] = 0
, and0 <= 2
is true ✓- Subarray
[4, 5, 6]
is special
Result: [true, true, true]
Another example to show a non-special case:
Let's modify nums = [2, 3, 5, 6, 7]
(notice 3 and 5 are both odd)
Building d
:
- Start:
d = [0, 1, 2, 3, 4]
- i = 1:
3
(odd) vs2
(even) → different →d[1] = 0
→d = [0, 0, 2, 3, 4]
- i = 2:
5
(odd) vs3
(odd) → same parity → keepd[2] = 2
→d = [0, 0, 2, 3, 4]
- i = 3:
6
(even) vs5
(odd) → different →d[3] = d[2] = 2
→d = [0, 0, 2, 2, 4]
- i = 4:
7
(odd) vs6
(even) → different →d[4] = d[3] = 2
→d = [0, 0, 2, 2, 2]
For query [0, 3]
: Check if d[3] <= 0
d[3] = 2
, and2 <= 0
is false ✗- Subarray
[2, 3, 5, 6]
is not special (contains consecutive odd numbers 3 and 5)
Solution Implementation
1class Solution:
2 def isArraySpecial(self, nums: List[int], queries: List[List[int]]) -> List[bool]:
3 # Get the length of the input array
4 n = len(nums)
5
6 # Initialize an array where each index stores the leftmost position
7 # from which we can form a special subarray ending at that index
8 leftmost_special_start = list(range(n))
9
10 # Build the leftmost_special_start array
11 # For each position, track how far left we can extend while maintaining alternating parity
12 for i in range(1, n):
13 # Check if current element has different parity than previous element
14 if nums[i] % 2 != nums[i - 1] % 2:
15 # If parities are different, we can extend the special subarray
16 # from the previous position's leftmost start
17 leftmost_special_start[i] = leftmost_special_start[i - 1]
18 # If parities are the same, leftmost_special_start[i] remains as i (initialized value)
19
20 # Process each query [start, end] and check if the subarray is special
21 # A subarray is special if the leftmost position we can reach from 'end'
22 # is at or before 'start'
23 result = []
24 for start, end in queries:
25 is_special = leftmost_special_start[end] <= start
26 result.append(is_special)
27
28 return result
29
1class Solution {
2 public boolean[] isArraySpecial(int[] nums, int[][] queries) {
3 int arrayLength = nums.length;
4
5 // Build an array to track the leftmost index where parity alternation breaks
6 // leftmostBreakIndex[i] stores the leftmost index from which we can reach index i
7 // while maintaining alternating parity (odd-even or even-odd pattern)
8 int[] leftmostBreakIndex = new int[arrayLength];
9
10 // Populate the leftmostBreakIndex array
11 for (int i = 1; i < arrayLength; ++i) {
12 // Check if current element and previous element have different parity
13 if (nums[i] % 2 != nums[i - 1] % 2) {
14 // If parity alternates, we can extend the special subarray
15 leftmostBreakIndex[i] = leftmostBreakIndex[i - 1];
16 } else {
17 // If parity doesn't alternate, mark current index as break point
18 leftmostBreakIndex[i] = i;
19 }
20 }
21
22 int queryCount = queries.length;
23 boolean[] result = new boolean[queryCount];
24
25 // Process each query to check if the subarray is special
26 for (int i = 0; i < queryCount; ++i) {
27 int startIndex = queries[i][0];
28 int endIndex = queries[i][1];
29
30 // A subarray is special if the leftmost break point at endIndex
31 // is less than or equal to startIndex (meaning no breaks in the range)
32 result[i] = leftmostBreakIndex[endIndex] <= startIndex;
33 }
34
35 return result;
36 }
37}
38
1class Solution {
2public:
3 vector<bool> isArraySpecial(vector<int>& nums, vector<vector<int>>& queries) {
4 int n = nums.size();
5
6 // leftmost[i] stores the leftmost index from which we can form a special subarray ending at index i
7 // A special subarray has alternating parity (odd/even) between adjacent elements
8 vector<int> leftmost(n);
9
10 // Initialize each position with its own index
11 // iota fills the vector with consecutive values starting from 0
12 iota(leftmost.begin(), leftmost.end(), 0);
13
14 // Build the leftmost array by checking parity alternation
15 for (int i = 1; i < n; ++i) {
16 // Check if current element and previous element have different parity
17 if (nums[i] % 2 != nums[i - 1] % 2) {
18 // If parities are different, extend the special subarray from previous position
19 leftmost[i] = leftmost[i - 1];
20 }
21 // If parities are same, leftmost[i] remains as i (starts new special subarray)
22 }
23
24 // Process each query and store results
25 vector<bool> result;
26 for (auto& query : queries) {
27 int left = query[0];
28 int right = query[1];
29
30 // Check if the special subarray ending at 'right' can extend all the way to 'left'
31 // If leftmost[right] <= left, then the entire range [left, right] is special
32 result.push_back(leftmost[right] <= left);
33 }
34
35 return result;
36 }
37};
38
1/**
2 * Determines if subarrays defined by queries are "special" (alternating parity).
3 * A subarray is special if adjacent elements have different parities (odd/even).
4 *
5 * @param nums - The input array of numbers
6 * @param queries - Array of query ranges [from, to] to check
7 * @returns Array of booleans indicating if each queried subarray is special
8 */
9function isArraySpecial(nums: number[], queries: number[][]): boolean[] {
10 const arrayLength: number = nums.length;
11
12 // Track the leftmost index where a special subarray can start and reach index i
13 // leftmostSpecialStart[i] = earliest index j such that nums[j..i] is special
14 const leftmostSpecialStart: number[] = Array.from({ length: arrayLength }, (_, index) => index);
15
16 // Build the leftmostSpecialStart array by checking adjacent elements
17 for (let i = 1; i < arrayLength; ++i) {
18 // Check if current element and previous element have different parities
19 if (nums[i] % 2 !== nums[i - 1] % 2) {
20 // If different parities, extend the special subarray from previous position
21 leftmostSpecialStart[i] = leftmostSpecialStart[i - 1];
22 }
23 // If same parity, leftmostSpecialStart[i] remains as i (starts fresh)
24 }
25
26 // Process each query and check if the subarray is special
27 return queries.map(([fromIndex, toIndex]) => {
28 // A subarray nums[fromIndex..toIndex] is special if
29 // the leftmost special start that reaches toIndex is at or before fromIndex
30 return leftmostSpecialStart[toIndex] <= fromIndex;
31 });
32}
33
Time and Space Complexity
The time complexity is O(n + q)
where n
is the length of the array nums
and q
is the number of queries.
- Building the array
d
requires iterating through the array once with a loop from 1 to n-1, which takesO(n)
time. - Processing the queries with the list comprehension
[d[t] <= f for f, t in queries]
takesO(q)
time as each query is processed inO(1)
time. - Therefore, the total time complexity is
O(n + q)
.
The space complexity is O(n)
where n
is the length of the array.
- The array
d
stores exactlyn
elements, requiringO(n)
space. - The output list stores
q
boolean values, but this is typically not counted as auxiliary space since it's the required output. - Therefore, the auxiliary space complexity is
O(n)
.
Note: The reference answer simplifies to O(n)
for time complexity, which is valid when considering that in many practical scenarios q ≤ n
, or when the query processing is considered part of the output generation rather than the core algorithm.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding the Parity Check Logic
A common mistake is incorrectly implementing the parity comparison, especially when dealing with negative numbers.
Incorrect Implementation:
# This might fail with negative numbers in some languages if (nums[i] + nums[i-1]) % 2 == 1: # Trying to check if sum is odd leftmost_special_start[i] = leftmost_special_start[i - 1]
Why it's wrong: While mathematically the sum of two numbers with different parities is odd, this approach can be problematic with negative numbers or overflow in some implementations.
Correct Implementation:
if nums[i] % 2 != nums[i - 1] % 2: # Directly compare parities leftmost_special_start[i] = leftmost_special_start[i - 1]
2. Off-by-One Errors in Query Processing
Developers might incorrectly interpret the query bounds or the comparison condition.
Incorrect Implementation:
# Wrong comparison operator result.append(leftmost_special_start[end] < start) # Should be <= # Or wrong index usage result.append(leftmost_special_start[start] <= end) # Checking wrong direction
Correct Implementation:
result.append(leftmost_special_start[end] <= start)
The key insight is that leftmost_special_start[end]
tells us how far left we can go from position end
while maintaining the special property. If this value is less than or equal to start
, it means we can reach start
from end
with alternating parities.
3. Handling Edge Cases with Single Element Subarrays
Some might forget that a single element is always considered a special subarray.
Potential Issue:
# If someone adds unnecessary validation if start == end: # Might incorrectly return False or add complex logic result.append(False) # Wrong!
Why the original solution handles it correctly:
The initialization leftmost_special_start = list(range(n))
ensures that for any single element query where start == end
, we have leftmost_special_start[end] = end = start
, so the condition leftmost_special_start[end] <= start
correctly evaluates to True
.
4. Inefficient Query Processing
A tempting but inefficient approach would be to check each query by iterating through the subarray.
Inefficient Implementation:
def isArraySpecial(self, nums, queries):
result = []
for start, end in queries:
is_special = True
for i in range(start + 1, end + 1):
if nums[i] % 2 == nums[i - 1] % 2:
is_special = False
break
result.append(is_special)
return result
Why it's problematic: This has O(q × m) time complexity where m is the average query length, which can be O(q × n) in the worst case.
The preprocessing solution is better: It achieves O(n + q) time complexity by doing the work upfront.
Which data structure is used in a depth first search?
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
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
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
Want a Structured Path to Master System Design Too? Don’t Miss This!