動態規划:合唱團問題解析(一)


牛客網網易的校招編程題

題目:有 n 個學生站成一排,每個學生有一個能力值,牛牛想從這 n 個學生中按照順序選取 k 名學生,要求相鄰兩個學生的位置編號的差不超過 d,使得這 k 個學生的能力值的乘積最大,你能返回最大的乘積嗎?
輸入:每個輸入包含 1 個測試用例。每個測試數據的第一行包含一個整數 n (1 <= n <= 50),表示學生的個數,接下來的一行,包含 n 個整數,按順序表示每個學生的能力值 ai(-50 <= ai <= 50)。接下來的一行包含兩個整數,k 和 d (1 <= k <= 10, 1 <= d <= 50)。
輸出:輸出一行表示最大的乘積。

因為本人剛學動態規划,所以我先把問題簡化后先用遞歸方式求解,再改進為記憶化搜索,然后用動態規划解決問題,最后求解原問題。
簡化后的問題:從 n 個自然數中選取 k 個數,使得這 k 個數的乘積最大。

遞歸求解

先嘗試用遞歸的方式自上而下的解決,定義狀態函數為 F(start, k),start 為自然數數組索引的起點,k 為要取的數的數量,返回從 start 到數組結束位置中取得的 k 個數的乘積的最大值。假設自然數組為 (X0, X1 ,X2, …, Xn-1) 共 n 個數,我們最終要求的就是 F(0, k)。假設我們選取其中一個數為必選的數,可以得出如下的遞歸樹去解釋該問題。

 

 

遞歸的終止條件為當 start >= n 的時候,這時 start 索引已經越界,所以直接返回數字 1 乘以被選取的數字,就相當於返回數組最后一個數字 Xn-1; 同時,當最后 k <= 0 的時候,說明這時無可選取的數字,也就是返回數字 1。基於以上條件,當存在選取超過一定范圍內的 k 個數時,會返回范圍內所有數字的乘積。實現的代碼如下所示:

 1 long long recursive(int a[], int index, int n, int k) {
 2     if (k <= 0 || index >= n)
 3         return 1;
 4 
 5     long long result = 0;
 6     for (int i=index; i < n; i++)
 7          result = max(result, a[i] * recursive(a, i+1, n, k-1));
 8     return result;
 9 }
10 
11 long long result(int a[], int n, int k){
12     return recursive(a, 0, n, k);
13 }

記憶化搜索

因為遞歸在處理更大規模的數據時運行效率是很低的,存在大量的重復運算,所以我們可以用記憶化搜索的方式去優化遞歸的方法。因為每個狀態依賴於兩個變量的變化,所以需要一個二維的數組去存儲已經計算過的值。實現的代碼如下所示:

 1 vector<vector<long long>> memo;
 2 long long memoSearch(int a[], int index, int n, int k) {
 3     if (k <= 0 || index >= n)
 4         return 1;
 5     if (memo[index][k] != -1)
 6         return memo[index][k];
 7 
 8     long long result = 0;
 9     for (int i = index; i < n; i++)
10         result = max(result, a[i] * memoSearch(a, i+1, n, k-1));
11     memo[index][k] = result;
12     return result;
13 }
14  
15 long long result(int a[], int n, int k){
16     memo = vector<vector<long long>>(n, vector<long long>(k+1, -1));
17     return memoSearch(a, 0, n, k);
18 }

動態規划

通過上面遞歸的分析,我們知道該問題是要求一個最優的解,當自頂向下的分析問題時,我們發現該問題是存在最優子問題的,同時這些子問題可能被重復的計算,所以我們可以用動態規划的方法去自底向上的解決問題,提高計算效率。

我們從最基本的一個子問題 F(start, 1) 開始分析,F(start, 1)=max(Xstart*F(1, 0), Xstart+1*F(2, 0), …, Xn-1*F(n, 0))。因為假設 k=0 時返回數字 1,所以可得 F(start, 1)=max(Xstart, Xstart+1, …, Xn-1)。所以當 k = 1 時,我們保留原來所有數組的值,而當 k = 2 時,從頭遍歷數組,在位置 (start, 2) 上存儲 Xstart*F(start+1, 1)。所以在編程實現時需要三個 for 循環,第一重循環以 k 計數,第二重以自然數組下標 n 計數,第三重循環取該下標 n 后被存儲的數,循環內計算該下標的自然數與存儲的數的最大值的積。當計算完最后一列 k 時,最后一列 k 中的最大值就是我們要求的問題的最優解。實現的代碼如下所示:

 1 vector<vector<long long>> memo;
 2 long long dpAlgorithm(int a[], int n, int k) {
 3     memo = vector<vector<long long>>(n, vector<long long>(k+1, -1));
 4     long long result = 0;
 5     for (int j = 1; j < k+1; j++) {
 6         for (int i = 0; i < n; i++) {
 7             if (j == 1) {
 8                 memo[i][j] = a[i];
 9             }
10             else {
11                 long long temp = 0;
12                 for (int index = i + 1; index < n; index++) {
13                     temp = max(temp, memo[index][j - 1]);
14                     memo[i][j] = temp * a[i];
15                 }
16             }
17             if (j == k) result = max(result, memo[i][j]);
18         }
19     }
20     return result;
21 }

動規算法優化

要實現前面所述的動態規划算法,我們需要一個 n * (k + 1) 的二維矩陣去存儲已經計算出的最優值,當 n 和 k 的值很大的時候,就需要更多額外的空間去求解。而實際上我們可以進一步的優化這個空間復雜度。因為在這個問題中,當我們選取一個固定的值時,我們是從其之后存儲的最大值與該固定的值相乘,所以之前被存儲的值事實上是可以被覆蓋的。所以我們只需要一個一維的長度為n的矩陣去實現該算法。實現的代碼如下所示:

 1 vector<vector<long long>> memo;
 2 long long dpAlgorithm2(int a[], int n, int k) {
 3     vector<long long> memo2 = vector<long long>(n, -1);
 4     long long result = 0;
 5     for (int j = 1; j < k + 1; j++) {
 6         for (int i = 0; i < n; i++) {
 7             if (j == 1) {
 8                 memo2[i] = a[i];
 9             }
10             else {
11                 for (int index = i + 1; index < n; index++) {
12                     memo2[i] = max(memo2[i], a[i] * memo2[index]);
13                 }
14             }
15             if (j == k) result = max(result, memo2[i]);
16         }
17     }
18     return result;
19 }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM