Facebook Pixel

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] and nums[i-1] have different parities, then d[i] = d[i-1] (the special subarray can extend further left)
  • For each query [from, to], the subarray is special if d[to] <= from, meaning the special subarray starting from position from can reach position to
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 position i can be part of the same special subarray that position i-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 position i.

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 Evaluator

Example 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) with nums[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) with nums[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) with nums[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) with nums[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, and 0 <= 0 is true ✓
    • Subarray [2, 3, 4] is special
  • Query [1, 3]: Check if d[3] <= 1

    • d[3] = 0, and 0 <= 1 is true ✓
    • Subarray [3, 4, 5] is special
  • Query [2, 4]: Check if d[4] <= 2

    • d[4] = 0, and 0 <= 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) vs 2 (even) → different → d[1] = 0d = [0, 0, 2, 3, 4]
  • i = 2: 5 (odd) vs 3 (odd) → same parity → keep d[2] = 2d = [0, 0, 2, 3, 4]
  • i = 3: 6 (even) vs 5 (odd) → different → d[3] = d[2] = 2d = [0, 0, 2, 2, 4]
  • i = 4: 7 (odd) vs 6 (even) → different → d[4] = d[3] = 2d = [0, 0, 2, 2, 2]

For query [0, 3]: Check if d[3] <= 0

  • d[3] = 2, and 2 <= 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 takes O(n) time.
  • Processing the queries with the list comprehension [d[t] <= f for f, t in queries] takes O(q) time as each query is processed in O(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 exactly n elements, requiring O(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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which data structure is used in a depth first search?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More