1. 程式人生 > >[LeetCode] Largest Sum of Averages 最大的平均數之和

[LeetCode] Largest Sum of Averages 最大的平均數之和

We partition a row of numbers A into at most K adjacent (non-empty) groups, then our score is the sum of the average of each group. What is the largest score we can achieve?

Note that our partition must use every number in A, and that scores are not necessarily integers.

Example:
Input: 
A = [9,1,2,3,9]
K = 3
Output: 20
Explanation: 
The best choice is to partition A into [9], [1, 2, 3], [9]. The answer is 9 + (1 + 2 + 3) / 3 + 9 = 20.
We could have also partitioned A into [9, 1], [2], [3, 9], for example.
That partition would lead to a score of 5 + 2 + 6 = 13, which is worse.

Note:

  • 1 <= A.length <= 100.
  • 1 <= A[i] <= 10000.
  • 1 <= K <= A.length.
  • Answers within 10^-6 of the correct answer will be accepted as correct.

這道題給了我們一個數組,說是讓我們將陣列分成至多K個非空組,然後說需要統計的分數是各組的平均數之和,讓我們求一個分割方法,使得這個分數值最大,當然這個分數值不一定是整型數。這道題限制了分割的組必須為非空組,那麼就是說K值要小於等於陣列的元素個數。但是實際上博主感覺這個必須為非空的限制有沒有都一樣,因為題目中說至多分成K組,也就是說可以根本不分組,那麼比如你輸入個A=[9,1], K=3,照樣返回一個10,給人的感覺好像是分成了[9], [1], [] 這三組一樣,但其實只是分成了兩組[9] 和 [1]。但我們不必糾結這些,不是重點。沒有啥思路的情況下我們就先想想brute force的解法唄,對於題目中給的那個例子,我們用最暴力的方法就是遍歷所有的可能性,即遍歷所有分割成三個組的情況,用三個for迴圈。貌似行的通,但問題來了,如果K大於3呢,每大一個,多加一個for迴圈麼,總共K個for迴圈?如果K=100呢,100個for迴圈麼?畫面太美我不敢看!顯然這道題用brute force是行不通的,那麼換個方法唄!像這種求極值的題,又是玩陣列的題,根據老夫行走江湖多年的經驗,十有八九都是用Dynamic Programming來做的。玩子陣列且跟極值有關的題天然適合用DP來做,想想為什麼?DP的本質是什麼,不就是狀態轉移方程,根據前面的狀態來更新當前的狀態。而子陣列不就是整個陣列的前一個狀態,不停的更新的使得我們最終能得到極值。

好,下面進入正題。DP走起,首先來考慮dp陣列的定義,我們如何定義dp陣列有時候很關鍵,定義的不好,那麼就無法寫出正確的狀態轉移方程。對於這道題,我們很容易直接用一個一維陣列dp,其中dp[i]表示範圍為[0, i]的子陣列分成三組能得到的最大分數。用這樣定義的dp陣列的話,狀態轉移方程將會非常難寫,因為我們忽略了一個重要的資訊,即K。dp陣列不把K加進去的話就不知道當前要分幾組,這個Hidden Information是解題的關鍵。這是DP中比較難的一類,有些DP題的隱藏資訊藏的更深,不挖出來就無法解題。這道題的dp陣列應該是個二維陣列,其中dp[i][k]表示範圍是[i, n-1]的子陣列分成k組的最大得分。那麼這裡你就會納悶了,為啥範圍是[i, n-1]而不是[0, i],為啥要取後半段呢,不著急,聽博主慢慢道來。由於把[i, n-1]範圍內的子陣列分成k組,那麼suppose我們已經知道了任意範圍內分成k-1組的最大分數,這是此型別題目的破題關鍵所在,要求狀態k,一定要先求出所有的狀態k-1,那麼問題就轉換成了從k-1組變成k組,即多分出一組,那麼在範圍[i, n-1]多分出一組,實際上就是將其分成兩部分,一部分是一組,另一部分是k-1組,怎麼分,就用一個變數j,遍歷範圍(i, n-1)中的每一個位置,那麼分成的這兩部分的分數如何計算呢?第一部分[i, j),由於是一組,那麼直接求出平均值即可,另一部分由於是k-1組,由於我們已經知道了所有k-1的情況,可以直接從cache中讀出來dp[j][k-1],二者相加即可 avg(i, j) + dp[j][k-1],所以我們可以得出狀態轉移方程如下:

