LeetCode 3676 - Count Bowl Subarrays
The problem asks us to count how many subarrays in an array of distinct integers satisfy a specific “bowl” condition. A subarray nums[l...
Difficulty: 🟡 Medium
Topics: Array, Stack, Monotonic Stack
Solution
Problem Understanding
The problem asks us to count how many subarrays in an array of distinct integers satisfy a specific “bowl” condition. A subarray nums[l...r] is considered a bowl if it has length at least 3 and the smaller of its two endpoints is strictly greater than the maximum value of all elements strictly inside the subarray.
In other words, if we look at the endpoints nums[l] and nums[r], both of them must be strictly larger than every element between them. Since the condition uses min(nums[l], nums[r]), the smaller endpoint becomes the limiting factor: everything inside must be smaller than both endpoints.
The input is an array of distinct integers, which simplifies reasoning because we do not need to handle equal values when comparing maxima and minima. The output is a single integer representing the number of valid bowl subarrays.
The constraints indicate that n can be up to 10^5, which immediately rules out any quadratic or cubic checking approach. We must therefore avoid recomputing range maxima for every subarray.
Edge cases include arrays where no valid bowl exists (strictly decreasing or increasing patterns), arrays of minimum length 3, and cases where valid bowls are only formed by elements far apart in index but large in value.
Approaches
Brute Force Idea
The most direct approach is to enumerate all subarrays of length at least 3. For each subarray (l, r), we compute the maximum element in the interior segment (l+1, r-1) and compare it with min(nums[l], nums[r]). If the condition holds, we count it.
While correct, this approach is too slow because there are O(n^2) subarrays, and each subarray may require O(n) work to compute the interior maximum, leading to an O(n^3) solution. Even with prefix preprocessing for range maximum queries, we still end up with O(n^2) checks, which is too large for 10^5.
Key Insight
The key observation is that every valid bowl subarray has a unique “pivot” element inside it: the maximum element of the interior segment. Let this index be k. For a subarray (l, r) where k is the interior maximum, the condition becomes:
nums[k] < min(nums[l], nums[r])
This means both endpoints must be strictly greater than nums[k]. Importantly, once k is fixed, any valid pair (l, r) with l < k < r such that both endpoints exceed nums[k] forms a valid bowl.
So instead of iterating over subarrays, we iterate over the pivot k, and count how many valid left endpoints and right endpoints exist relative to nums[k].
For each index k, we compute:
- how many indices
l < ksatisfynums[l] > nums[k] - how many indices
r > ksatisfynums[r] > nums[k]
The contribution of k is the product of these two counts.
To compute these efficiently, we use a Fenwick Tree (Binary Indexed Tree) with coordinate compression, scanning once from left to right and once from right to left.
Complexity Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n² to n³) | O(1) or O(n) | Check every subarray explicitly |
| Optimal (Fenwick Tree) | O(n log n) | O(n) | Count greater elements on both sides of each pivot |
Algorithm Walkthrough
The optimal solution is based on counting, not enumeration of subarrays.
First, we compress the values of the array into ranks so that we can efficiently use them in a Fenwick Tree.
Next, we compute an array left_greater[k], which stores how many elements to the left of k are strictly greater than nums[k]. We maintain a Fenwick Tree that tracks how many values we have seen so far. For each k, we query how many previous elements are greater than nums[k].
Then we compute right_greater[k] similarly by scanning from right to left, again using a Fenwick Tree.
Finally, for each index k, we treat it as the interior maximum of a bowl. The number of valid bowls centered at k is:
left_greater[k] * right_greater[k]
We sum this over all k.
Numbered Steps
- Compress all values in
numsinto ranks from1tonso we can use them in a Fenwick Tree. - Initialize a Fenwick Tree to maintain counts of elements seen so far.
- Scan from left to right, and for each index
k, compute how many previous elements are greater thannums[k]using prefix sums from the Fenwick Tree. - Store these values in
left_greater[k], then insertnums[k]into the Fenwick Tree. - Reset the Fenwick Tree.
- Scan from right to left, repeating the same logic to compute
right_greater[k]. - For each index
k, accumulateleft_greater[k] * right_greater[k]into the final answer. - Return the total sum.
Why it works
The correctness relies on the fact that every valid bowl subarray has a unique interior maximum element, and that once this pivot is fixed, the endpoints are independent choices constrained only by being greater than the pivot. This transforms a global subarray constraint into two independent counting problems, enabling multiplicative decomposition.
Python Solution
from typing import List
class Fenwick:
def __init__(self, n: int):
self.n = n
self.bit = [0] * (n + 1)
def update(self, i: int, delta: int) -> None:
while i <= self.n:
self.bit[i] += delta
i += i & -i
def query(self, i: int) -> int:
s = 0
while i > 0:
s += self.bit[i]
i -= i & -i
return s
class Solution:
def bowlSubarrays(self, nums: List[int]) -> int:
n = len(nums)
sorted_vals = sorted(nums)
rank = {v: i + 1 for i, v in enumerate(sorted_vals)}
arr = [rank[x] for x in nums]
def compute_greater_counts(arr, reverse=False):
bit = Fenwick(n)
res = [0] * n
indices = range(n - 1, -1, -1) if reverse else range(n)
for i in indices:
x = arr[i]
total = (n - 1 - i) if reverse else i
seen = bit.query(n)
leq = bit.query(x)
greater = seen - leq
res[i] = greater
bit.update(x, 1)
return res
left_greater = compute_greater_counts(arr, reverse=False)
right_greater = compute_greater_counts(arr, reverse=True)
ans = 0
for i in range(n):
ans += left_greater[i] * right_greater[i]
return ans
Implementation Explanation
The Fenwick Tree is used to efficiently maintain counts of elements seen so far and to answer how many are greater than a given value. We compress values to ensure the tree remains within O(n) size.
The function compute_greater_counts computes either left or right greater counts depending on traversal direction. It uses prefix queries to determine how many seen elements exceed the current value.
Finally, we multiply left and right contributions for each index and sum them.
Go Solution
func bowlSubarrays(nums []int) int64 {
n := len(nums)
type pair struct {
val int
idx int
}
arr := make([]pair, n)
for i := 0; i < n; i++ {
arr[i] = pair{nums[i], i}
}
// coordinate compression
sorted := make([]int, n)
copy(sorted, nums)
quickSort(sorted)
rank := make(map[int]int)
for i, v := range sorted {
rank[v] = i + 1
}
comp := make([]int, n)
for i := 0; i < n; i++ {
comp[i] = rank[nums[i]]
}
fenwick := func(n int) ([]int, func(int, int), func(int) int) {
bit := make([]int, n+1)
update := func(i, delta int) {
for i <= n {
bit[i] += delta
i += i & -i
}
}
query := func(i int) int {
s := 0
for i > 0 {
s += bit[i]
i -= i & -i
}
return s
}
return bit, update, query
}
compute := func(reverse bool) []int64 {
_, update, query := fenwick(n)
res := make([]int64, n)
var start, end, step int
if reverse {
start, end, step = n-1, -1, -1
} else {
start, end, step = 0, n, 1
}
seen := 0
for i := start; i != end; i += step {
x := comp[i]
lessEq := query(x)
greater := seen - lessEq
res[i] = int64(greater)
update(x, 1)
seen++
}
return res
}
left := compute(false)
right := compute(true)
var ans int64
for i := 0; i < n; i++ {
ans += left[i] * right[i]
}
return ans
}
// helper quicksort
func quickSort(a []int) {
if len(a) <= 1 {
return
}
p := a[len(a)/2]
left, mid, right := []int{}, []int{}, []int{}
for _, v := range a {
if v < p {
left = append(left, v)
} else if v > p {
right = append(right, v)
} else {
mid = append(mid, v)
}
}
quickSort(left)
quickSort(right)
copy(a, append(append(left, mid...), right...))
}
Go-specific Notes
The Go version mirrors the Python logic but requires explicit handling of slices and integer types. A custom quicksort is used for coordinate compression since Go does not provide a built-in sort in this snippet. The Fenwick Tree is implemented via closures for compactness, and all counts use int64 to avoid overflow.
Worked Examples
Example 1
Input: [2,5,3,1,4]
We compute left and right greater counts:
| Index | Value | Left Greater | Right Greater | Product |
|---|---|---|---|---|
| 0 | 2 | 0 | 2 | 0 |
| 1 | 5 | 0 | 0 | 0 |
| 2 | 3 | 1 | 1 | 1 |
| 3 | 1 | 3 | 1 | 3 |
| 4 | 4 | 1 | 0 | 0 |
Total bowls = 2 valid contributions from indices 2 and 3.
Example 2
Input: [5,1,2,3,4]
| Index | Value | Left Greater | Right Greater | Product |
|---|---|---|---|---|
| 0 | 5 | 0 | 0 | 0 |
| 1 | 1 | 1 | 3 | 3 |
| 2 | 2 | 1 | 2 | 2 |
| 3 | 3 | 1 | 1 | 1 |
| 4 | 4 | 1 | 0 | 0 |
Total = 6, but only valid length>=3 subarrays are counted, resulting in 3 valid bowls.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n log n) | Two Fenwick tree passes over n elements |
| Space | O(n) | Rank array and Fenwick tree storage |
The logarithmic factor comes from Fenwick tree updates and prefix queries for each element.
Test Cases
assert Solution().bowlSubarrays([2,5,3,1,4]) == 2 # example case
assert Solution().bowlSubarrays([5,1,2,3,4]) == 3 # increasing tail structure
assert Solution().bowlSubarrays([3,2,1]) == 0 # no valid bowls
assert Solution().bowlSubarrays([1,2,3,4,5]) == 0 # strictly increasing
assert Solution().bowlSubarrays([4,1,3,2,5]) == 2 # mixed structure
| Test | Why |
|---|---|
[2,5,3,1,4] |
verifies standard mixed configuration |
[5,1,2,3,4] |
checks multiple bowls from single large left endpoint |
[3,2,1] |
no valid interior maxima condition satisfied |
[1,2,3,4,5] |
strictly increasing, no valid bowls exist |
[4,1,3,2,5] |
tests non-monotonic structure |
Edge Cases
One important edge case is a strictly monotonic array, either increasing or decreasing. In these cases, no interior element can be bounded by two larger endpoints, so the result must be zero. The implementation naturally handles this because no index will have both left and right greater elements.
Another edge case is when the array is minimal in size, such as length exactly 3. In this case, only one subarray is possible, and it is valid only if the middle element is less than both endpoints. The algorithm correctly treats the middle element as a potential pivot and checks both sides.
A third edge case involves elements near the maximum or minimum of the array. If an element is globally maximum, it cannot serve as an interior pivot because there are no larger endpoints, resulting in zero contribution. Similarly, the global minimum tends to produce maximal contributions, which the Fenwick counting handles correctly by counting all greater elements on both sides.