3113. Find the Number of Subarrays Where Boundary Elements Are Maximum
Problem Description
You are given an array of positive integers nums
.
Your task is to count the number of subarrays where the first element and the last element are both equal to the largest element in that subarray.
In other words, for each subarray you consider, you need to check if:
- The maximum value in the subarray appears at both the beginning and end positions
- This means the boundary elements (first and last) must be the same value, and this value must be the largest in the entire subarray
For example:
- A subarray
[3, 1, 2, 3]
would count if3
is the maximum value in this subarray - A subarray
[5]
of length 1 always counts since the first and last element are the same - A subarray
[2, 3, 2]
would not count because3
is the maximum but doesn't appear at both ends
Return the total count of all such valid subarrays.
Intuition
Let's think about what makes a valid subarray. For a subarray to be valid, its first and last elements must be equal to the maximum element. This means:
- The first and last elements must be the same value
- No element in between can be larger than this boundary value
A key observation is that when we fix an element x
as our boundary (first/last) element, any valid subarray ending at x
must have all its elements less than or equal to x
.
This leads us to think about processing the array from left to right. For each position, we want to know: how many valid subarrays can end at this position?
When we encounter an element x
:
- Any subarray of length 1 (just
[x]
itself) is always valid - For longer subarrays, we need
x
to also appear somewhere earlier, and all elements between those two occurrences ofx
must be≤ x
The challenge is tracking which previous occurrences of values can form valid subarrays with our current element. If we encounter a value larger than some previous value, that larger value "blocks" the previous value from forming valid subarrays that extend to future positions.
This blocking behavior suggests using a monotonic stack. We maintain a decreasing stack where:
- Each entry tracks a value and how many valid subarrays can end with that value
- When we see a new element
x
, we pop all smaller elements (they're blocked byx
) - If we find an equal element in the stack, we can extend all its valid subarrays to include the current position
- We track the count of valid subarrays ending at each position
The monotonic decreasing property ensures that any value in our stack can potentially form valid subarrays with future equal values, as there's no larger "blocking" element between them.
Learn more about Stack, Binary Search and Monotonic Stack patterns.
Solution Approach
We implement the solution using a monotonic decreasing stack. Each element in the stack is a pair [x, cnt]
where x
is the value and cnt
is the count of valid subarrays ending with x
as the rightmost element.
Here's how the algorithm works:
-
Initialize an empty stack and answer counter
stk = []
to maintain our monotonic stackans = 0
to accumulate the total count
-
Process each element
x
in the arrayFor each element
x
, we perform three key operations:a) Pop smaller elements:
while stk and stk[-1][0] < x: stk.pop()
We remove all elements from the stack that are smaller than
x
. These elements cannot form valid subarrays that extend beyondx
becausex
would be larger than them, violating the condition that boundary elements must be the maximum.b) Update or add the current element:
if not stk or stk[-1][0] > x: stk.append([x, 1]) else: stk[-1][1] += 1
- If the stack is empty or the top element is greater than
x
, we push[x, 1]
onto the stack. The count is1
because at minimum,[x]
itself forms a valid subarray. - If the top element equals
x
, we've found another occurrence ofx
that can extend all previous valid subarrays ending withx
. We increment the count by1
to include the new subarray formed between thisx
and the previousx
.
c) Add to answer:
ans += stk[-1][1]
We add the count of valid subarrays ending at the current position to our answer.
- If the stack is empty or the top element is greater than
-
Return the total count
The key insight is that the monotonic decreasing property of the stack ensures that:
- Any value in the stack can potentially form valid subarrays with future occurrences of the same value
- When we see value
x
again, all elements between the two occurrences ofx
are guaranteed to be≤ x
(due to our popping mechanism) - The count accumulates as we find more occurrences of the same value, representing all possible valid subarrays ending at each position
Time Complexity: O(n)
where n
is the length of the array, as each element is pushed and popped at most once.
Space Complexity: O(n)
for the stack in the worst case when all elements are in decreasing order.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with the array nums = [3, 1, 3, 5, 1]
.
We'll track our monotonic decreasing stack and count valid subarrays at each step.
Initial state:
- Stack:
[]
- Answer:
0
Step 1: Process element 3
- Stack is empty, so push
[3, 1]
(value 3 with count 1) - Stack:
[[3, 1]]
- Add count to answer:
ans = 0 + 1 = 1
- Valid subarrays so far:
[3]
Step 2: Process element 1
- 1 < 3 (top of stack), so push
[1, 1]
as new element - Stack:
[[3, 1], [1, 1]]
- Add count to answer:
ans = 1 + 1 = 2
- Valid subarrays ending here:
[1]
Step 3: Process element 3
- Pop
[1, 1]
since 1 < 3 (removing blocked element) - Stack after popping:
[[3, 1]]
- Top element equals 3, so increment its count:
[[3, 2]]
- Add count to answer:
ans = 2 + 2 = 4
- Valid subarrays ending here:
[3]
and[3,1,3]
Step 4: Process element 5
- Pop
[3, 2]
since 3 < 5 (removing blocked element) - Stack after popping:
[]
- Stack is empty, so push
[5, 1]
- Stack:
[[5, 1]]
- Add count to answer:
ans = 4 + 1 = 5
- Valid subarrays ending here:
[5]
Step 5: Process element 1
- 1 < 5 (top of stack), so push
[1, 1]
as new element - Stack:
[[5, 1], [1, 1]]
- Add count to answer:
ans = 5 + 1 = 6
- Valid subarrays ending here:
[1]
Final Answer: 6
The valid subarrays are:
[3]
at index 0[1]
at index 1[3,1,3]
from index 0 to 2[3]
at index 2[5]
at index 3[1]
at index 4
Notice how when we encountered the second 3
, we could form 2 valid subarrays: the single element [3]
and the longer subarray [3,1,3]
. The stack helped us track that we had seen 3
before and that all elements between were ≤ 3, making [3,1,3]
valid.
Solution Implementation
1class Solution:
2 def numberOfSubarrays(self, nums: List[int]) -> int:
3 # Stack to maintain elements in decreasing order
4 # Each element is [value, count of subarrays ending at this position]
5 stack = []
6 total_subarrays = 0
7
8 for current_num in nums:
9 # Remove all elements from stack that are smaller than current number
10 # This maintains the decreasing order property
11 while stack and stack[-1][0] < current_num:
12 stack.pop()
13
14 # If stack is empty or top element is greater than current number,
15 # add new element with count 1 (the subarray containing just itself)
16 if not stack or stack[-1][0] > current_num:
17 stack.append([current_num, 1])
18 else:
19 # If top element equals current number, increment its count
20 # This counts additional subarrays ending at current position
21 stack[-1][1] += 1
22
23 # Add the count of valid subarrays ending at current position
24 total_subarrays += stack[-1][1]
25
26 return total_subarrays
27
1class Solution {
2 public long numberOfSubarrays(int[] nums) {
3 // Stack to maintain elements in descending order
4 // Each entry stores [value, count of subarrays ending at current position with this value as maximum]
5 Deque<int[]> stack = new ArrayDeque<>();
6 long totalCount = 0;
7
8 for (int currentNum : nums) {
9 // Remove all elements from stack that are smaller than current number
10 // These elements cannot be the maximum in any subarray that includes current number
11 while (!stack.isEmpty() && stack.peek()[0] < currentNum) {
12 stack.pop();
13 }
14
15 // Check if we need to add a new entry or update existing one
16 if (stack.isEmpty() || stack.peek()[0] > currentNum) {
17 // Current number is either the smallest seen so far or smaller than stack top
18 // Create new entry with count 1 (subarray containing only current element)
19 stack.push(new int[] {currentNum, 1});
20 } else {
21 // Current number equals stack top, increment the count
22 // This means we can extend previous subarrays with same maximum
23 stack.peek()[1]++;
24 }
25
26 // Add count of valid subarrays ending at current position
27 totalCount += stack.peek()[1];
28 }
29
30 return totalCount;
31 }
32}
33
1class Solution {
2public:
3 long long numberOfSubarrays(vector<int>& nums) {
4 // Stack to maintain pairs of (value, count of subarrays ending at current position with this max)
5 vector<pair<int, int>> stack;
6 long long result = 0;
7
8 for (int currentNum : nums) {
9 // Remove elements from stack that are smaller than current number
10 // (they can't be the maximum in any subarray containing current element)
11 while (!stack.empty() && stack.back().first < currentNum) {
12 stack.pop_back();
13 }
14
15 // If stack is empty or top element is greater than current number,
16 // push new pair with count 1 (current element forms 1 subarray by itself)
17 if (stack.empty() || stack.back().first > currentNum) {
18 stack.push_back(make_pair(currentNum, 1));
19 }
20 // If top element equals current number, increment its count
21 // (extends all previous subarrays with this maximum)
22 else {
23 stack.back().second++;
24 }
25
26 // Add count of valid subarrays ending at current position
27 result += stack.back().second;
28 }
29
30 return result;
31 }
32};
33
1/**
2 * Counts the number of subarrays where each element is the maximum element
3 * @param nums - Input array of numbers
4 * @returns The total count of valid subarrays
5 */
6function numberOfSubarrays(nums: number[]): number {
7 // Stack to store [value, count] pairs
8 // Maintains elements in non-increasing order
9 const stack: [number, number][] = [];
10 let answer = 0;
11
12 for (const currentNum of nums) {
13 // Remove all elements from stack that are smaller than current number
14 // These elements cannot be the maximum in any subarray ending at current position
15 while (stack.length > 0 && stack[stack.length - 1][0] < currentNum) {
16 stack.pop();
17 }
18
19 // If stack is empty or top element is greater than current number,
20 // push current number with count 1
21 if (stack.length === 0 || stack[stack.length - 1][0] > currentNum) {
22 stack.push([currentNum, 1]);
23 } else {
24 // If top element equals current number, increment its count
25 stack[stack.length - 1][1]++;
26 }
27
28 // Add the count of subarrays ending at current position
29 // where the current element or equal elements are the maximum
30 answer += stack[stack.length - 1][1];
31 }
32
33 return answer;
34}
35
Time and Space Complexity
Time Complexity: O(n)
The algorithm iterates through the array once with a for loop that processes each element. For each element, it performs:
- A while loop that pops elements from the stack
- At most one append operation or one increment operation
- Adding to the answer counter
Although there's a nested while loop inside the for loop, each element can be pushed onto the stack at most once and popped at most once throughout the entire execution. This means the total number of operations across all iterations is bounded by 2n
(each element is pushed once and popped at most once), resulting in an amortized O(1)
operation per element and overall O(n)
time complexity.
Space Complexity: O(n)
The space complexity is determined by the stack stk
which stores pairs of [value, count]
. In the worst case scenario, when the array is strictly decreasing, no elements would be popped from the stack, and we would have one entry in the stack for each distinct element. Since we could have up to n
distinct elements in the array, the stack could grow to size n
, resulting in O(n)
space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Misunderstanding the Count Accumulation
The Problem: A common mistake is thinking that when we encounter a duplicate value, we should only add 1 to the count, rather than incrementing and then adding the total count. This misunderstanding leads to undercounting valid subarrays.
Incorrect Implementation:
if not stack or stack[-1][0] > current_num: stack.append([current_num, 1]) total_subarrays += 1 # Wrong: only adding 1 else: stack[-1][1] += 1 total_subarrays += 1 # Wrong: only adding 1 instead of stack[-1][1]
Why It's Wrong:
When we see a value x
for the second time, we're not just forming one new subarray. We're forming:
- The subarray
[x]
by itself - The subarray from the previous
x
to currentx
When we see it a third time, we form three subarrays ending at that position, and so on.
Correct Approach:
Always add stack[-1][1]
to the answer, which represents all valid subarrays ending at the current position.
Pitfall 2: Not Handling the Stack Pop Condition Correctly
The Problem:
Using <=
instead of <
in the while loop condition, which would incorrectly pop equal elements.
Incorrect Implementation:
while stack and stack[-1][0] <= current_num: # Wrong: using <= instead of < stack.pop()
Why It's Wrong:
If we pop elements that are equal to the current number, we lose the ability to track consecutive occurrences of the same value. For example, with array [3, 3, 3]
:
- Processing first
3
: stack becomes[[3, 1]]
- Processing second
3
: if we use<=
, we'd pop the first3
, losing track of valid subarrays - This would give us count 3 instead of the correct count 6
Correct Approach:
Only pop elements strictly smaller than the current number: stack[-1][0] < current_num
Pitfall 3: Forgetting Edge Cases
The Problem: Not properly handling single-element subarrays or assuming the stack will always have elements.
Example Scenario:
For array [5, 4, 3, 2, 1]
(strictly decreasing), each element forms only one valid subarray (itself).
Potential Bug:
# Forgetting to check if stack is empty before accessing stack[-1] if stack[-1][0] > current_num: # Error if stack is empty stack.append([current_num, 1])
Correct Approach: Always check if the stack is not empty before accessing its elements:
if not stack or stack[-1][0] > current_num: stack.append([current_num, 1])
The three-steps of Depth First Search are:
- Identify states;
- Draw the state-space tree;
- DFS on the state-space tree.
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
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
Monotonic Stack Deque Intro If you'd prefer a video div class responsive iframe iframe src https www youtube com embed Dq_ObZwTY_Q title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div The word monotonic means a list or
Want a Structured Path to Master System Design Too? Don’t Miss This!