dp[i][k] = max(avg(i, n) + max_{j > i} (avg(i, j) + dp[j][k-1]))

這裡的avg(i, n)是其可能出現的情況,由於是至多分為k組,所以我們可以不分組,所以直接計算範圍[i, n-1]內的平均值,然後用j來遍歷區間(i, n-1)中的每一個位置,最終得到的dp[i][k]就即為所求。注意這裡我們通過建立累加和陣列sums來快速計算某個區間之和。博主覺得這道題十分的經典,考察點非常的多,很具有代表性,標為Hard都不過分,前面提到了dp[i][k]表示的是範圍[i, n-1]的子陣列分成k組的最大得分,現在想想貌似定義為[0, i]範圍內的子陣列分成k組的最大得分應該也是可以的,那麼此時j就是遍歷(0, i)中的每個位置了,好像也沒什麼不妥的地方,有興趣的童鞋可以嘗試的寫一下~

解法一:

class Solution {
public:
    double largestSumOfAverages(vector<int>& A, int K) {
        int n = A.size();
        vector<double> sums(n + 1);
        vector<vector<double>> dp(n, vector<double>(K));
        for (int i = 0; i < n; ++i) {
            sums[i + 1] = sums[i] + A[i];
        }
        for (int i = 0; i < n; ++i) {
            dp[i][0] = (sums[n] - sums[i]) / (n - i);
        }    
        for (int k = 1; k < K; ++k) {
            for (int i = 0; i < n - 1; ++i) {
                for (int j = i + 1; j < n; ++j) {
                    dp[i][k] = max(dp[i][k], (sums[j] - sums[i]) / (j - i) + dp[j][k - 1]);
                }
            }
        }
        return dp[0][K - 1];
    }
};

我們可以對空間進行優化,由於每次的狀態k,只跟前一個狀態k-1有關,所以我們不需要將所有的狀態都儲存起來,只需要儲存前一個狀態的值就行了,那麼我們就用一個一維陣列就可以了,不斷的進行覆蓋,從而達到了節省空間的目的,參見程式碼如下:

解法二:

class Solution {
public:
    double largestSumOfAverages(vector<int>& A, int K) {
        int n = A.size();
        vector<double> sums(n + 1);
        vector<double> dp(n);
        for (int i = 0; i < n; ++i) {
            sums[i + 1] = sums[i] + A[i];
        }
        for (int i = 0; i < n; ++i) {
            dp[i] = (sums[n] - sums[i]) / (n - i);
        }    
        for (int k = 1; k < K; ++k) {
            for (int i = 0; i < n - 1; ++i) {
                for (int j = i + 1; j < n; ++j) {
                    dp[i] = max(dp[i], (sums[j] - sums[i]) / (j - i) + dp[j]);
                }
            }
        }
        return dp[0];
    }
};

我們也可以是用遞迴加記憶陣列的方式來實現,記憶陣列的運作原理和DP十分類似,也是一種cache,將已經計算過的結果儲存起來,用的時候直接取即可,避免了大量的重複計算,參見程式碼如下:

解法三:

class Solution {
public:
    double largestSumOfAverages(vector<int>& A, int K) {
        int n = A.size();
        vector<vector<double>> memo(101, vector<double>(101));
        double cur = 0;
        for (int i = 0; i < n; ++i) {
            cur += A[i];
            memo[i + 1][1] = cur / (i + 1);
        }
        return helper(A, K, n, memo);
    }
    double helper(vector<int>& A, int k, int j, vector<vector<double>>& memo) {
        if (memo[j][k] > 0) return memo[j][k];
        double cur = 0;
        for (int i = j - 1; i > 0; --i) {
            cur += A[i];
            memo[j][k] = max(memo[j][k], helper(A, k - 1, i, memo) + cur / (j - i));
        }
        return memo[j][k];
    }
};

參考資料: