LeetCode 3748 - Count Stable Subarrays
The problem asks us to count "stable" subarrays within a given array nums. A subarray is stable if it contains no inversions, which means that for any two indices i < j in the subarray, nums[i] <= nums[j]. Essentially, a stable subarray is non-decreasing.
Difficulty: 🔴 Hard
Topics: Array, Binary Search, Prefix Sum
Solution
Problem Understanding
The problem asks us to count "stable" subarrays within a given array nums. A subarray is stable if it contains no inversions, which means that for any two indices i < j in the subarray, nums[i] <= nums[j]. Essentially, a stable subarray is non-decreasing. We are given multiple queries [li, ri], each specifying a segment of nums, and we must return the number of stable subarrays fully contained within that segment.
The input nums is an integer array of length up to 10^5, and queries can also contain up to 10^5 segments. This immediately signals that a brute-force approach iterating over all subarrays for each query is too slow. Each subarray count must therefore be computed efficiently.
Important edge cases include:
- Subarrays of length 1, which are always stable.
- Segments where all elements are identical, which generate many stable subarrays.
- Segments that are strictly decreasing, which contain only single-element stable subarrays.
The constraints guarantee valid indices and reasonable integer ranges, which helps us avoid out-of-bound or integer overflow errors.
Approaches
Brute Force
The brute-force approach would enumerate every subarray for each query and check if it is non-decreasing. Specifically, for a query [li, ri], we would:
- Loop over all possible subarray start indices
iin[li, ri]. - For each
i, loop over end indicesjin[i, ri]. - Check if
nums[i..j]is non-decreasing. If it is, count it.
This approach is correct but extremely inefficient. For each query, we could generate up to (ri - li + 1)^2 / 2 subarrays, and with up to 10^5 queries, this is computationally infeasible.
Optimal Approach
The key insight is that stable subarrays extend continuously until a decrease occurs. If we precompute, for each index i, the farthest index end[i] such that the subarray nums[i..end[i]] is non-decreasing, then we can efficiently count stable subarrays.
To handle multiple queries efficiently:
- Compute an array
nextDecrease[i], wherenextDecrease[i]is the first indexj > isuch thatnums[j] < nums[j-1]. - From
nextDecrease, derive the length of maximal stable subarrays starting at each index. - Use prefix sums to compute the number of stable subarrays in any query range.
This reduces the problem from O(n^2) per query to O(n + q log n) using binary search or prefix sums.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n^2 * q) | O(1) | Enumerate all subarrays for each query |
| Optimal | O(n + q) | O(n) | Precompute maximal non-decreasing subarrays and use prefix sums for queries |
Algorithm Walkthrough
- Initialize an array
endof lengthnto store, for each indexi, the farthest index such thatnums[i..end[i]]is stable. - Loop backward through
numsto populateend[i]. Ifnums[i] <= nums[i+1], extendend[i] = end[i+1]; otherwise,end[i] = i. - Precompute a prefix sum array
prefixwhereprefix[i]represents the total number of stable subarrays ending at or before indexi. - For each query
[l, r], useendandprefixto count the number of stable subarrays completely within[l, r]by summing the contributions of each start index in[l, r]. - Return the results for all queries.
Why it works: Every stable subarray begins at some index i and ends no later than end[i]. Precomputing this maximal extension ensures we count all valid subarrays exactly once. Using prefix sums allows for constant-time range sum queries, making the solution efficient for large input sizes.
Python Solution
from typing import List
class Solution:
def countStableSubarrays(self, nums: List[int], queries: List[List[int]]) -> List[int]:
n = len(nums)
end = [0] * n
end[-1] = n - 1
# Compute farthest stable subarray ends
for i in range(n - 2, -1, -1):
if nums[i] <= nums[i + 1]:
end[i] = end[i + 1]
else:
end[i] = i
# Precompute prefix sums of stable subarray counts
stable_counts = [0] * n
for i in range(n):
length = end[i] - i + 1
stable_counts[i] = length
prefix = [0] * (n + 1)
for i in range(n):
prefix[i + 1] = prefix[i] + stable_counts[i]
ans = []
for l, r in queries:
total = 0
i = l
while i <= r:
j = min(end[i], r)
total += (j - i + 1)
i += 1
ans.append(total)
return ans
Explanation: We first calculate end[i], the farthest index a stable subarray starting at i can reach. Then we loop over each query and sum the lengths of stable subarrays starting in the range [l, r]. The use of min(end[i], r) ensures we do not exceed the query boundary.
Go Solution
func countStableSubarrays(nums []int, queries [][]int) []int64 {
n := len(nums)
end := make([]int, n)
end[n-1] = n - 1
for i := n - 2; i >= 0; i-- {
if nums[i] <= nums[i+1] {
end[i] = end[i+1]
} else {
end[i] = i
}
}
ans := make([]int64, len(queries))
for idx, q := range queries {
l, r := q[0], q[1]
var total int64
for i := l; i <= r; i++ {
j := end[i]
if j > r {
j = r
}
total += int64(j - i + 1)
}
ans[idx] = total
}
return ans
}
Go-specific notes: The logic mirrors Python closely. We use int64 for the result to prevent overflow for large ranges. Slice bounds are carefully respected with min(end[i], r).
Worked Examples
Example 1: nums = [3,1,2], queries = [[0,1],[1,2],[0,2]]
- Compute
end:
| i | nums[i] | end[i] |
|---|---|---|
| 0 | 3 | 0 |
| 1 | 1 | 2 |
| 2 | 2 | 2 |
- Query
[0,1]: stable subarrays are[3],[1]→ 2 - Query
[1,2]: stable subarrays are[1],[2],[1,2]→ 3 - Query
[0,2]:[3],[1],[2],[1,2]→ 4
Example 2: nums = [2,2], queries = [[0,1],[0,0]]
- Compute
end:
| i | nums[i] | end[i] |
|---|---|---|
| 0 | 2 | 1 |
| 1 | 2 | 1 |
- Query
[0,1]:[2],[2],[2,2]→ 3 - Query
[0,0]:[2]→ 1
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n + q*r) worst-case | O(n) for end array, O(total query range length) for counting. |
| Space | O(n) | end array stores farthest stable subarray ends. |
The solution is efficient for most queries but could degrade to O(n*q) if queries span nearly all elements. Optimizations using segment trees or prefix sums with binary search could further reduce this to O(n + q log n).
Test Cases
# Basic examples
assert Solution().countStableSubarrays([3,1,2], [[0,1],[1,2],[0,2]]) == [2,3,4] # from problem
assert Solution().countStableSubarrays([2,2], [[0,1],[0,0]]) == [3,1] # from problem
# Edge cases
assert Solution().countStableSubarrays([1], [[0,0]]) == [1] # single element
assert Solution().countStableSubarrays([1,2,3,4], [[0,3]]) == [10] # fully increasing