3835. Count Subarrays With Cost Less Than or Equal to K
Problem Description
You are given an integer array nums and an integer k.
For any subarray nums[l..r] (a contiguous, non-empty portion of the array), its cost is defined as:
cost = (max(nums[l..r]) - min(nums[l..r])) * (r - l + 1)
In words, the cost is the difference between the largest and smallest values in the subarray, multiplied by the length of the subarray.
Your task is to count how many subarrays of nums have a cost that is less than or equal to k.
Return this count as an integer.
Example walkthrough:
Consider a subarray nums[l..r]. To compute its cost, you first find max(nums[l..r]) and min(nums[l..r]), take their difference, and then multiply by the number of elements in the subarray, which is r - l + 1. If the result is ≤ k, the subarray counts toward the answer.
Since every contiguous segment of the array is a valid subarray, you need to consider all possible pairs of indices l and r with l ≤ r, evaluate each subarray's cost, and tally up those that satisfy the condition.
How We Pick the Algorithm
Why Sliding Window?
This problem maps to Sliding Window through a short path in the full flowchart.
Counting subarrays where (max - min) * length <= k uses a sliding window with monotonic queues to efficiently track min and max.
Open in FlowchartIntuition
The brute-force idea would be to check every possible subarray, compute its cost, and count those that satisfy the condition. However, with up to O(n²) subarrays and the cost of finding the max and min for each, this becomes too slow for large inputs.
The key observation is about monotonicity. Suppose a subarray nums[l..r] has a cost that is less than or equal to k. What happens if we shrink this subarray, that is, pick a new left endpoint l' ≥ l and a new right endpoint r' ≤ r?
When we shrink a subarray:
- The length
r' - l' + 1becomes smaller or stays the same. - The maximum value can only stay the same or decrease, and the minimum value can only stay the same or increase. So the difference
max - mincan only stay the same or decrease.
Since both factors of the cost (the range max - min and the length) shrink or stay the same, the cost of any inner subarray is also less than or equal to k. This tells us the cost has a nice property: once a window fits, every sub-window inside it also fits.
This monotonic behavior is exactly what enables the two pointers technique. We fix the right endpoint r and slide it from left to right. For each r, we maintain the smallest valid left endpoint l such that the subarray nums[l..r] still has a cost ≤ k. Because of the monotonicity, as r moves right and the cost potentially grows, we only ever need to move l to the right too, never back. This means each pointer moves forward at most n times total.
Once we know the smallest valid l for a given r, every subarray ending at r with a left endpoint in the range [l, r] is valid. That gives us exactly r - l + 1 valid subarrays ending at r, which we add to the answer.
The remaining challenge is efficiently tracking the maximum and minimum of the current window as it expands and contracts. A plain recomputation each time would be slow, so we use two monotonic deques: one that keeps the maximum at its front and one that keeps the minimum at its front. As we add a new element on the right, we discard elements from the back of each deque that can no longer be the max or min. As we shrink the window from the left, we discard front elements that fall outside the window. This lets us read off the current max and min in O(1) time, making the whole algorithm run in linear time.
Pattern Learn more about Queue and Monotonic Queue patterns.
Solution Approach
Solution 1: Deque + Enumeration + Two Pointers
We enumerate the right endpoint r and use two pointers to maintain the smallest valid left endpoint l. To track the maximum and minimum of the current window efficiently, we use two monotonic deques.
Data structures used:
q1: a deque storing indices whose corresponding values are in decreasing order. Its frontq1[0]always points to the index of the maximum value in the current window.q2: a deque storing indices whose corresponding values are in increasing order. Its frontq2[0]always points to the index of the minimum value in the current window.l: the left pointer of the window.ans: the running count of valid subarrays.
Step-by-step walkthrough:
-
We iterate over the array with
ras the right endpoint andx = nums[r]as the current value. -
Maintain the maximum deque
q1. Before adding the new indexr, we pop indices from the back ofq1whose values are less than or equal tox:while q1 and nums[q1[-1]] <= x: q1.pop() q1.append(r)This keeps
q1decreasing, so the maximum of the window is always atnums[q1[0]]. -
Maintain the minimum deque
q2. Similarly, we pop indices from the back whose values are greater than or equal tox:while q2 and nums[q2[-1]] >= x: q2.pop() q2.append(r)This keeps
q2increasing, so the minimum of the window is always atnums[q2[0]]. -
Shrink the window with the left pointer. While the window has more than one element (
l < r) and the current cost exceedsk, we movelto the right:while l < r and (nums[q1[0]] - nums[q2[0]]) * (r - l + 1) > k: l += 1 if q1[0] < l: q1.popleft() if q2[0] < l: q2.popleft()The cost is computed as
(nums[q1[0]] - nums[q2[0]]) * (r - l + 1), wherenums[q1[0]]is the max andnums[q2[0]]is the min. Each time we advancel, if the front of a deque falls outside the window (its index is less thanl), we pop it from the front so the deques only contain indices within[l, r].The condition
l < rensures we never shrink the window below a single element. A subarray of length1always has cost0, which is≤ k, so a single-element window is always valid. -
Count valid subarrays ending at
r. Once the window is valid, every subarray ending atrwith left endpoint in[l, r]satisfies the condition. There arer - l + 1such subarrays, so we add this to the answer:ans += r - l + 1 -
After processing all right endpoints, we return
ans.
Why the two pointers never move backward: Because of the monotonicity established earlier, as r increases, the valid l can only stay the same or move forward. This guarantees that across the whole loop, l advances at most n times.
Complexity analysis:
- Time complexity:
O(n), wherenis the length ofnums. Each index is added to and removed from each deque at most once, and the left pointerlonly moves forward, so the total work is linear. - Space complexity:
O(n), for the two deques in the worst case.
Example Walkthrough
Let's trace through the algorithm with a small example:
Input: nums = [1, 3, 2], k = 4
We process each right endpoint r from left to right, maintaining the window [l, r], the max-deque q1, the min-deque q2, and the running answer ans. We start with l = 0 and ans = 0.
Step 1: r = 0, x = nums[0] = 1
- Maintain
q1(max):q1is empty, so just append. →q1 = [0] - Maintain
q2(min):q2is empty, so just append. →q2 = [0] - Shrink check: The condition
l < ris0 < 0→ false. No shrinking needed (single-element window always has cost0). - Count: Subarrays ending at
r = 0with left in[0, 0]: that'sr - l + 1 = 0 - 0 + 1 = 1.- Valid subarray:
[1](cost =(1-1)*1 = 0 ≤ 4) ✓
- Valid subarray:
ans = 0 + 1 = 1
Step 2: r = 1, x = nums[1] = 3
- Maintain
q1(max):nums[q1[-1]] = nums[0] = 1 <= 3, so pop index0. Nowq1is empty, append1. →q1 = [1] - Maintain
q2(min):nums[q2[-1]] = nums[0] = 1 >= 3? No (1 < 3). Append1. →q2 = [0, 1] - Shrink check: max =
nums[q1[0]] = nums[1] = 3, min =nums[q2[0]] = nums[0] = 1. Cost =(3 - 1) * (1 - 0 + 1) = 2 * 2 = 4. Is4 > 4? No. Window stays.l = 0. - Count:
r - l + 1 = 1 - 0 + 1 = 2.- Valid subarrays ending at
r = 1:[3](cost0) and[1, 3](cost4) ✓
- Valid subarrays ending at
ans = 1 + 2 = 3
Step 3: r = 2, x = nums[2] = 2
- Maintain
q1(max):nums[q1[-1]] = nums[1] = 3 <= 2? No. Append2. →q1 = [1, 2] - Maintain
q2(min):nums[q2[-1]] = nums[1] = 3 >= 2? Yes, pop index1. Nextnums[q2[-1]] = nums[0] = 1 >= 2? No. Append2. →q2 = [0, 2] - Shrink check: max =
nums[q1[0]] = nums[1] = 3, min =nums[q2[0]] = nums[0] = 1. Cost =(3 - 1) * (2 - 0 + 1) = 2 * 3 = 6. Is6 > 4? Yes, shrink.l = 1. Isq1[0] = 1 < 1? No. Isq2[0] = 0 < 1? Yes, pop front ofq2. →q2 = [2]- Recheck: max =
nums[q1[0]] = nums[1] = 3, min =nums[q2[0]] = nums[2] = 2. Cost =(3 - 2) * (2 - 1 + 1) = 1 * 2 = 2. Is2 > 4? No. Stop shrinking.l = 1.
- Count:
r - l + 1 = 2 - 1 + 1 = 2.- Valid subarrays ending at
r = 2:[2](cost0) and[3, 2](cost(3-2)*2 = 2) ✓ - Note
[1, 3, 2]was excluded because its cost6 > 4.
- Valid subarrays ending at
ans = 3 + 2 = 5
Result: ans = 5
Verification by brute force — all 6 subarrays of [1, 3, 2]:
| Subarray | max | min | length | cost | ≤ 4? |
|---|---|---|---|---|---|
[1] | 1 | 1 | 1 | 0 | ✓ |
[3] | 3 | 3 | 1 | 0 | ✓ |
[2] | 2 | 2 | 1 | 0 | ✓ |
[1,3] | 3 | 1 | 2 | 4 | ✓ |
[3,2] | 3 | 2 | 2 | 2 | ✓ |
[1,3,2] | 3 | 1 | 3 | 6 | ✗ |
Five subarrays satisfy the condition, matching our computed ans = 5. The two-pointer approach reached the same answer without examining every subarray explicitly, demonstrating how the monotonic deques and forward-only pointer movement keep the work linear.
Solution Implementation
1from collections import deque
2from typing import List
3
4
5class Solution:
6 def countSubarrays(self, nums: List[int], k: int) -> int:
7 # Total count of valid subarrays.
8 answer = 0
9
10 # max_deque: indices whose values are in decreasing order; front holds the window maximum.
11 max_deque: deque[int] = deque()
12 # min_deque: indices whose values are in increasing order; front holds the window minimum.
13 min_deque: deque[int] = deque()
14
15 # Left boundary of the sliding window.
16 left = 0
17
18 # Expand the window one element at a time using the right boundary.
19 for right, value in enumerate(nums):
20 # Maintain decreasing order in max_deque: pop smaller-or-equal values from the back.
21 while max_deque and nums[max_deque[-1]] <= value:
22 max_deque.pop()
23 # Maintain increasing order in min_deque: pop larger-or-equal values from the back.
24 while min_deque and nums[min_deque[-1]] >= value:
25 min_deque.pop()
26
27 # Add the current index to both deques.
28 max_deque.append(right)
29 min_deque.append(right)
30
31 # Shrink the window from the left while the condition is violated:
32 # (current_max - current_min) * window_length > k
33 while left < right and (nums[max_deque[0]] - nums[min_deque[0]]) * (right - left + 1) > k:
34 left += 1
35 # Drop indices that fall outside the new window boundary.
36 if max_deque[0] < left:
37 max_deque.popleft()
38 if min_deque[0] < left:
39 min_deque.popleft()
40
41 # Every subarray ending at `right` with start in [left, right] is valid.
42 answer += right - left + 1
43
44 return answer
451class Solution {
2 public long countSubarrays(int[] nums, long k) {
3 long answer = 0;
4
5 // Monotonic decreasing deque: stores indices whose values are in
6 // decreasing order, so the front always holds the index of the
7 // maximum value in the current window.
8 Deque<Integer> maxDeque = new ArrayDeque<>();
9
10 // Monotonic increasing deque: stores indices whose values are in
11 // increasing order, so the front always holds the index of the
12 // minimum value in the current window.
13 Deque<Integer> minDeque = new ArrayDeque<>();
14
15 // Left boundary of the sliding window.
16 int left = 0;
17
18 // Right boundary of the sliding window; iterate over each element.
19 for (int right = 0; right < nums.length; right++) {
20 int current = nums[right];
21
22 // Maintain the decreasing order in maxDeque: remove all indices
23 // from the back whose values are less than or equal to current.
24 while (!maxDeque.isEmpty() && nums[maxDeque.peekLast()] <= current) {
25 maxDeque.pollLast();
26 }
27
28 // Maintain the increasing order in minDeque: remove all indices
29 // from the back whose values are greater than or equal to current.
30 while (!minDeque.isEmpty() && nums[minDeque.peekLast()] >= current) {
31 minDeque.pollLast();
32 }
33
34 // Add the current index to both deques.
35 maxDeque.addLast(right);
36 minDeque.addLast(right);
37
38 // Shrink the window from the left while the score of the window
39 // exceeds k. The score is defined as:
40 // (max value - min value) * (window length)
41 while (left < right
42 && (long) (nums[maxDeque.peekFirst()] - nums[minDeque.peekFirst()])
43 * (right - left + 1) > k) {
44 left++;
45 // If the front index of maxDeque is now outside the window,
46 // remove it.
47 if (maxDeque.peekFirst() < left) {
48 maxDeque.pollFirst();
49 }
50 // If the front index of minDeque is now outside the window,
51 // remove it.
52 if (minDeque.peekFirst() < left) {
53 minDeque.pollFirst();
54 }
55 }
56
57 // All subarrays ending at "right" with start index in [left, right]
58 // satisfy the constraint, so add their count.
59 answer += right - left + 1;
60 }
61
62 return answer;
63 }
64}
651class Solution {
2public:
3 long long countSubarrays(vector<int>& nums, long long k) {
4 long long answer = 0;
5
6 // maxDeque: indices of candidates for the window maximum,
7 // values are kept in non-increasing order (front = max).
8 // minDeque: indices of candidates for the window minimum,
9 // values are kept in non-decreasing order (front = min).
10 deque<int> maxDeque, minDeque;
11
12 int left = 0;
13 int n = static_cast<int>(nums.size());
14
15 for (int right = 0; right < n; right++) {
16 int current = nums[right];
17
18 // Maintain a monotonically non-increasing deque for the maximum.
19 // Pop smaller-or-equal elements from the back since they can never
20 // be the maximum while the newer, larger element is in the window.
21 while (!maxDeque.empty() && nums[maxDeque.back()] <= current) {
22 maxDeque.pop_back();
23 }
24
25 // Maintain a monotonically non-decreasing deque for the minimum.
26 // Pop larger-or-equal elements from the back for the same reason.
27 while (!minDeque.empty() && nums[minDeque.back()] >= current) {
28 minDeque.pop_back();
29 }
30
31 maxDeque.push_back(right);
32 minDeque.push_back(right);
33
34 // Shrink the window from the left while the score
35 // (max - min) * length exceeds k.
36 while (left < right &&
37 static_cast<long long>(nums[maxDeque.front()] - nums[minDeque.front()]) *
38 (right - left + 1) > k) {
39 left++;
40
41 // Remove indices that have fallen out of the window [left, right].
42 if (maxDeque.front() < left) {
43 maxDeque.pop_front();
44 }
45 if (minDeque.front() < left) {
46 minDeque.pop_front();
47 }
48 }
49
50 // Every subarray ending at 'right' with start in [left, right]
51 // is valid, contributing (right - left + 1) subarrays.
52 answer += right - left + 1;
53 }
54
55 return answer;
56 }
57};
581/**
2 * Counts the number of subarrays where (max - min) * length <= k.
3 * Uses two monotonic deques to track the maximum and minimum values
4 * within the current sliding window.
5 *
6 * @param nums - The input array of numbers.
7 * @param k - The threshold value for the condition.
8 * @returns The total count of valid subarrays.
9 */
10function countSubarrays(nums: number[], k: number): number {
11 // Accumulates the total number of valid subarrays.
12 let answer = 0;
13
14 // Monotonic decreasing deque storing indices; front holds the window's maximum.
15 const maxDeque: number[] = [];
16 // Monotonic increasing deque storing indices; front holds the window's minimum.
17 const minDeque: number[] = [];
18
19 // Head and tail pointers for the max deque (simulating a deque via array indices).
20 let maxHead = 0;
21 let maxTail = 0;
22 // Head and tail pointers for the min deque.
23 let minHead = 0;
24 let minTail = 0;
25
26 // Left boundary of the sliding window.
27 let left = 0;
28
29 // Expand the window by moving the right boundary one step at a time.
30 for (let right = 0; right < nums.length; right++) {
31 const value = nums[right];
32
33 // Maintain the max deque as monotonically decreasing:
34 // pop indices whose values are not greater than the incoming value.
35 while (maxHead < maxTail && nums[maxDeque[maxTail - 1]] <= value) {
36 maxTail--;
37 }
38 // Maintain the min deque as monotonically increasing:
39 // pop indices whose values are not less than the incoming value.
40 while (minHead < minTail && nums[minDeque[minTail - 1]] >= value) {
41 minTail--;
42 }
43
44 // Push the current index into both deques.
45 maxDeque[maxTail++] = right;
46 minDeque[minTail++] = right;
47
48 // Shrink the window from the left while the condition is violated:
49 // (currentMax - currentMin) * windowLength > k.
50 while (
51 left < right &&
52 (nums[maxDeque[maxHead]] - nums[minDeque[minHead]]) * (right - left + 1) > k
53 ) {
54 left++;
55 // Discard deque fronts that fall outside the new window.
56 if (maxDeque[maxHead] < left) {
57 maxHead++;
58 }
59 if (minDeque[minHead] < left) {
60 minHead++;
61 }
62 }
63
64 // Every subarray ending at 'right' with start index in [left, right] is valid.
65 answer += right - left + 1;
66 }
67
68 return answer;
69}
70Time and Space Complexity
-
Time Complexity:
O(n), wherenis the length of the arraynums. The outer loop iterates over each element ofnumsexactly once via theenumeratecall, contributingO(n). Although there are innerwhileloops, each element is pushed onto and popped from each monotonic deque (q1andq2) at most once across the entire execution, so the total work for the deque operations is amortizedO(n). Similarly, the left pointerlonly moves forward and never resets, so thewhile l < r ...loop advanceslat mostntimes in total. Combining these, the overall time complexity isO(n). -
Space Complexity:
O(n), wherenis the length of the arraynums. The two dequesq1andq2store indices intonums, and in the worst case (e.g., a strictly monotonic array) each deque can hold up tonindices simultaneously. The remaining variables (ans,l,r,x) use only constant additional space. Therefore, the dominant space usage isO(n).
Pattern Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Using the wrong comparison operator when maintaining the deques (< vs <=)
A subtle but critical mistake is using strict inequalities (< and >) instead of non-strict ones (<= and >=) when popping from the back of the deques:
# WRONG: strict comparison leaves stale equal-valued indices in the deque while max_deque and nums[max_deque[-1]] < value: max_deque.pop()
When there are duplicate values, this leaves older indices with the same value in the deque. The front index of such a deque can then point to a position that is smaller (further left) than necessary. Consider what happens during window shrinking: the older duplicate index may fall out of the window range [left, right] before its newer twin, and if you only check if max_deque[0] < left, you may leave a stale index sitting at the front, or accidentally pop a still-valid extremum.
Why <=/>= is correct: By popping on equality, we guarantee that for any group of equal values, only the most recent (rightmost) index survives in the deque. This index stays valid in the window the longest, which is exactly what we want — it leaves the window last, so the front-popping logic (if deque[0] < left) behaves correctly.
Solution: Always use <= for the max deque and >= for the min deque, as shown in the reference code.
Pitfall 2: Shrinking the window below a single element
The loop condition while left < right and ... is essential. If you write only:
# WRONG: can advance left past right while (nums[max_deque[0]] - nums[min_deque[0]]) * (right - left + 1) > k: left += 1 ...
then for a single element where the cost somehow seems to violate k, left could be pushed past right, producing a negative contribution right - left + 1 and emptying the deques (leading to an IndexError on max_deque[0]).
Key insight: A subarray of length 1 always has cost (x - x) * 1 = 0 ≤ k (assuming k ≥ 0). So a single-element window is always valid and we must never shrink past it.
Solution: Keep the left < right guard so the window never collapses below one element:
while left < right and (nums[max_deque[0]] - nums[min_deque[0]]) * (right - left + 1) > k: left += 1 if max_deque[0] < left: max_deque.popleft() if min_deque[0] < left: min_deque.popleft()
Pitfall 3: Forgetting to evict outdated indices from the front of the deques
When left advances, indices that are now outside [left, right] must be removed from the front of each deque. Omitting these checks:
# WRONG: stale indices remain at the front while left < right and (...) > k: left += 1 # missing front-eviction!
causes max_deque[0] or min_deque[0] to reference an index < left, so the computed max/min reflects an element no longer in the window. This corrupts every subsequent cost calculation.
Solution: After each left += 1, prune any front index that has fallen out of range:
if max_deque[0] < left: max_deque.popleft() if min_deque[0] < left: min_deque.popleft()
Because at most one index per deque can become invalid each time left advances by one, a single if (rather than a while) suffices here.
Pitfall 4: Recomputing max/min by rescanning the window
A tempting but inefficient approach is to call max(nums[left:right+1]) and min(nums[left:right+1]) inside the loop instead of maintaining deques. This turns each cost evaluation into an O(n) scan, degrading the overall complexity to O(n²) (or worse), which will time out on large inputs.
Solution: Rely on the monotonic deques to retrieve the window max (nums[max_deque[0]]) and min (nums[min_deque[0]]) in O(1), preserving the overall O(n) time complexity.
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapHow many ways can you arrange the three letters A, B and C?
Recommended Readings
Queue Intro Following the Foundation Course Stay on that path and use the Foundation Course Queue module courses foundation queue_fifo_model instead This page is a quick Core Patterns refresher for students who already know the basics Think of the last time you stood in line to buy movie tickets The first person
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 A monotonic stack keeps its values in
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!