Given an array consisting of n
integers, find the contiguous subarray whose length is greater than or equal to k
that has the maximum average value. And you need to output the maximum average value.
Example 1:
Input: [1,12,-5,-6,50,3], k = 4 Output: 12.75 Explanation: when length is 5, maximum average value is 10.8, when length is 6, maximum average value is 9.16667. Thus return 12.75.
Note:
- 1 <=
k
<=n
<= 10,000. - Elements of the given array will be in range [-10,000, 10,000].
- The answer with the calculation error less than 10-5 will be accepted.
這道題是之前那道 Maximum Average Subarray I 的拓展,那道題說是要找長度為k的子數組的最大平均值,而這道題要找長度大於等於k的子數組的最大平均值。加了個大於k的條件,情況就復雜很多了,之前只要遍歷所有長度為k的子數組就行了,現在還要包括所有長度大於k的子數組。我們首先來看 brute force 的方法,就是遍歷所有的長度大於等於k的子數組,並計算平均值並更新結 果res。那么先建立累加和數組 sums,結果 res 初始化為前k個數字的平均值,然后讓i從 k+1 個數字開始遍歷,此時的 sums[i] 就是前 k+1 個數組組成的子數組之和,我們用其平均數來更新結果 res,然后從開頭開始去掉數字,直到子數組剩余k個數字為止,再用其平均值來更新解結果 res,通過這種方法,我們就遍歷了所有長度大於等於k的子數組。這里需要注意的一點是,更新結果 res 的步驟不能寫成 res = min(res, t / (i + 1)) 這種形式,會 TLE,必須要在if中判斷 t > res * (i + 1) 才能 accept,寫成 t / (i + 1) > res 也不行,必須要用乘法,這也說明了計算機不喜歡算除法吧,參見代碼如下:
解法一:
class Solution { public: double findMaxAverage(vector<int>& nums, int k) { int n = nums.size(); vector<int> sums = nums; for (int i = 1; i < n; ++i) { sums[i] = sums[i - 1] + nums[i]; } double res = (double)sums[k - 1] / k; for (int i = k; i < n; ++i) { double t = sums[i]; if (t > res * (i + 1)) res = t / (i + 1); for (int j = i - k; j >= 0; --j) { t = sums[i] - sums[j]; if (t > res * (i - j)) res = t / (i - j); } } return res; } };
我們再來看一種 O(n2) 時間復雜度的方法,這里對上面的解法進行了空間上的優化,並沒有長度為n數組,而是使用了 preSum 和 sum 兩個變量來代替,preSum 初始化為前k個數字之和,sum 初始化為 preSum,結果 res 初始化為前k個數字的平均值,然后從第 k+1 個數字開始遍歷,首先 preSum 加上這個數字,sum 更新為 preSum,然后用當前 k+1 個數字的平均值來更新結果 res。和上面的方法一樣,我們還是要從開頭開始去掉數字,直到子數組剩余k個數字為止,然后用其平均值來更新解結果 res,那么每次就用 sum 減去 nums[j],就可以不斷的縮小子數組的長度了,用當前平均值更新結果 res,注意還是要用乘法來判斷大小,參見代碼如下:
解法二:
class Solution { public: double findMaxAverage(vector<int>& nums, int k) { double preSum = accumulate(nums.begin(), nums.begin() + k, 0); double sum = preSum, res = preSum / k; for (int i = k; i < nums.size(); ++i) { preSum += nums[i]; sum = preSum; if (sum > res * (i + 1)) res = sum / (i + 1); for (int j = 0; j <= i - k; ++j) { sum -= nums[j]; if (sum > res * (i - j)) res = sum / (i - j); } } return res; } };
下面來看一種優化時間復雜度到 O(nlg(max - min)) 的解法,其中 max 和 min 分別是數組中的最大值和最小值,是利用了二分搜索法,博主之前寫了一篇 LeetCode Binary Search Summary 二分搜索法小結 的博客,這里的二分法應該是小結的第四類,也是最難的那一類,因為判斷折半的方向是一個子函數,這里我們沒有用子函數,而是寫到了一起,可以抽出來成為一個子函數,這一類的特點就是不再是簡單的大小比較,而是需要一些復雜的操作來確定折半方向。這里主要借鑒了蔡文森特大神的帖子,所求的最大平均值一定是介於原數組的最大值和最小值之間,所以我們的目標是用二分法來快速的在這個范圍內找到要求的最大平均值,初始化 left 為原數組的最小值,right 為原數組的最大值,然后 mid 就是 left 和 right 的中間值,難點就在於如何得到 mid 和要求的最大平均值之間的大小關系,從而判斷折半方向。我們想,如果已經算出來了這個最大平均值 maxAvg,那么對於任意一個長度大於等於k的數組,如果讓每個數字都減去 maxAvg,那么得到的累加差值一定是小於等於0的,這個不難理解,比如下面這個數組:
[1, 2, 3, 4] k = 2
我們一眼就可以看出來最大平均值 maxAvg = 3.5,所以任何一個長度大於等於2的子數組每個數字都減去 maxAvg 的差值累加起來都小於等於0,只有產生這個最大平均值的子數組 [3, 4],算出來才正好等於0,其他都是小於0的。那么可以根據這個特點來確定折半方向,我們通過 left 和 right 值算出來的 mid,可以看作是 maxAvg 的一個 candidate,所以就讓數組中的每一個數字都減去 mid,然后算差值的累加和,一旦發現累加和大於0了,那么說明 mid 比 maxAvg 小,這樣就可以判斷方向了。
我們建立一個累加和數組 sums,然后求出原數組中最小值賦給 left,最大值賦給 right,題目中說了誤差是 1e-5,所以循環條件就是 right 比 left 大 1e-5,然后算出來 mid,定義一個 minSum 初始化為0,布爾型變量 check,初始化為 false。然后開始遍歷數組,先更新累加和數組 sums,注意這個累加和數組不是原始數字的累加,而是它們和 mid 相減的差值累加。我們的目標是找長度大於等於k的子數組的平均值大於 mid,由於每個數組都減去了 mid,那么就轉換為找長度大於等於k的子數組的差累積值大於0。建立差值累加數組的意義就在於通過 sums[i] - sums[j] 來快速算出j和i位置中間數字之和,那么只要j和i中間正好差k個數字即可,然后 minSum 就是用來保存j位置之前的子數組差累積的最小值,所以當 i >= k 時,我們用 sums[i - k] 來更新 minSum,這里的 i - k 就是j的位置,然后判斷如果 sums[i] - minSum > 0了,說明找到了一段長度大於等k的子數組平均值大於 mid 了,就可以更新 left 為 mid 了,我們標記 check 為 true,並退出循環。在 for 循環外面,當 check 為 true 的時候,left 更新為 mid,否則 right 更新為 mid,參見代碼如下:
解法三:
class Solution { public: double findMaxAverage(vector<int>& nums, int k) { int n = nums.size(); vector<double> sums(n + 1, 0); double left = *min_element(nums.begin(), nums.end()); double right = *max_element(nums.begin(), nums.end()); while (right - left > 1e-5) { double minSum = 0, mid = left + (right - left) / 2; bool check = false; for (int i = 1; i <= n; ++i) { sums[i] = sums[i - 1] + nums[i - 1] - mid; if (i >= k) { minSum = min(minSum, sums[i - k]); } if (i >= k && sums[i] > minSum) {check = true; break;} } if (check) left = mid; else right = mid; } return left; } };
下面這種解法對上面的方法優化了空間復雜度 ,使用 preSum 和 sum 來代替數組,思路和上面完全一樣,可以參加上面的講解,注意這里我們的第二個if中是判斷 i >= k - 1,而上面的方法是判斷 i >= k,這是因為上面的 sums 數組初始化了 n + 1 個元素,注意坐標的轉換,而第一個 if 中 i >= k 不變是因為j和i之間就差了k個,所以不需要考慮坐標的轉換,參見代碼如下:
解法四:
class Solution { public: double findMaxAverage(vector<int>& nums, int k) { double left = *min_element(nums.begin(), nums.end()); double right = *max_element(nums.begin(), nums.end()); while (right - left > 1e-5) { double minSum = 0, sum = 0, preSum = 0, mid = left + (right - left) / 2; bool check = false; for (int i = 0; i < nums.size(); ++i) { sum += nums[i] - mid; if (i >= k) { preSum += nums[i - k] - mid; minSum = min(minSum, preSum); } if (i >= k - 1 && sum > minSum) {check = true; break;} } if (check) left = mid; else right = mid; } return left; } };
Github 同步地址:
https://github.com/grandyang/leetcode/issues/644
類似題目:
參考資料:
https://leetcode.com/problems/maximum-average-subarray-ii/
https://leetcode.com/problems/maximum-average-subarray-ii/discuss/105498/c-binary-search-130ms