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)
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:
- The longest strictly increasing prefix ending at index
i
- 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 pointeri
backwards until we findnums[i] < nums[j]
(ori
becomes -1) - This ensures that if we keep prefix
nums[0...i]
and suffixnums[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 indexi
)- Plus 1 for removing everything before
j
(keeping only the suffix)
- We stop when
nums[j-1] >= nums[j]
because the suffix starting atj-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 EvaluatorExample 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
andn - 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)
- Removing
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, soi
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]
- This counts removing
- Check:
nums[4] = 4 < nums[5] = 6
? Yes, continue
Move to j = 4
:
nums[j] = 4
- Check prefix:
nums[0] = 2 < 4
? Yes, soi
stays at 0 - Add
i + 2 = 2
→ans = 6
- This counts removing
[1, 3, 5]
leaving[2] + [4, 6]
- And removing
[2, 1, 3, 5]
leaving just[4, 6]
- This counts removing
- Check:
nums[3] = 5 > nums[4] = 4
? Yes, STOP!- The suffix
[5, 4, 6]
is not strictly increasing
- The suffix
Final answer: 6 incremovable subarrays
These are:
[2, 1, 3, 5, 4, 6]
→ removes entire array →[]
[1, 3, 5, 4, 6]
→ removes suffix →[2]
[1, 3, 5, 4]
→ leaves[2, 6]
[2, 1, 3, 5, 4]
→ leaves[6]
[1, 3, 5]
→ leaves[2, 4, 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:
-
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. -
If the entire array is strictly increasing (checked by
if i == n - 1
), we return immediately with a constant time calculation. -
The second while loop starts from the end of the array and moves backwards. In the worst case, variable
j
iterates through all positions fromn-1
to1
. For each position ofj
, the inner while loop adjusts pointeri
backwards. The key observation is that pointeri
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 mostO(n)
operations in total, notO(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 isi + 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.
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
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
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
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!