Facebook Pixel

2972. Count the Number of Incremovable Subarrays II

Problem Description

You are given a 0-indexed array of positive integers nums.

A subarray is called incremovable if removing it from the original array results in the remaining elements forming a strictly increasing sequence. For example, in the array [5, 3, 4, 6, 7], the subarray [3, 4] is incremovable because removing it leaves [5, 6, 7], which is strictly increasing.

Your task is to count the total number of incremovable subarrays in the given array nums.

Key points to understand:

  • A subarray must be contiguous and non-empty
  • After removing the subarray, the remaining array (which could be empty) must be strictly increasing
  • An empty array is considered strictly increasing
  • Strictly increasing means each element must be strictly greater than the previous one (no equal values allowed)

For instance, if we have nums = [1, 2, 3, 4], which is already strictly increasing, any subarray we remove will leave either an empty array or a strictly increasing array, so all possible subarrays are incremovable. The total count would be n * (n + 1) / 2 = 10 subarrays.

The challenge involves identifying which subarrays, when removed, maintain the strictly increasing property in the remaining elements. The remaining elements after removal could be:

  • Only a prefix of the original array
  • Only a suffix of the original array
  • A combination of prefix and suffix elements
  • An empty array (when the entire array is removed)
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is to think about what remains after removing a subarray, rather than what we remove. After removing any subarray, we're left with either:

  • Nothing (empty array)
  • A prefix of the original array
  • A suffix of the original array
  • Both a prefix and a suffix concatenated together

Since an empty array and any strictly increasing sequence are valid, we need to count all possible ways to achieve these configurations.

Let's first consider the simplest case: if the entire array is already strictly increasing, then removing any subarray will leave a strictly increasing sequence (or empty). This gives us n * (n + 1) / 2 total subarrays.

For the general case, we need to identify:

  1. The longest strictly increasing prefix ending at index i
  2. The longest strictly increasing suffix starting from the end

If we can only keep a prefix (removing everything from some point onwards), we can remove:

  • nums[i+1...n-1] (keep the full increasing prefix)
  • nums[i...n-1] (keep prefix minus last element)
  • nums[i-1...n-1] (keep prefix minus last two elements)
  • And so on, down to removing the entire array

This gives us i + 2 possibilities when keeping only a prefix.

For keeping a suffix (with or without a prefix), we iterate through possible starting points j of the suffix from right to left. For each valid suffix starting at j, we need to find how many elements from the prefix can be kept such that the last prefix element is less than nums[j]. We use pointer i to track this.

The clever part is moving i backwards when needed to maintain nums[i] < nums[j]. For each valid suffix position j, we add i + 2 to our count (representing all possible prefixes that can combine with this suffix, including the empty prefix).

We stop when we find nums[j-1] >= nums[j] because at that point, the suffix is no longer strictly increasing.

Learn more about Two Pointers and Binary Search patterns.

Solution Approach

The implementation uses a two-pointer approach to efficiently count all incremovable subarrays.

Step 1: Find the longest strictly increasing prefix

i, n = 0, len(nums)
while i + 1 < n and nums[i] < nums[i + 1]:
    i += 1

We iterate from the start to find the last index i where nums[0] < nums[1] < ... < nums[i]. This gives us the longest strictly increasing prefix.

Step 2: Handle the special case

if i == n - 1:
    return n * (n + 1) // 2

If i == n - 1, the entire array is strictly increasing. Any subarray removal will result in a strictly increasing sequence, so we return the total number of subarrays: n * (n + 1) // 2.

Step 3: Count subarrays when keeping only a prefix

ans = i + 2

We initialize our answer with i + 2, which represents all the ways to remove a suffix and keep only a prefix (or nothing):

  • Remove nums[i+1...n-1] (keep full prefix)
  • Remove nums[i...n-1] (keep prefix except last element)
  • Remove nums[i-1...n-1] (keep prefix except last 2 elements)
  • ... down to removing the entire array

Step 4: Count subarrays when keeping a suffix (with or without prefix)

j = n - 1
while j:
    while i >= 0 and nums[i] >= nums[j]:
        i -= 1
    ans += i + 2
    if nums[j - 1] >= nums[j]:
        break
    j -= 1

