1. 程式人生 > >[LeetCode] Maximum Average Subarray II 子陣列的最大平均值之二

[LeetCode] Maximum Average Subarray II 子陣列的最大平均值之二

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. 1 <= k <= n <= 10,000.
  2. Elements of the given array will be in range [-10,000, 10,000].
  3. 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;
    }
};

類似題目:

參考資料: