644. Maximum Average Subarray II
Problem Description
You're given an integer array nums
which contains n
elements, and an integer k
. The task is to find a contiguous subarray whose length is at least k
which has the maximum possible average value. It's important to note that the answer needs to be accurate but is allowed a small margin for error. Specifically, any answer within 10^-5
(0.00001) of the actual answer will be considered correct.
A subarray, in this context, is a sequence of elements from the array nums
that lies next to each other, without any gaps. For example, if nums
is [1, 3, 5, 7, 9]
and k
is 2
, we have to find a contiguous sequence of length 2
or more that gives the highest average. The returned value should be the average of that subarray.
The essence of the problem is to find the optimal section of the list - it can be from k
to n
elements long - and calculate the average of the numbers within this range. The difficulty lies in doing this efficiently, as brute force methods that check all possible subarrays will be too slow when the array nums
is large.
Intuition
The intuition behind the solution involves binary search and prefix sums. Instead of directly searching for the maximum average, we flip the problem on its head: for a given average v
, determine if there's a subarray with an average greater than or equal to v
. Then we use binary search on v
, narrowing down the range until we find the maximum average to satisfy the condition.
Here's how we arrive at the solution approach:
- Initially, we know that the maximum average lies between the minimum and maximum values in our array
nums
. These are our initial lower (l
) and upper (r
) bounds for binary search. - We define a check function to verify if a subarray exists that has an average at least as large as
v
. This involves some smart tricks:- Calculate the prefix sum of the array elements minus
v
timesk
. This transforms the problem into finding a subarray sum that is non-negative. - Use a running sum (
s
) to keep track of the current sum of the lastk
elements, and a minimum sum (mi
) to keep the smallest sum of anyk
elements we've seen. We update these as we move the running sum forward throughnums
. - If, at any point, our running sum minus the minimum sum we've seen so far (
mi
) is non-negative, it means there is a subarray with an average of at leastv
.
- Calculate the prefix sum of the array elements minus
- The binary search operates by repeatedly checking the midpoint of our
l
andr
range against thecheck
function. If the function returnstrue
, we know an average of at leastmid
is possible, so we move the lower boundl
tomid
. Otherwise, we move the upper boundr
tomid
. - We continue the binary search until our range is less than
10^-5
, at which point we can return eitherl
orr
as our result, since they will be sufficiently close to the maximum average.
This solution approach is much more efficient than checking every possible subarray, as it takes advantage of the properties of averages and the structure of contiguous subarrays to reduce the problem to a binary search over possible average values.
Learn more about Binary Search and Prefix Sum patterns.
Solution Approach
The solution uses a combination of binary search and prefix sums to efficiently calculate the maximum average subarray. Here is a step-by-step explanation of how the implementation works:
-
Binary Search Initialization: We start by establishing our search range for the possible maximum average, with
l
being the minimum value innums
andr
being the maximum value. These represent the absolute possible bounds for the average. -
Helper Function -
check()
: This function is crucial; it checks if there exists a contiguous subarray with an average value of at leastv
. Here's how it operates:- Sum up the first
k
elements ofnums
, then subtractk*v
from this sum to gets
. This operation shifts the problem from finding a maximum average subarray to finding a non-negative sum subarray. - Initialize two variables,
t
andmi
, that represent the total sum and minimum sum encountered so far, respectively. Initially,t
is0
andmi
is the smallest possible value. - Iterate over the array starting from the
k-th
element. For each element at indexi
, addnums[i] - v
tos
andnums[i - k] - v
tot
, effectively moving thek-length
window one step forward. - Then, update
mi
to be the minimum of itself andt
, which is the sum of the firsti-k
elements of the array minusk*v
. This tracks the smallest sum we have seen up to that point, before considering the currentk-length
window. - If
s - mi >= 0
, the function returnsTrue
, indicating that it's possible to have an average of at leastv
. This is because we found a contiguous subarray where the sum of the numbers minusk*v
is at least zero, which means the average of that subarray is at leastv
.
- Sum up the first
-
Binary Search Loop: The binary search loop keeps iterating while
r - l
is greater than a very small epsilon value (1e-5
). Within this loop:- Calculate the midpoint
mid
as(l + r) / 2
. - Use the
check()
function with this midpoint value. Ifcheck(mid)
returnsTrue
, we know that it's possible to have a subarray with an average greater than or equal tomid
, so we move the lower bound up tomid
. If the result isFalse
, we make the upper boundr
equal tomid
. This continuously narrows the range of possible averages.
- Calculate the midpoint
-
Getting the Result: Once the binary search loop exits, the value of
l
(orr
, as they are very close to each other) is a sufficiently accurate representation of the maximum average, within an error margin of10^-5
.
The solution elegantly combines binary search, which is a classic divide and conquer algorithm, with a manipulation of sums to reframe the problem into one that is far more computationally efficient. The running time complexity is O(n * log(max(nums) - min(nums)))
, as we have to run our check once for each iteration of binary search, and the range of the binary search is dictated by the values within nums
. The linear pass in the check function, where we iterate over the array updates running sums, takes O(n)
time. The constant factor has been reduced significantly as compared to a brute force method that might consider every possible subarray individually, moving the solution from possibly quadratic time complexity to linearithmic.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's illustrate the solution approach with a small example. Assume we have an array nums = [1, 12, 2, 6, 7]
and k = 2
.
Step 1: Binary Search Initialization
- The minimum value in
nums
is1
, and the maximum value is12
. So we initialize our binary search range withl = 1
andr = 12
.
Step 2: Helper Function - check()
- For illustration purposes, let's assume the binary search is at a point where it's testing
v = 6.5
. We would initializes
as the sum of the firstk
elements minusk*v
, which is1 + 12 - 2*6.5 = 0
. Initializet
andmi
as well. Here,t
will start at0
andmi
will be0
, as we have not seen any sum yet.
Step 3: Iterate and Check
- Start iterating from the k-th element of
nums
(index1
): For each elementnums[i]
, wherei >= k
, do the following (example fori = 2
):- Add
nums[i] - v
tos
(running sum), sos += 2 - 6.5
, and we gets = -4.5
. - Add
nums[i - k] - v
tot
. Fori = 2
, it's1 - 6.5
(sincek=2
), sot = -5.5
. - Update
mi
to be the minimum ofmi
andt
. As this is the first update andt
is less than our initialmi
, we now setmi = t = -5.5
.
- Add
- Continuing this process, when
i = 4
(nums[i] = 7
):s += 7 - 6.5
, nows = 0.5 - 4.5 = -4
.t
has been updated as we moved along, let's say it is now-1.5
.mi
was updated along the way and it'smin(mi, t)
; we'll consider it-5.5
.
- When we check
s - mi
for eachi
, we are looking for it to be>= 0
. In this case, ati = 4
,s - mi
is-4 - (-5.5) = 1.5
, which is>= 0
. So, a subarray with an average at least as high as6.5
exists.
Step 4: Binary Search Loop
- We'd repeat the above process for different values of
v
, adjustingl
andr
with each iteration. Ifcheck(6.5)
returnedTrue
, we'd setl = 6.5
. If it returnedFalse
, we'd setr = 6.5
. - Continue until
r - l < 10^-5
.
Step 5: Result
- Once
l
andr
are within10^-5
of each other, the binary search terminates, and either value gives us the maximum possible average subarray to within the accepted error margin.
In this example, let's say the binary search concluded with l = 6.49995
and r = 6.50000
, with an error margin below 10^-5
, either l
or r
would serve as the result for the maximum average, which is approximately 6.5
.
Solution Implementation
1from typing import List
2
3class Solution:
4 def findMaxAverage(self, nums: List[int], k: int) -> float:
5 # Helper function to check if average value v can be the result
6 def can_be_average(v: float) -> bool:
7 # Initialize the sum of the first k elements adjusted by subtracting v
8 current_sum = sum(nums[:k]) - k * v
9 # If the current average is already >= 0, return True
10 if current_sum >= 0:
11 return True
12 prev_sum = min_sum = 0
13 # Iterate over the rest of the elements
14 for i in range(k, len(nums)):
15 # Update the sum for the new window by including the new element and excluding the old
16 current_sum += nums[i] - v
17 # Update the sum for the previous window
18 prev_sum += nums[i - k] - v
19 # Keep track of the minimum sum encountered so far
20 min_sum = min(min_sum, prev_sum)
21 # If the current window sum is greater than any seen before, return True
22 if current_sum >= min_sum:
23 return True
24 return False
25
26 # Defining precision for the binary search result
27 precision = 1e-5
28 # Setting the lower and upper bounds of binary search to the min and max of nums
29 left, right = min(nums), max(nums)
30
31 # Binary search routine to find maximum average
32 while right - left >= precision:
33 mid = (left + right) / 2
34 # If the current mid value can be an average, update the lower bound
35 if can_be_average(mid):
36 left = mid
37 # Otherwise, update the upper bound
38 else:
39 right = mid
40 # The result is the left bound after the binary search loop ends
41 return left
42
1class Solution {
2 // Calculates the maximum average of any subarray of length k
3 public double findMaxAverage(int[] nums, int k) {
4 double precision = 1e-5;
5 double lowerBound = 1e10;
6 double upperBound = -1e10;
7
8 // Finding the lowest and highest numbers in the input array nums
9 for (int num : nums) {
10 lowerBound = Math.min(lowerBound, num);
11 upperBound = Math.max(upperBound, num);
12 }
13
14 // Binary search to find the maximum average subarray within the precision range
15 while (upperBound - lowerBound >= precision) {
16 double mid = (lowerBound + upperBound) / 2;
17 if (canFindLargerAverage(nums, k, mid)) {
18 lowerBound = mid;
19 } else {
20 upperBound = mid;
21 }
22 }
23 // After the loop, lowerBound is the maximum average within specified precision
24 return lowerBound;
25 }
26
27 // Helper method to check if we can find an average larger than 'averageValue'
28 private boolean canFindLargerAverage(int[] nums, int k, double averageValue) {
29 double sum = 0;
30 // Calculate the initial sum of the first k elements reduced by the averageValue
31 for (int i = 0; i < k; ++i) {
32 sum += nums[i] - averageValue;
33 }
34
35 if (sum >= 0) {
36 return true;
37 }
38
39 double prevSum = 0; // Sum of the values from the start to i - k
40 double minPrevSum = 0; // Minimum sum encountered so far for prevSum
41
42 // Iterate through the rest of the elements
43 for (int i = k; i < nums.length; ++i) {
44 sum += nums[i] - averageValue; // Increment current sum
45 prevSum += nums[i - k] - averageValue; // Increment previous sum
46 minPrevSum = Math.min(minPrevSum, prevSum); // Update the minimum of previous sums
47
48 // If the current sum - the smallest sum of prevSum is non-negative,
49 // then there exists a subarray with an average ≥ averageValue
50 if (sum >= minPrevSum) {
51 return true;
52 }
53 }
54 return false;
55 }
56}
57
1class Solution {
2public:
3 // Function to find the maximum average subarray of size 'k' in the given vector 'nums'
4 double findMaxAverage(vector<int>& nums, int k) {
5 // Epsilon value to control the precision of our answer
6 double epsilon = 1e-5;
7
8 // Initialize left and right boundaries for binary search
9 double left = *min_element(nums.begin(), nums.end());
10 double right = *max_element(nums.begin(), nums.end());
11
12 // Lambda function to check if there's a subarray with average greater than 'value'
13 auto canFindLargerAverage = [&](double value) {
14 // Initial sum of the first 'k' elements with 'value' subtracted from each one
15 double sum = 0;
16 for (int i = 0; i < k; ++i) {
17 sum += nums[i] - value;
18 }
19 // If the sum is already non-negative, we can return true
20 if (sum >= 0) {
21 return true;
22 }
23 double sum_excluding_first_i_elements = 0, min_sum_excluding_first_i_elements = 0;
24 // Check the rest of the array to find if any subarray can have an average larger than 'value'
25 for (int i = k; i < nums.size(); ++i) {
26 sum += nums[i] - value; // Add the next element to 'sum'
27 sum_excluding_first_i_elements += nums[i - k] - value; // Update the sum excluding the first i elements
28 min_sum_excluding_first_i_elements = min(min_sum_excluding_first_i_elements, sum_excluding_first_i_elements); // Update the minimum sum
29
30 // If the sum is greater than or equal to the minimum sum, we can return true
31 if (sum >= min_sum_excluding_first_i_elements) {
32 return true;
33 }
34 }
35 // If no subarray has an average greater than 'value', return false
36 return false;
37 };
38
39 // Perform a binary search to find the maximum average within an epsilon range
40 while (right - left >= epsilon) {
41 // Calculate mid-point of the range
42 double mid = (left + right) / 2;
43 // Use the lambda function to check if we can find a larger average
44 if (canFindLargerAverage(mid)) {
45 left = mid;
46 } else {
47 right = mid;
48 }
49 }
50 // Return the maximum average which is on the 'left' due to the way we updated it in binary search
51 return left;
52 }
53};
54
1function findMaxAverage(nums: number[], k: number): number {
2 const epsilon = 1e-5; // This is the precision for the floating-point comparison.
3 let left = Math.min(...nums); // Initialize 'left' to the smallest element in the array.
4 let right = Math.max(...nums); // Initialize 'right' to the largest element in the array.
5
6 // 'check' function checks if there exists a subarray of length 'k' with average value greater than or equal to 'targetAverage'.
7 const check = (targetAverage: number): boolean => {
8 // Initial sum of the first 'k' elements adjusted by 'targetAverage'.
9 let sum = nums.slice(0, k).reduce((a, b) => a + b) - targetAverage * k;
10 if (sum >= 0) {
11 return true; // The average of the first 'k' elements is already greater than 'targetAverage'.
12 }
13
14 // 'totalSum' stores the sum of previous elements adjusted by 'targetAverage' in the sliding window.
15 // 'minPrevSum' stores the minimum sum encountered in the sliding window.
16 let totalSum = 0;
17 let minPrevSum = 0;
18
19 for (let i = k; i < nums.length; ++i) {
20 sum += nums[i] - targetAverage; // Include the next element into 'sum' while shifting the window.
21 totalSum += nums[i - k] - targetAverage; // Include the element that is exiting the window into 'totalSum'.
22
23 // Update 'minPrevSum' to the minimum sum we have seen so far.
24 minPrevSum = Math.min(minPrevSum, totalSum);
25
26 // If the current sum adjusted by the minimum previous sum we've seen is non-negative,
27 // it means there exists a subarray with an average at least 'targetAverage'.
28 if (sum >= minPrevSum) {
29 return true;
30 }
31 }
32 return false;
33 };
34
35 // Use binary search to find the maximum average to a precision of 'epsilon'.
36 while (right - left >= epsilon) {
37 const mid = (left + right) / 2; // Calculate the mid point between left and right.
38 if (check(mid)) {
39 left = mid; // If there exists a subarray with average greater than 'mid', move 'left' to 'mid'.
40 } else {
41 right = mid; // Otherwise, move 'right' to 'mid'.
42 }
43 }
44 // 'left' will contain the maximum average subarray value within the desired precision.
45 return left;
46}
47
Time and Space Complexity
Time Complexity
The time complexity of the findMaxAverage
function consists of two main parts: the binary search over the range of values and the sliding window check within the check
function.
-
The binary search runs in
O(log((max(nums) - min(nums)) / eps))
time because it narrows down the range[min(nums), max(nums)]
by half in each iteration until the range is smaller thaneps (= 1e-5)
. -
Within each iteration of the binary search, the
check
function is called once. Thecheck
function uses a sliding window approach that takesO(n)
time, wheren
is the length of thenums
list.
Combining these two parts, the overall time complexity of the function is O(n * log((max(nums) - min(nums)) / eps))
.
Space Complexity
The space complexity of the function is O(1)
.
- There are only a constant number of extra variables used (
s
,t
,mi
,eps
,l
,r
,mid
), and no additional space that scales with the input size is required.
Together, the function achieves linearithmic time complexity and constant space complexity.
Learn more about how to find time and space complexity quickly using problem constraints.
Which algorithm should you use to find a node that is close to the root of the tree?
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
LeetCode 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 we
Want a Structured Path to Master System Design Too? Don’t Miss This!