We iterate j from right to left, treating nums[j] as the first element of the suffix we want to keep:

  • For each position j, we adjust pointer i backwards until we find nums[i] < nums[j] (or i becomes -1)
  • This ensures that if we keep prefix nums[0...i] and suffix nums[j...n-1], the concatenation is strictly increasing
  • We add i + 2 to the answer:
    • i + 1 ways to choose different prefix lengths (from empty to full prefix up to index i)
    • Plus 1 for removing everything before j (keeping only the suffix)
  • We stop when nums[j-1] >= nums[j] because the suffix starting at j-1 wouldn't be strictly increasing

The algorithm efficiently counts all valid configurations in O(n) time by cleverly reusing the pointer i as we move j leftward, avoiding redundant comparisons.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution with nums = [2, 1, 3, 5, 4, 6].

Step 1: Find longest strictly increasing prefix

  • Start with i = 0
  • Check: nums[0] = 2 < nums[1] = 1? No, so stop
  • Longest prefix ends at i = 0 (just element [2])

Step 2: Check if entire array is increasing

  • Since i = 0 and n - 1 = 5, the array is not fully increasing
  • Continue to count incremovable subarrays

Step 3: Count subarrays keeping only prefix

  • ans = i + 2 = 0 + 2 = 2
  • This counts:
    • Removing [1, 3, 5, 4, 6] → leaves [2] (strictly increasing)
    • Removing [2, 1, 3, 5, 4, 6] → leaves [] (empty, valid)

Step 4: Count subarrays keeping suffix (with/without prefix)

Start with j = 5 (last element):

  • nums[j] = 6
  • Check prefix: nums[0] = 2 < 6? Yes, so i stays at 0
  • Add i + 2 = 0 + 2 = 2 to answer → ans = 4
    • This counts removing [1, 3, 5, 4] leaving [2] + [6]
    • And removing [2, 1, 3, 5, 4] leaving just [6]
  • Check: nums[4] = 4 < nums[5] = 6? Yes, continue

Move to j = 4:

  • nums[j] = 4
  • Check prefix: nums[0] = 2 < 4? Yes, so i stays at 0
  • Add i + 2 = 2ans = 6
    • This counts removing [1, 3, 5] leaving [2] + [4, 6]
    • And removing [2, 1, 3, 5] leaving just [4, 6]
  • Check: nums[3] = 5 > nums[4] = 4? Yes, STOP!
    • The suffix [5, 4, 6] is not strictly increasing

Final answer: 6 incremovable subarrays

These are:

  1. [2, 1, 3, 5, 4, 6] → removes entire array → []
  2. [1, 3, 5, 4, 6] → removes suffix → [2]
  3. [1, 3, 5, 4] → leaves [2, 6]
  4. [2, 1, 3, 5, 4] → leaves [6]
  5. [1, 3, 5] → leaves [2, 4, 6]
  6. [2, 1, 3, 5] → leaves [4, 6]

Solution Implementation

