牛客網網易的校招編程題
題目:有 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 }