LeetCode 795: Number of Subarrays with Bounded Maximum
A clear explanation of counting contiguous subarrays whose maximum value lies inside a given inclusive range.
Problem Restatement
We are given an integer array nums and two integers left and right.
We need to count how many non-empty contiguous subarrays have a maximum value in this range:
left <= max(subarray) <= right
The subarray must be contiguous.
Input and Output
| Item | Meaning |
|---|---|
| Input | nums, left, and right |
| Output | Number of valid non-empty contiguous subarrays |
| Valid subarray | Its maximum value is between left and right |
| Range | Inclusive: [left, right] |
Function shape:
class Solution:
def numSubarrayBoundedMax(
self,
nums: list[int],
left: int,
right: int
) -> int:
...
Examples
Example 1:
nums = [2, 1, 4, 3]
left = 2
right = 3
The valid subarrays are:
[2]
[2, 1]
[3]
Their maximum values are 2, 2, and 3.
So the answer is:
3
Example 2:
nums = [2, 9, 2, 5, 6]
left = 2
right = 8
The value 9 is greater than right, so any subarray containing 9 is invalid.
The valid subarrays are inside the segments around 9.
The answer is:
7
First Thought: Check Every Subarray
A direct solution is to generate every subarray.
For each start index, extend the end index one step at a time while tracking the maximum value.
Then count the subarray if its maximum is between left and right.
This works, but there are O(n^2) subarrays.
For large input, this is too slow.
Key Insight
Instead of asking:
How many subarrays have maximum in [left, right]?
we can count:
subarrays with maximum <= right
-
subarrays with maximum < left
That gives exactly the subarrays whose maximum is at least left and at most right.
Define:
count_at_most(bound)
as the number of subarrays whose maximum value is at most bound.
Then the answer is:
count_at_most(right) - count_at_most(left - 1)
Counting Subarrays With Maximum At Most a Bound
Scan the array from left to right.
Keep length, the number of consecutive elements ending at the current index that are <= bound.
If nums[i] <= bound, then every subarray ending at i and starting inside this valid streak is valid.
So we add length to the answer.
If nums[i] > bound, the streak breaks, and length becomes 0.
Example:
nums = [2, 1, 4, 3]
bound = 3
Scan:
| Index | Value | Current valid streak | Added |
|---|---|---|---|
0 |
2 |
1 |
1 |
1 |
1 |
2 |
2 |
2 |
4 |
0 |
0 |
3 |
3 |
1 |
1 |
Total subarrays with maximum <= 3:
4
They are:
[2], [2, 1], [1], [3]
Now count maximum <= 1:
[1]
So the final answer is:
4 - 1 = 3
Algorithm
- Define
count_at_most(bound). - Inside it:
- Set
total = 0. - Set
length = 0. - For each number:
- If
num <= bound, incrementlength. - Otherwise, reset
length = 0. - Add
lengthtototal.
- If
- Set
- Return:
count_at_most(right) - count_at_most(left - 1)
Correctness
For a fixed bound, count_at_most(bound) counts exactly the subarrays whose maximum is at most bound.
When nums[i] <= bound, the current valid streak length tells us how many contiguous subarrays ending at i contain only values <= bound. Each of those subarrays has maximum at most bound, so adding length is correct.
When nums[i] > bound, no valid subarray ending at i can include this value. The valid streak must reset to 0.
Therefore, count_at_most(bound) returns the number of subarrays with maximum value at most bound.
A subarray has maximum in [left, right] exactly when its maximum is at most right and not at most left - 1. So subtracting:
count_at_most(right) - count_at_most(left - 1)
counts exactly the subarrays whose maximum lies in the required inclusive range.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(n) |
We scan the array twice |
| Space | O(1) |
Only counters are stored |
Implementation
class Solution:
def numSubarrayBoundedMax(
self,
nums: list[int],
left: int,
right: int
) -> int:
def count_at_most(bound: int) -> int:
total = 0
length = 0
for num in nums:
if num <= bound:
length += 1
else:
length = 0
total += length
return total
return count_at_most(right) - count_at_most(left - 1)
Code Explanation
The helper function counts subarrays whose maximum is at most bound:
def count_at_most(bound: int) -> int:
length stores the current streak of values that are <= bound:
length = 0
If the current number is allowed, we extend the streak:
if num <= bound:
length += 1
If the current number is too large, every subarray ending here is invalid, so we reset:
else:
length = 0
Each valid streak ending at the current index contributes length subarrays:
total += length
Finally, subtract the count of subarrays whose maximum is too small:
return count_at_most(right) - count_at_most(left - 1)
Alternative One-Pass Version
We can also solve it in one scan.
Track:
| Variable | Meaning |
|---|---|
last_too_large |
Last index where nums[i] > right |
last_valid |
Last index where nums[i] >= left |
At index i, the number of valid subarrays ending at i is:
last_valid - last_too_large
Implementation:
class Solution:
def numSubarrayBoundedMax(
self,
nums: list[int],
left: int,
right: int
) -> int:
answer = 0
last_too_large = -1
last_valid = -1
for i, num in enumerate(nums):
if num > right:
last_too_large = i
if num >= left:
last_valid = i
answer += last_valid - last_too_large
return answer
Testing
def run_tests():
s = Solution()
assert s.numSubarrayBoundedMax([2, 1, 4, 3], 2, 3) == 3
assert s.numSubarrayBoundedMax([2, 9, 2, 5, 6], 2, 8) == 7
assert s.numSubarrayBoundedMax([1, 1, 1], 2, 3) == 0
assert s.numSubarrayBoundedMax([2, 2, 2], 2, 2) == 6
assert s.numSubarrayBoundedMax([4, 4, 4], 2, 3) == 0
assert s.numSubarrayBoundedMax([1, 2, 3, 4], 2, 3) == 5
print("all tests passed")
run_tests()
Test coverage:
| Test | Why |
|---|---|
[2, 1, 4, 3] |
Standard example |
[2, 9, 2, 5, 6] |
Contains a value greater than right |
| All values too small | No subarray reaches left |
| All values exactly valid | Every subarray is valid |
| All values too large | Every subarray is invalid |
| Mixed increasing values | Checks range boundaries |