1class Solution:
2    def incremovableSubarrayCount(self, nums: List[int]) -> int:
3        # Find the longest strictly increasing prefix
4        left_boundary = 0
5        n = len(nums)
6      
7        # Extend the left boundary while array is strictly increasing
8        while left_boundary + 1 < n and nums[left_boundary] < nums[left_boundary + 1]:
9            left_boundary += 1
10      
11        # If entire array is strictly increasing, any subarray can be removed
12        # Total subarrays = n * (n + 1) / 2
13        if left_boundary == n - 1:
14            return n * (n + 1) // 2
15      
16        # Count subarrays that can be removed
17        # Start with subarrays that include everything from index (left_boundary + 1) onwards
18        # Plus the empty removal and full array removal
19        result = left_boundary + 2
20      
21        # Process from the right side
22        right_pointer = n - 1
23      
24        while right_pointer > 0:
25            # Find valid left boundaries that can connect with current right element
26            # Move left boundary back while it's greater than or equal to right element
27            while left_boundary >= 0 and nums[left_boundary] >= nums[right_pointer]:
28                left_boundary -= 1
29          
30            # Add count of valid removals ending just before right_pointer
31            # (left_boundary + 1) positions from start, plus empty prefix
32            result += left_boundary + 2
33          
34            # Stop if right side is no longer strictly increasing
35            if nums[right_pointer - 1] >= nums[right_pointer]:
36                break
37          
38            right_pointer -= 1
39      
40        return result
41
1class Solution {
2    public long incremovableSubarrayCount(int[] nums) {
3        int leftBoundary = 0;
4        int arrayLength = nums.length;
5      
6        // Find the longest strictly increasing prefix
7        // This represents the maximum valid left portion that can remain
8        while (leftBoundary + 1 < arrayLength && nums[leftBoundary] < nums[leftBoundary + 1]) {
9            leftBoundary++;
10        }
11      
12        // Special case: entire array is strictly increasing
13        // Any subarray can be removed (including empty subarray)
14        // Total count = n + (n-1) + ... + 1 = n*(n+1)/2
15        if (leftBoundary == arrayLength - 1) {
16            return arrayLength * (arrayLength + 1L) / 2;
17        }
18      
19        // Initialize answer with subarrays that include everything from leftBoundary+1 to end
20        // These are: removing [leftBoundary+1...end], [leftBoundary...end], ..., [0...end]
21        // Count = leftBoundary + 2 (including the case of removing the entire array)
22        long totalCount = leftBoundary + 2;
23      
24        // Process each possible right boundary of the remaining array
25        for (int rightBoundary = arrayLength - 1; rightBoundary > 0; rightBoundary--) {
26            // Adjust left boundary to maintain strictly increasing property
27            // Find the largest index in left portion where nums[index] < nums[rightBoundary]
28            while (leftBoundary >= 0 && nums[leftBoundary] >= nums[rightBoundary]) {
29                leftBoundary--;
30            }
31          
32            // Add count of valid removals ending just before rightBoundary
33            // These are subarrays from positions [leftBoundary+1...rightBoundary-1] to [0...rightBoundary-1]
34            totalCount += leftBoundary + 2;
35          
36            // If right portion is not strictly increasing, stop
37            // No need to check further as remaining right portions won't be valid
38            if (nums[rightBoundary - 1] >= nums[rightBoundary]) {
39                break;
40            }
41        }
42      
43        return totalCount;
44    }
45}
46
1class Solution {
2public:
3    long long incremovableSubarrayCount(vector<int>& nums) {
4        int leftBoundary = 0;
5        int arraySize = nums.size();
6      
7        // Find the longest strictly increasing prefix
8        // leftBoundary will be the last index of this prefix
9        while (leftBoundary + 1 < arraySize && nums[leftBoundary] < nums[leftBoundary + 1]) {
10            ++leftBoundary;
11        }
12      
13        // Special case: if the entire array is strictly increasing
14        // then any subarray can be removed (including empty subarray)
15        // Total count = n + (n-1) + (n-2) + ... + 1 = n*(n+1)/2
16        if (leftBoundary == arraySize - 1) {
17            return static_cast<long long>(arraySize) * (arraySize + 1) / 2;
18        }
19      
20        // Initialize answer with subarrays that include everything after the prefix
21        // These are: remove nothing, remove from index 0 to 0, 0 to 1, ..., 0 to leftBoundary
22        // Total: leftBoundary + 2 options
23        long long totalCount = leftBoundary + 2;
24      
25        // Process strictly increasing suffix from right to left
26        for (int rightIndex = arraySize - 1; rightIndex > 0; --rightIndex) {
27            // Adjust leftBoundary to maintain valid combination
28            // We need nums[leftBoundary] < nums[rightIndex] for valid removal
29            while (leftBoundary >= 0 && nums[leftBoundary] >= nums[rightIndex]) {
30                --leftBoundary;
31            }
32          
33            // Add count of valid removals ending just before rightIndex
34            // Can remove subarrays from indices (leftBoundary+1) to (rightIndex-1)
35            // Plus the option to remove nothing between them
36            totalCount += leftBoundary + 2;
37          
38            // If suffix is no longer strictly increasing, stop
39            if (nums[rightIndex - 1] >= nums[rightIndex]) {
40                break;
41            }
42        }
43      
44        return totalCount;
45    }
46};
47
1/**
2 * Counts the number of incremovable subarrays in the given array.
3 * An incremovable subarray is one that, when removed, leaves the remaining array strictly increasing.
4 * 
5 * @param nums - The input array of numbers
6 * @returns The count of incremovable subarrays
7 */
8function incremovableSubarrayCount(nums: number[]): number {
9    const arrayLength: number = nums.length;
10  
11    // Find the longest strictly increasing prefix
12    let prefixEndIndex: number = 0;
13    while (prefixEndIndex + 1 < arrayLength && nums[prefixEndIndex] < nums[prefixEndIndex + 1]) {
14        prefixEndIndex++;
15    }
16  
17    // If the entire array is strictly increasing, all subarrays can be removed
18    // Total number of subarrays = n * (n + 1) / 2
19    if (prefixEndIndex === arrayLength - 1) {
20        return (arrayLength * (arrayLength + 1)) / 2;
21    }
22  
23    // Initialize result with subarrays that can be removed from the start
24    // prefixEndIndex + 2 accounts for:
25    // - Empty subarray (remove nothing)
26    // - All subarrays starting from index 0 up to prefixEndIndex
27    let result: number = prefixEndIndex + 2;
28  
29    // Process subarrays that can be removed from the end
30    for (let suffixStartIndex: number = arrayLength - 1; suffixStartIndex > 0; suffixStartIndex--) {
31        // Find the largest prefix index where nums[prefixEndIndex] < nums[suffixStartIndex]
32        // This ensures the remaining array is strictly increasing after removal
33        while (prefixEndIndex >= 0 && nums[prefixEndIndex] >= nums[suffixStartIndex]) {
34            prefixEndIndex--;
35        }
36      
37        // Add count of valid removals ending at suffixStartIndex
38        // prefixEndIndex + 2 represents all valid starting positions for removal
39        result += prefixEndIndex + 2;
40      
41        // If the suffix is not strictly increasing, stop processing
42        if (nums[suffixStartIndex - 1] >= nums[suffixStartIndex]) {
43            break;
44        }
45    }
46  
47    return result;
48}
49

