644. Maximum Average Subarray II 🔒
Problem Description
You are given an integer array nums
with n
elements and an integer k
.
Your task is to find a contiguous subarray whose length is at least k
that has the maximum possible average value. Return this maximum average value.
In other words, among all subarrays with length greater than or equal to k
, you need to find the one with the highest average (sum of elements divided by the number of elements) and return that average value.
The answer will be accepted if it has a calculation error less than 10^-5
from the correct answer.
For example, if nums = [1, 12, -5, -6, 50, 3]
and k = 4
, you need to check all subarrays with length 4, 5, and 6:
- Subarrays of length 4:
[1, 12, -5, -6]
with average 0.5,[12, -5, -6, 50]
with average 12.75,[-5, -6, 50, 3]
with average 10.5 - Subarrays of length 5:
[1, 12, -5, -6, 50]
with average 10.4,[12, -5, -6, 50, 3]
with average 10.8 - Subarray of length 6:
[1, 12, -5, -6, 50, 3]
with average 9.16667
The maximum average among all these subarrays is 12.75.
Intuition
The key insight is that we can use binary search on the answer itself. Instead of trying to find the maximum average directly, we can ask a simpler question: "Is there a subarray of length at least k
with average value greater than or equal to some value v
?"
Why does this work? Because if we can find such a subarray with average ≥ v
, then the maximum average must be at least v
. If we cannot find such a subarray, then the maximum average must be less than v
. This creates a monotonic property perfect for binary search.
The search space for our answer is bounded - the minimum possible average is the minimum element in the array (when we take just that element if k=1
), and the maximum possible average is the maximum element in the array (when we take just that element if k=1
).
Now, how do we check if there exists a subarray with length ≥ k
and average ≥ v
? Here's the clever transformation:
If a subarray [a₁, a₂, ..., aⱼ]
has average ≥ v
, then:
(a₁ + a₂ + ... + aⱼ) / j ≥ v
a₁ + a₂ + ... + aⱼ ≥ v × j
(a₁ - v) + (a₂ - v) + ... + (aⱼ - v) ≥ 0
This transforms our problem! If we subtract v
from each element, we just need to check if there's a subarray of length ≥ k
with non-negative sum.
To efficiently check this, we use a sliding window technique with prefix sums. We maintain the minimum prefix sum seen so far (at least k
positions back), and check if the current prefix sum minus this minimum is non-negative. This gives us the maximum sum for any subarray ending at the current position with length ≥ k
.
By combining binary search with this efficient checking mechanism, we can find the maximum average in O(n log(max-min))
time.
Learn more about Binary Search and Prefix Sum patterns.
Solution Approach
The solution uses binary search combined with a sliding window technique to find the maximum average.
Binary Search Setup:
- Initialize left boundary
l = min(nums)
and right boundaryr = max(nums)
- Set precision
eps = 1e-5
for the binary search termination condition - While
r - l >= eps
, computemid = (l + r) / 2
and check if there exists a subarray with length≥ k
and average≥ mid
- If such a subarray exists, update
l = mid
(the answer could be higher) - Otherwise, update
r = mid
(the answer must be lower)
The Check Function:
The check(v)
function determines if there's a subarray of length ≥ k
with average ≥ v
.
-
Transform the problem: Subtract
v
from each element conceptually. Now we need to find if there's a subarray of length≥ k
with sum≥ 0
. -
Initial window: Calculate the sum of the first
k
elements minusk * v
:s = sum(nums[:k]) - k * v
If
s >= 0
, we found a valid subarray immediately. -
Sliding window with prefix sum optimization:
- Maintain
s
as the cumulative sum up to current position (all elements minusv
) - Maintain
t
as the cumulative sum of elements that are nowk
positions behind - Maintain
mi
as the minimum prefix sum seen so far (at leastk
positions back)
For each new element at position
i
(wherei >= k
):s += nums[i] - v # Add current element to cumulative sum t += nums[i - k] - v # Add element that's k positions behind mi = min(mi, t) # Update minimum prefix sum
- Maintain
-
Check condition: If
s >= mi
, it means there exists a subarray ending at positioni
with length≥ k
and sum≥ 0
. This is becauses - mi
represents the sum of some subarray with length≥ k
.
Why this works:
s
represents the sum of all elements from index 0 toi
(after subtractingv
)mi
represents the minimum prefix sum among positions 0 toi-k
s - mi
gives us the maximum sum of any subarray ending ati
with length≥ k
- If
s - mi >= 0
(equivalent tos >= mi
), we found a valid subarray
Time Complexity: O(n * log((max-min)/eps))
where n
is the array length. The binary search runs O(log((max-min)/eps))
iterations, and each check takes O(n)
time.
Space Complexity: O(1)
as we only use a few variables to track the sums and minimum values.
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 a small example with nums = [1, 12, -5, -6, 50, 3]
and k = 4
.
Step 1: Binary Search Setup
l = min(nums) = -6
,r = max(nums) = 50
- We'll binary search for the maximum average between -6 and 50
Step 2: First Binary Search Iteration
mid = (-6 + 50) / 2 = 22
- Check if there's a subarray of length ≥ 4 with average ≥ 22
Step 3: Running check(22)
- Transform array by subtracting 22:
[-21, -10, -27, -28, 28, -19]
- Need to find if there's a subarray of length ≥ 4 with sum ≥ 0
Initial window (first k=4 elements):
- Sum = -21 + (-10) + (-27) + (-28) = -86
- Since -86 < 0, continue sliding
Sliding to position 4 (index 4):
s = -86 + 28 = -58
(cumulative sum from 0 to 4)t = -21
(cumulative sum from 0 to 0)mi = -21
(minimum prefix sum so far)- Check: Is -58 ≥ -21? No, continue
Sliding to position 5 (index 5):
s = -58 + (-19) = -77
(cumulative sum from 0 to 5)t = -21 + (-10) = -31
(cumulative sum from 0 to 1)mi = min(-21, -31) = -31
- Check: Is -77 ≥ -31? No
No valid subarray found with average ≥ 22, so r = 22
Step 4: Second Binary Search Iteration
mid = (-6 + 22) / 2 = 8
- Check if there's a subarray of length ≥ 4 with average ≥ 8
Step 5: Running check(8)
- Transform array by subtracting 8:
[-7, 4, -13, -14, 42, -5]
Initial window:
- Sum = -7 + 4 + (-13) + (-14) = -30
- Since -30 < 0, continue sliding
Sliding to position 4:
s = -30 + 42 = 12
(cumulative sum from 0 to 4)t = -7
(cumulative sum from 0 to 0)mi = -7
- Check: Is 12 ≥ -7? Yes!
- This means subarray from position 1 to 4 has sum = 12 - (-7) = 19 ≥ 0
- Original subarray
[12, -5, -6, 50]
has average = 51/4 = 12.75 ≥ 8 ✓
Valid subarray found, so l = 8
Step 6: Continue Binary Search The process continues, narrowing the range:
- Next
mid = (8 + 22) / 2 = 15
, check fails, sor = 15
- Next
mid = (8 + 15) / 2 = 11.5
, check succeeds, sol = 11.5
- Next
mid = (11.5 + 15) / 2 = 13.25
, check fails, sor = 13.25
- Next
mid = (11.5 + 13.25) / 2 = 12.375
, check succeeds, sol = 12.375
- Continue until
r - l < eps
Final Answer: The algorithm converges to approximately 12.75, which is indeed the maximum average (from subarray [12, -5, -6, 50]
).
Solution Implementation
1class Solution:
2 def findMaxAverage(self, nums: List[int], k: int) -> float:
3 def can_achieve_average(target_avg: float) -> bool:
4 """
5 Check if there exists a subarray of length at least k
6 with average >= target_avg.
7
8 We transform the problem by subtracting target_avg from each element.
9 If any subarray sum >= 0, then its average >= target_avg.
10 """
11 # Calculate sum of first k elements after transformation
12 current_sum = sum(nums[:k]) - k * target_avg
13 if current_sum >= 0:
14 return True
15
16 # Use sliding window with prefix sum optimization
17 # prefix_sum tracks sum of elements that have left the minimum window
18 prefix_sum = 0
19 min_prefix_sum = 0 # Minimum prefix sum seen so far
20
21 for i in range(k, len(nums)):
22 # Add new element to current sum
23 current_sum += nums[i] - target_avg
24 # Add element that just left the k-size window to prefix
25 prefix_sum += nums[i - k] - target_avg
26 # Track minimum prefix sum (best point to start subarray)
27 min_prefix_sum = min(min_prefix_sum, prefix_sum)
28
29 # If current_sum - min_prefix_sum >= 0, we found a valid subarray
30 # This represents the best subarray ending at position i
31 if current_sum >= min_prefix_sum:
32 return True
33
34 return False
35
36 # Binary search on the answer
37 epsilon = 1e-5
38 left, right = min(nums), max(nums)
39
40 while right - left >= epsilon:
41 mid = (left + right) / 2
42 if can_achieve_average(mid):
43 # If we can achieve this average, try for a higher one
44 left = mid
45 else:
46 # If we cannot achieve this average, try a lower one
47 right = mid
48
49 return left
50
1class Solution {
2 /**
3 * Finds the maximum average value of any contiguous subarray of length at least k.
4 * Uses binary search on the answer combined with a sliding window technique.
5 *
6 * @param nums the input array
7 * @param k the minimum length of subarray
8 * @return the maximum average value
9 */
10 public double findMaxAverage(int[] nums, int k) {
11 // Precision for binary search termination
12 double epsilon = 1e-5;
13
14 // Initialize binary search bounds with minimum and maximum values in array
15 double left = 1e10;
16 double right = -1e10;
17
18 // Find actual minimum and maximum values in the array
19 for (int num : nums) {
20 left = Math.min(left, num);
21 right = Math.max(right, num);
22 }
23
24 // Binary search for the maximum average
25 while (right - left >= epsilon) {
26 double mid = (left + right) / 2;
27
28 // Check if we can find a subarray with average >= mid
29 if (canFindSubarrayWithAverage(nums, k, mid)) {
30 left = mid; // Try to find a larger average
31 } else {
32 right = mid; // Current average is too large
33 }
34 }
35
36 return left;
37 }
38
39 /**
40 * Checks if there exists a contiguous subarray of length at least k
41 * with average value >= targetAverage.
42 *
43 * @param nums the input array
44 * @param k the minimum length of subarray
45 * @param targetAverage the target average to check against
46 * @return true if such subarray exists, false otherwise
47 */
48 private boolean canFindSubarrayWithAverage(int[] nums, int k, double targetAverage) {
49 // Calculate sum of first k elements minus targetAverage
50 // If sum >= 0, then average of these k elements >= targetAverage
51 double currentSum = 0;
52 for (int i = 0; i < k; ++i) {
53 currentSum += nums[i] - targetAverage;
54 }
55
56 // Check if first k elements already satisfy the condition
57 if (currentSum >= 0) {
58 return true;
59 }
60
61 // Use sliding window to check subarrays of length > k
62 double previousSum = 0; // Sum of elements that can be excluded from current window
63 double minPreviousSum = 0; // Minimum sum of prefix that can be excluded
64
65 for (int i = k; i < nums.length; ++i) {
66 // Extend current window to include nums[i]
67 currentSum += nums[i] - targetAverage;
68
69 // Add the element that just left the k-size window to previousSum
70 previousSum += nums[i - k] - targetAverage;
71
72 // Track minimum prefix sum that can be excluded
73 minPreviousSum = Math.min(minPreviousSum, previousSum);
74
75 // Check if current window minus the minimum prefix has sum >= 0
76 // This represents the best subarray ending at position i with length >= k
77 if (currentSum >= minPreviousSum) {
78 return true;
79 }
80 }
81
82 return false;
83 }
84}
85
1class Solution {
2public:
3 double findMaxAverage(vector<int>& nums, int k) {
4 // Binary search precision
5 double epsilon = 1e-5;
6
7 // Binary search bounds: minimum and maximum possible averages
8 double left = *min_element(nums.begin(), nums.end());
9 double right = *max_element(nums.begin(), nums.end());
10
11 // Lambda function to check if we can find a subarray with average >= targetAvg
12 auto canAchieveAverage = [&](double targetAvg) {
13 // Calculate sum of first k elements minus targetAvg
14 // If sum >= 0, then average of these k elements >= targetAvg
15 double currentSum = 0;
16 for (int i = 0; i < k; ++i) {
17 currentSum += nums[i] - targetAvg;
18 }
19
20 // Check if first k elements already satisfy the condition
21 if (currentSum >= 0) {
22 return true;
23 }
24
25 // Use sliding window to check subarrays of length > k
26 double prefixSum = 0; // Sum of elements that have left the minimum window
27 double minPrefixSum = 0; // Minimum prefix sum seen so far
28
29 for (int i = k; i < nums.size(); ++i) {
30 // Extend window to include nums[i]
31 currentSum += nums[i] - targetAvg;
32
33 // Add the element that just left the minimum k-window to prefix
34 prefixSum += nums[i - k] - targetAvg;
35
36 // Track minimum prefix sum (best starting point to exclude)
37 minPrefixSum = min(minPrefixSum, prefixSum);
38
39 // Check if current window minus the minimum prefix gives average >= targetAvg
40 // This represents the best subarray ending at position i
41 if (currentSum >= minPrefixSum) {
42 return true;
43 }
44 }
45
46 return false;
47 };
48
49 // Binary search for the maximum achievable average
50 while (right - left >= epsilon) {
51 double mid = (left + right) / 2;
52
53 if (canAchieveAverage(mid)) {
54 // If we can achieve this average, try for a higher one
55 left = mid;
56 } else {
57 // If we cannot achieve this average, try for a lower one
58 right = mid;
59 }
60 }
61
62 return left;
63 }
64};
65
1/**
2 * Finds the maximum average value of any contiguous subarray of length at least k
3 * Uses binary search on the answer combined with a sliding window technique
4 * @param nums - The input array of numbers
5 * @param k - The minimum length of the subarray
6 * @returns The maximum average value
7 */
8function findMaxAverage(nums: number[], k: number): number {
9 const EPSILON = 1e-5;
10
11 // Binary search bounds: minimum and maximum possible average values
12 let leftBound = Math.min(...nums);
13 let rightBound = Math.max(...nums);
14
15 /**
16 * Checks if there exists a subarray with length >= k that has average >= targetAvg
17 * @param targetAvg - The target average to check
18 * @returns True if such subarray exists, false otherwise
19 */
20 const canAchieveAverage = (targetAvg: number): boolean => {
21 // Calculate sum of first k elements minus k * targetAvg
22 // If sum >= 0, then average of first k elements >= targetAvg
23 let currentSum = nums.slice(0, k).reduce((acc, val) => acc + val, 0) - targetAvg * k;
24
25 if (currentSum >= 0) {
26 return true;
27 }
28
29 // Track the prefix sum and minimum prefix sum for optimization
30 let prefixSum = 0;
31 let minPrefixSum = 0;
32
33 // Extend the window and check all possible subarrays
34 for (let i = k; i < nums.length; i++) {
35 // Add new element to current sum
36 currentSum += nums[i] - targetAvg;
37
38 // Update prefix sum (elements that can be excluded from the beginning)
39 prefixSum += nums[i - k] - targetAvg;
40
41 // Track minimum prefix sum seen so far
42 minPrefixSum = Math.min(minPrefixSum, prefixSum);
43
44 // If current sum minus minimum prefix >= 0, we found a valid subarray
45 // This represents the best subarray ending at position i
46 if (currentSum >= minPrefixSum) {
47 return true;
48 }
49 }
50
51 return false;
52 };
53
54 // Binary search for the maximum average
55 while (rightBound - leftBound >= EPSILON) {
56 const midValue = (leftBound + rightBound) / 2;
57
58 if (canAchieveAverage(midValue)) {
59 // If we can achieve this average, try for a higher one
60 leftBound = midValue;
61 } else {
62 // If we cannot achieve this average, try for a lower one
63 rightBound = midValue;
64 }
65 }
66
67 return leftBound;
68}
69
Time and Space Complexity
The time complexity is O(n × log M)
, where n
is the length of the array nums
and M
is the difference between the maximum and minimum values in the array.
The algorithm uses binary search on the answer space [min(nums), max(nums)]
. The binary search continues while r - l >= eps
, where eps = 1e-5
. The number of iterations is O(log((max(nums) - min(nums))/eps))
, which simplifies to O(log M)
when considering M = max(nums) - min(nums)
.
In each iteration of the binary search, the check
function is called, which:
- Computes the sum of the first
k
elements:O(k)
- Iterates through the remaining
n - k
elements:O(n - k)
- Overall takes
O(n)
time
Therefore, the total time complexity is O(n × log M)
.
The space complexity is O(1)
. The algorithm only uses a constant amount of extra space for variables like s
, t
, mi
, l
, r
, mid
, and eps
, regardless of the input size.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Initialization of Minimum Prefix Sum
The Pitfall:
A common mistake is initializing min_prefix_sum
with a large positive value like float('inf')
instead of 0
. This breaks the algorithm's logic.
# WRONG - This will fail to find valid subarrays
min_prefix_sum = float('inf')
Why it's wrong:
- The
min_prefix_sum
represents the minimum sum of a prefix that can be excluded from our total sum - Initially, we can choose to exclude nothing (empty prefix), which has sum 0
- If we initialize with
float('inf')
, we might miss valid subarrays that use all elements from the start
The Solution:
Always initialize min_prefix_sum = 0
to represent the option of not excluding any prefix:
min_prefix_sum = 0 # Correct initialization
2. Off-by-One Error in Sliding Window
The Pitfall:
Incorrectly calculating which element to add to the prefix sum, often using nums[i-k+1]
instead of nums[i-k]
.
# WRONG - This adds the wrong element to prefix prefix_sum += nums[i - k + 1] - target_avg
Why it's wrong:
- At position
i
, we're considering subarrays of lengthk
toi+1
- The element at
i-k
is the one that just became eligible to be excluded - Using
i-k+1
would track the wrong element, leading to incorrect subarray sums
The Solution:
Use the correct index i-k
for the element that's exactly k
positions behind:
prefix_sum += nums[i - k] - target_avg # Correct indexing
3. Precision Issues in Binary Search
The Pitfall: Using integer division or incorrect epsilon value, leading to infinite loops or premature termination.
# WRONG - Integer division loses precision mid = (left + right) // 2 # WRONG - Epsilon too large, might miss the optimal answer epsilon = 0.01
Why it's wrong:
- Integer division truncates decimals, causing loss of precision in average calculations
- Too large epsilon might terminate before finding the answer within the required
10^-5
precision
The Solution: Use floating-point division and appropriate epsilon:
mid = (left + right) / 2 # Float division epsilon = 1e-5 # Or smaller, like 1e-6 for safety
4. Forgetting Initial k-Length Window Check
The Pitfall: Only checking windows in the sliding phase and forgetting to check if the first k elements already form a valid subarray.
# WRONG - Missing initial check
def can_achieve_average(target_avg):
current_sum = sum(nums[:k]) - k * target_avg
# Forgot to check if current_sum >= 0 here!
prefix_sum = 0
min_prefix_sum = 0
# ... rest of sliding window
Why it's wrong:
- The first k elements form a valid subarray of exactly length k
- If their average is already >= target_avg, we should return True immediately
- Missing this check causes incorrect results when the optimal subarray is exactly the first k elements
The Solution: Always check the initial k-length window before sliding:
current_sum = sum(nums[:k]) - k * target_avg
if current_sum >= 0: # Don't forget this check!
return True
Which data structure is used to implement recursion?
Recommended Readings
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
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
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!