LeetCode 2444 - Count Subarrays With Fixed Bounds
This problem asks us to count how many contiguous subarrays satisfy two exact boundary conditions at the same time: 1. The minimum element in the subarray must be exactly minK. 2. The maximum element in the subarray must be exactly maxK.
Difficulty: 🔴 Hard
Topics: Array, Queue, Sliding Window, Monotonic Queue
Solution
Problem Understanding
This problem asks us to count how many contiguous subarrays satisfy two exact boundary conditions at the same time:
- The minimum element in the subarray must be exactly
minK. - The maximum element in the subarray must be exactly
maxK.
The key detail is that these are exact matches, not inequalities. A valid subarray cannot simply have values greater than or equal to minK and less than or equal to maxK. Instead, the smallest number present must equal minK, and the largest number present must equal maxK.
We are given an integer array nums and two integers, minK and maxK. The goal is to return the total number of contiguous subarrays whose minimum and maximum values are exactly equal to these bounds.
For example, if nums = [1,3,5,2], minK = 1, and maxK = 5, then [1,3,5] is valid because its minimum is 1 and its maximum is 5. Similarly, [1,3,5,2] is valid for the same reason. However, [3,5] is invalid because its minimum is 3, not 1.
The constraints are important:
nums.lengthcan be as large as10^5- Values can be as large as
10^6
Because the input size is large, any algorithm worse than roughly O(n log n) is likely too slow. A brute force solution that examines every possible subarray would require O(n²) or worse, which becomes infeasible for 10^5 elements.
Several edge cases immediately stand out:
If minK == maxK, then every valid subarray must contain only that exact value. For example, [1,1,1,1] with minK = maxK = 1 makes every subarray valid.
If elements outside the range [minK, maxK] appear, they instantly invalidate any subarray containing them. For example, if minK = 1 and maxK = 5, then a value like 7 breaks any candidate subarray.
Another tricky case occurs when one of the required values is missing. Even if all numbers lie within bounds, no subarray can be valid unless both minK and maxK appear.
Approaches
Brute Force Approach
A straightforward idea is to generate every possible subarray and compute its minimum and maximum values.
For every starting index i, we consider every ending index j, forming the subarray nums[i:j+1]. We then compute the minimum and maximum values inside that subarray and check whether:
min(subarray) == minKmax(subarray) == maxK
If both conditions hold, we increment our answer.
This approach is correct because it explicitly checks every possible subarray and verifies the problem constraints exactly. However, it is extremely inefficient.
There are O(n²) subarrays in an array of length n. If we recompute minimum and maximum values for each subarray, that costs O(n) per subarray, resulting in O(n³) time complexity.
Even with optimization, where we maintain running minimum and maximum while extending subarrays, we still require O(n²) time, which is too slow for n = 10^5.
Key Insight for an Optimal Solution
The crucial observation is that we do not need to inspect every subarray individually.
Instead, we can process the array in a single pass while tracking:
- The most recent index of
minK - The most recent index of
maxK - The most recent invalid index, where the value is outside
[minK, maxK]
At every position i, we ask:
How many valid subarrays end at index
i?
A subarray ending at i is valid only if:
- It contains at least one
minK - It contains at least one
maxK - It contains no invalid values
The earliest valid start position is constrained by the most recent invalid index. Meanwhile, the latest possible start position that guarantees both required values is determined by the earlier of the latest minK and latest maxK.
This lets us compute the number of valid subarrays ending at each position in constant time.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(1) | Enumerates all subarrays and tracks min/max |
| Optimal Sliding Window | O(n) | O(1) | Tracks recent positions of required and invalid values |
Algorithm Walkthrough
Optimal Sliding Window Algorithm
- Initialize three indices:
last_min_index = -1, stores the latest occurrence ofminKlast_max_index = -1, stores the latest occurrence ofmaxKlast_invalid_index = -1, stores the latest index where a value falls outside[minK, maxK]
We initialize them to -1 because initially nothing has been seen.
2. Iterate through the array from left to right.
For each index i, examine nums[i].
3. Check whether the current value is invalid.
If nums[i] < minK or nums[i] > maxK, then this element cannot appear in any valid subarray.
Update:
last_invalid_index = i
This means any future valid subarray must start strictly after this position.
4. Update the latest occurrence of minK.
If nums[i] == minK, update:
last_min_index = i
5. Update the latest occurrence of maxK.
If nums[i] == maxK, update:
last_max_index = i
6. Determine whether a valid subarray ending at i exists.
A valid subarray ending at i must contain both required values.
The limiting position is:
min(last_min_index, last_max_index)
This tells us the earliest point where both required values are guaranteed to exist. 7. Count valid starting positions.
The start index must be greater than last_invalid_index.
Therefore:
valid_count = min(last_min_index, last_max_index) - last_invalid_index
If this value is negative, no valid subarray exists ending at i, so we add 0.
8. Add the contribution to the total answer.
Every valid starting point creates one valid subarray ending at i.
Why it works
The algorithm maintains an invariant:
At every index i, we know:
- The latest position where
minKappeared - The latest position where
maxKappeared - The latest invalid boundary
A valid subarray ending at i must begin after the invalid boundary and include both required values. The earliest position where both required values are guaranteed is the smaller of the two latest required indices.
Therefore:
min(last_min_index, last_max_index) - last_invalid_index
precisely counts how many valid starting positions exist.
Since every subarray is counted exactly once, when its ending index is processed, the algorithm is correct.
Python Solution
from typing import List
class Solution:
def countSubarrays(self, nums: List[int], minK: int, maxK: int) -> int:
last_min_index = -1
last_max_index = -1
last_invalid_index = -1
total_subarrays = 0
for index, value in enumerate(nums):
if value < minK or value > maxK:
last_invalid_index = index
if value == minK:
last_min_index = index
if value == maxK:
last_max_index = index
valid_endings = (
min(last_min_index, last_max_index)
- last_invalid_index
)
total_subarrays += max(0, valid_endings)
return total_subarrays
The implementation closely follows the algorithm.
We first initialize tracking indices to -1, indicating that neither boundary value nor invalid elements have been seen yet.
As we iterate through the array, each element may update one or more trackers. If the element lies outside the valid range, it resets the valid search region by updating last_invalid_index.
Whenever we encounter minK or maxK, we store their latest positions. At every index, we calculate how many valid subarrays end there.
The expression:
min(last_min_index, last_max_index)
ensures both required values exist. If one has not appeared yet, the result remains -1, naturally producing zero contribution.
Finally, we accumulate all valid endings into the total count.
Go Solution
func countSubarrays(nums []int, minK int, maxK int) int64 {
lastMinIndex := -1
lastMaxIndex := -1
lastInvalidIndex := -1
var totalSubarrays int64 = 0
for index, value := range nums {
if value < minK || value > maxK {
lastInvalidIndex = index
}
if value == minK {
lastMinIndex = index
}
if value == maxK {
lastMaxIndex = index
}
validEndings := min(lastMinIndex, lastMaxIndex) - lastInvalidIndex
if validEndings > 0 {
totalSubarrays += int64(validEndings)
}
}
return totalSubarrays
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
The Go solution mirrors the Python implementation almost exactly.
One important difference is integer handling. The problem can produce a very large number of valid subarrays, potentially close to n * (n + 1) / 2, which exceeds the range of a 32-bit integer. Therefore, the return type is int64, and we explicitly cast contributions before accumulation.
Go also does not provide a built-in min function for integers, so we define a small helper function.
Unlike Python, Go slices are already reference types, so no special handling for arrays is required.
Worked Examples
Example 1
Input:
nums = [1,3,5,2,7,5]
minK = 1
maxK = 5
| i | nums[i] | last_min | last_max | last_invalid | valid endings | total |
|---|---|---|---|---|---|---|
| 0 | 1 | 0 | -1 | -1 | 0 | 0 |
| 1 | 3 | 0 | -1 | -1 | 0 | 0 |
| 2 | 5 | 0 | 2 | -1 | 1 | 1 |
| 3 | 2 | 0 | 2 | -1 | 1 | 2 |
| 4 | 7 | 0 | 2 | 4 | 0 | 2 |
| 5 | 5 | 0 | 5 | 4 | 0 | 2 |
At index 2, the subarray [1,3,5] becomes valid.
At index 3, [1,3,5,2] is valid.
The 7 invalidates future subarrays crossing index 4.
Final answer: 2.
Example 2
Input:
nums = [1,1,1,1]
minK = 1
maxK = 1
| i | nums[i] | last_min | last_max | last_invalid | valid endings | total |
|---|---|---|---|---|---|---|
| 0 | 1 | 0 | 0 | -1 | 1 | 1 |
| 1 | 1 | 1 | 1 | -1 | 2 | 3 |
| 2 | 1 | 2 | 2 | -1 | 3 | 6 |
| 3 | 1 | 3 | 3 | -1 | 4 | 10 |
Every subarray is valid.
Final answer: 10.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | We scan the array exactly once |
| Space | O(1) | Only a few tracking variables are used |
The algorithm is linear because every element is processed exactly once, and each step performs constant-time updates and arithmetic.
The space complexity is constant since no additional data structures proportional to input size are created.
Test Cases
sol = Solution()
assert sol.countSubarrays([1, 3, 5, 2, 7, 5], 1, 5) == 2
# Example case
assert sol.countSubarrays([1, 1, 1, 1], 1, 1) == 10
# Every subarray valid
assert sol.countSubarrays([2, 2, 2], 1, 5) == 0
# Missing both bounds
assert sol.countSubarrays([1, 5], 1, 5) == 1
# Smallest valid subarray
assert sol.countSubarrays([1], 1, 1) == 1
# Single element valid case
assert sol.countSubarrays([1, 2, 3], 1, 3) == 1
# Only whole array works
assert sol.countSubarrays([1, 2, 7, 1, 5], 1, 5) == 1
# Invalid element splits regions
assert sol.countSubarrays([5, 1, 5, 1], 1, 5) == 6
# Multiple overlapping valid subarrays
assert sol.countSubarrays([1, 5, 1, 5, 1], 1, 5) == 10
# Dense valid combinations
assert sol.countSubarrays([8, 8, 8], 1, 5) == 0
# All elements invalid
| Test | Why |
|---|---|
[1,3,5,2,7,5] |
Official example |
[1,1,1,1] |
minK == maxK case |
[2,2,2] |
Missing required values |
[1,5] |
Minimum valid size |
[1] |
Single element boundary |
[1,2,3] |
Only one valid subarray |
[1,2,7,1,5] |
Invalid value resets window |
[5,1,5,1] |
Overlapping valid subarrays |
[1,5,1,5,1] |
Stress overlapping counting |
[8,8,8] |
Entire array invalid |
Edge Cases
When minK == maxK
This case is easy to mishandle because the minimum and maximum requirements collapse into the same value. Every valid subarray must consist entirely of that exact number.
For example:
nums = [1,1,1]
minK = 1
maxK = 1
Every subarray becomes valid. The implementation naturally handles this because both last_min_index and last_max_index update together.
Invalid Numbers Splitting the Array
Any value outside [minK, maxK] instantly invalidates every subarray containing it.
For example:
nums = [1,5,9,1,5]
The 9 forces a reset. Valid subarrays before index 2 cannot connect with those after index 2.
The algorithm handles this using last_invalid_index, ensuring future starts occur only after the invalid position.
Missing One Required Boundary
A subarray cannot be valid unless it contains both minK and maxK.
For example:
nums = [1,2,2,2]
minK = 1
maxK = 5
Although all values are within range, 5 never appears. Therefore, no valid subarray exists.
The implementation naturally handles this because one of the tracked indices remains -1, making:
min(last_min_index, last_max_index)
negative, which contributes zero to the answer.