Time and Space Complexity

Time Complexity: O(n)

The algorithm consists of three main parts:

  1. The first while loop iterates from the beginning of the array to find the longest strictly increasing prefix. This takes at most O(n) iterations.

  2. If the entire array is strictly increasing (checked by if i == n - 1), we return immediately with a constant time calculation.

  3. The second while loop starts from the end of the array and moves backwards. In the worst case, variable j iterates through all positions from n-1 to 1. For each position of j, the inner while loop adjusts pointer i backwards. The key observation is that pointer i only moves backwards throughout the entire execution - it starts at some position and can only decrease. Therefore, across all iterations of the outer loop, the inner while loop performs at most O(n) operations in total, not O(n) per iteration of the outer loop.

Since each element is visited at most a constant number of times across all operations, the overall time complexity is O(n).

Space Complexity: O(1)

The algorithm only uses a fixed number of variables (i, j, n, and ans) regardless of the input size. No additional data structures like arrays, lists, or hash maps are created. All operations are performed in-place using only these constant extra variables, resulting in O(1) space complexity.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Boundary Condition When Connecting Prefix and Suffix

The Pitfall: A common mistake is using the wrong comparison operator when checking if a prefix can connect with a suffix. Developers might incorrectly write:

while left_boundary >= 0 and nums[left_boundary] > nums[right_pointer]:
    left_boundary -= 1

This would check for nums[left_boundary] > nums[right_pointer] instead of nums[left_boundary] >= nums[right_pointer]. The issue is that we need strict inequality for a valid connection (the last element of the prefix must be strictly less than the first element of the suffix).

Why It Happens: The confusion arises because we're looking for strictly increasing sequences, so developers might think "strictly greater" is the right condition. However, we're actually moving the left boundary backwards to find where it becomes strictly less than the right element.

The Fix: Use the correct condition:

while left_boundary >= 0 and nums[left_boundary] >= nums[right_pointer]:
    left_boundary -= 1

After this loop, left_boundary points to the rightmost position where nums[left_boundary] < nums[right_pointer], ensuring a valid strictly increasing connection.

2. Not Handling the Edge Case of Already Strictly Increasing Array

The Pitfall: Forgetting to handle the special case where the entire array is already strictly increasing can lead to incorrect counting. Without this check, the algorithm might undercount valid subarrays.

# Missing this crucial check:
if left_boundary == n - 1:
    return n * (n + 1) // 2

Why It Happens: When the array is entirely strictly increasing, every single subarray is incremovable (removing any subarray leaves either an empty array or a strictly increasing array). The general algorithm logic doesn't naturally handle this case.

The Fix: Always check if left_boundary == n - 1 after finding the longest strictly increasing prefix. If true, return the total number of subarrays immediately.

3. Resetting the Left Boundary for Each Right Position

The Pitfall: Some might reset left_boundary to its original value for each new right_pointer position:

right_pointer = n - 1
while right_pointer > 0:
    left_boundary = original_left_boundary  # Wrong! Don't reset
    while left_boundary >= 0 and nums[left_boundary] >= nums[right_pointer]:
        left_boundary -= 1
    # ...

Why It Happens: It seems logical to start fresh for each suffix position, but this creates O(n²) time complexity and is unnecessary.

The Fix: Reuse the left_boundary position from the previous iteration. As right_pointer moves left, the values typically decrease (in a strictly increasing suffix), so the valid left_boundary can only move left or stay the same. This optimization maintains O(n) time complexity:

# Don't reset left_boundary; let it continue from where it was
while right_pointer > 0:
    while left_boundary >= 0 and nums[left_boundary] >= nums[right_pointer]:
        left_boundary -= 1
    # ...

4. Off-by-One Error in Counting

The Pitfall: Incorrectly calculating the number of valid removals for each configuration:

result = left_boundary + 1  # Wrong! Should be left_boundary + 2
# or later:
result += left_boundary + 1  # Wrong! Should be left_boundary + 2

Why It Happens: The confusion comes from counting the different ways to remove subarrays:

  • When left_boundary = i, we can keep prefixes of length 0, 1, 2, ..., i, which is i + 1 options
  • Plus we need to count the case where we keep only the suffix (no prefix), adding 1 more
  • Total: i + 2 options

The Fix: Always use left_boundary + 2 when adding to the result, accounting for all valid prefix lengths (including empty) plus the suffix-only option.

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

What's the output of running the following function using input [30, 20, 10, 100, 33, 12]?

1def fun(arr: List[int]) -> List[int]:
2    import heapq
3    heapq.heapify(arr)
4    res = []
5    for i in range(3):
6        res.append(heapq.heappop(arr))
7    return res
8
1public static int[] fun(int[] arr) {
2    int[] res = new int[3];
3    PriorityQueue<Integer> heap = new PriorityQueue<>();
4    for (int i = 0; i < arr.length; i++) {
5        heap.add(arr[i]);
6    }
7    for (int i = 0; i < 3; i++) {
8        res[i] = heap.poll();
9    }
10    return res;
11}
12
1class HeapItem {
2    constructor(item, priority = item) {
3        this.item = item;
4        this.priority = priority;
5    }
6}
7
8class MinHeap {
9    constructor() {
10        this.heap = [];
11    }
12
13    push(node) {
14        // insert the new node at the end of the heap array
15        this.heap.push(node);
16        // find the correct position for the new node
17        this.bubble_up();
18    }
19
20    bubble_up() {
21        let index = this.heap.length - 1;
22
23        while (index > 0) {
24            const element = this.heap[index];
25            const parentIndex = Math.floor((index - 1) / 2);
26            const parent = this.heap[parentIndex];
27
28            if (parent.priority <= element.priority) break;
29            // if the parent is bigger than the child then swap the parent and child
30            this.heap[index] = parent;
31            this.heap[parentIndex] = element;
32            index = parentIndex;
33        }
34    }
35
36    pop() {
37        const min = this.heap[0];
38        this.heap[0] = this.heap[this.size() - 1];
39        this.heap.pop();
40        this.bubble_down();
41        return min;
42    }
43
44    bubble_down() {
45        let index = 0;
46        let min = index;
47        const n = this.heap.length;
48
49        while (index < n) {
50            const left = 2 * index + 1;
51            const right = left + 1;
52
53            if (left < n && this.heap[left].priority < this.heap[min].priority) {
54                min = left;
55            }
56            if (right < n && this.heap[right].priority < this.heap[min].priority) {
57                min = right;
58            }
59            if (min === index) break;
60            [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61            index = min;
62        }
63    }
64
65    peek() {
66        return this.heap[0];
67    }
68
69    size() {
70        return this.heap.length;
71    }
72}
73
74function fun(arr) {
75    const heap = new MinHeap();
76    for (const x of arr) {
77        heap.push(new HeapItem(x));
78    }
79    const res = [];
80    for (let i = 0; i < 3; i++) {
81        res.push(heap.pop().item);
82    }
83    return res;
84}
85

Recommended Readings

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

Load More