# APCS實作題2023年10月第4題:投資遊戲 > 日期:2024年8月16日 > 作者:王一哲 > [ZeroJudge 題目連結](https://zerojudge.tw/ShowProblem?problemid=m373) ## 題目 ### 問題描述 你擁有一個長度為 $n$ 的陣列,代表每天的投資收益,以及 $k~(1 \leq k \leq 20)$ 張金牌。 你可以自行決定投資的開始和結束日期。在你選擇投資的每一天,你可以選擇消耗一張金牌來跳過當天,或者不使用金牌而拿取當天的收益。你的目標是找出如何投資,以實現最大的總收益。 請注意,你只能在投資期間進出一次。 <br /> ### 輸入說明 第一行包含兩個整數:$n$ 和 $k$,以空格分隔。$n$ 表示天數,$k$ 表示金牌數。 第二行包含 $n$ 個整數,以空格分隔,代表每天的投資收益。這些整數按照天數的順序給出,數值範圍為 $-10000$ 到 $10000$。 子題分數: - 20%:滿足 $k=0$,且 $1 \leq n \leq 2000$。 - 20%:滿足 $k=0$,且 $1 \leq n \leq 150000$。 - 60%:滿足 $1 \leq k \leq 20$,且 $1 \leq n \leq 150000$。 <br /> ### 輸出格式 請輸出一個整數,代表達到的最大收益。 <br /> ### 範例輸入1 ``` 9 0 3 1 -2 3 -2 3 -5 2 2 ``` ### 範例輸出1 ``` 6 ``` ### 範例輸入2 ``` 9 2 3 1 -2 3 -2 3 -5 2 2 ``` ### 範例輸出2 ``` 12 ``` ### 範例輸入3 ``` 9 4 3 1 -2 3 -2 3 -5 2 2 ``` ### 範例輸出3 ``` 14 ``` ### 範例輸入4 ``` 3 0 -1 -5 -3 ``` ### 範例輸出4 ``` 0 ``` <br /><br /> ## Python 程式碼 ### 寫法1 解題想法,開一個 $(n+1) \times (k+1)$ 的二維串列 dp,其中 $dp[i][j]$ 代表第 $i$ 天、使用 $k$ 面金牌的最大收益。在 for 迴圈之中,要先處理使用金牌的狀況,如果在第 $i$ 天使用 $j$ 面金牌,則 $dp[i][j]$ 會是前一天(第 $i-1$ 天)、使用 $j-1$ 面金牌的值,因此第8行寫成 ```python if j > 0: dp[i][j] = dp[i-1][j-1] ``` 但也有可能在第 $i$ 天不使用金牌,第 $i$ 天使用 $j$ 面金牌的損益,第 $i-1$ 天使用 $j$ 面金牌的損益加上當天的損益 $data[i]$,再與第8行處理過、使用金牌的損益比較取最大值,因此第9行寫成 ```python dp[i][j] = max(dp[i-1][j] + data[i], dp[i][j]) ``` 第9筆測資超時。 ```python= n, k = map(int, input().split()) # 讀取天數 n、金牌數量 k data = [0] # 每天的損益資料,為了配合讀取時的索引值,先放入第0天的資料0 data += list(map(int, input().split())) # 讀取每天的損益資料 dp = [[0]*(k+1) for _ in range(n+1)] # dp[i][j] 為第i天、使用j面金牌的最大收益 for i in range(1, n+1): # i = 1 ~ n for j in range(k+1): # j = 0 ~ k if j > 0: dp[i][j] = dp[i-1][j-1] # 先處理使用金牌的狀況 dp[i][j] = max(dp[i-1][j] + data[i], dp[i][j]) # 第一項為不使用金牌的損益,取兩項中較大者 ans = 0 # 答案預設為 0 for d in dp: ans = max(ans, *d) # 依序由 dp 讀取資料 d,再用 * 拆開與 ans 一起取最大值 print(ans) # 印出答案 ``` <br /> 雖然把第12行放在前一個 for 迴圈之中,少跑一次 for 迴圈,但仍然會在第9筆測資超時。 ```python= n, k = map(int, input().split()) # 讀取天數 n、金牌數量 k data = [0] # 每天的損益資料,為了配合讀取時的索引值,先放入第0天的資料0 data += list(map(int, input().split())) # 讀取每天的損益資料 dp = [[0]*(k+1) for _ in range(n+1)] # dp[i][j] 為第i天、使用j塊金牌的最大收益 ans = 0 # 答案預設為 0 for i in range(1, n+1): # i = 1 ~ n for j in range(k+1): # j = 0 ~ k if j > 0: dp[i][j] = dp[i-1][j-1] # 先處理使用金牌的狀況 dp[i][j] = max(dp[i-1][j] + data[i], dp[i][j]) # 第一項為不使用金牌的損益,取兩項中較大者 ans = max(ans, dp[i][-1]) # 因為 dp[i] 的最後一項是 dp[i] 的最大值,最取 ans 及 dp[i][-1] 之中較大者 print(ans) # 印出答案 ``` <br /><br /> ### 寫法2 吳邦一教授的寫法,利用滾動陣列節省時間及使用的記憶體。費時最久約為 1.6 s,使用記憶體最多約為 19.9 MB,通過測試。 ```python= n, k = map(int, input().split()) # 讀取天數 n、金牌數量 k val = list(map(int, input().split())) # 讀取每天的損益資料 d = [0]*(k+1) # 前一天使用 j 面金牌的最大損益 p = [0]*(k+1) # 今天使用 j 面金牌的最大損益 ans = 0 # 答案預設為 0 for v in val: # 由 val 依序讀取當天的損益 v p[0] = max(d[0], 0) + v # 先處理不用金牌的狀況,取前一天損益 d[0] 與 0 較大者,再加上 v for j in range(1, k+1): # j = 1 ~ k p[j] = max(d[j] + v, d[j-1]) # 第一項為不使用金牌、第二項為使用金牌的損益,取兩項中較大者 ans = max(ans, p[-1]) # 因為 p 的最後一項是 p 的最大值,最取 ans 及 p[-1] 之中較大者 d, p = p, d # 交換 d、p 的內容 print(ans) # 印出答案 ``` <br /><br /> ## C++ 程式碼 ### 寫法1 費時最久約為 44 ms,使用記憶體最多約為 13.9 MB,通過測試。 ```cpp= #include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { ios::sync_with_stdio(0); cin.tie(0); int n, k; cin >> n >> k; // 讀取天數 n、金牌數量 k vector<vector<int>> dp (n+1, vector<int> (k+1, 0)); // dp[i][j] 為第i天、使用j面金牌的最大收益 for(int i=1; i<=n; i++) { // i = 1 ~ n int v; cin >> v; // 讀取第i天的損益 for(int j=0; j<=k; j++) { // j = 0 ~ k if (j > 0) dp[i][j] = dp[i-1][j-1]; // 先處理使用金牌的狀況 dp[i][j] = max(dp[i-1][j] + v, dp[i][j]); // 第一項為不使用金牌的損益,取兩項中較大者 } } int ans = 0; // 答案預設為 0 for(auto d : dp) ans = max(ans, d.back()); // 取 ans 與 d.back() 之中的最大值 cout << ans << "\n"; // 印出答案 return 0; } ``` <br /> 如果將上方程式碼之中找 ans 的第18、19行移到前一個 for 迴圈之中,處理完 dp[i] 之後更新 ans,可以少跑一次 for 迴圈,花費的時間可以再少一點。費時最久約為 36 ms,使用記憶體最多約為 13.9 MB,通過測試。 ```cpp= #include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { ios::sync_with_stdio(0); cin.tie(0); int n, k; cin >> n >> k; // 讀取天數 n、金牌數量 k vector<vector<int>> dp (n+1, vector<int> (k+1, 0)); // dp[i][j] 為第i天、使用j塊金牌的最大收益 int ans = 0; // 答案預設為 0 for(int i=1; i<=n; i++) { // i = 1 ~ n int v; cin >> v; // 讀取第i天的損益 for(int j=0; j<=k; j++) { // j = 0 ~ k if (j > 0) dp[i][j] = dp[i-1][j-1]; // 先處理使用金牌的狀況 dp[i][j] = max(dp[i-1][j] + v, dp[i][j]); // 第一項為不使用金牌的損益,取兩項中較大者 } ans = max(ans, dp[i].back()); // 取 ans 與 dp[i].back() 之中的最大值 } cout << ans << "\n"; // 印出答案 return 0; } ``` <br /><br /> ### 寫法2 吳邦一教授的寫法,利用滾動陣列節省時間及使用的記憶體。費時最久約為 35 ms,使用記憶體最多約為 348 kB,通過測試。 ```cpp= #include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { ios::sync_with_stdio(0); cin.tie(0); int n, k; cin >> n >> k; // 讀取天數 n、金牌數量 k vector<int> d (k+1, 0), p (k+1, 0); // 前一天、今天使用 j 面金牌的最大收益 int ans = 0; // 答案預設為 0 for(int i=1; i<=n; i++) { // i = 1 ~ n int v; cin >> v; // 讀取第i天的損益 p[0] = max(d[0], 0) + v; // 處理不使用金牌的狀況,取 d[0] 及 0 較大者再加上 v,取 0 代表放棄之前的損益 for(int j=1; j<=k; j++) { // j = 1 ~ k p[j] = max(d[j] + v, d[j-1]); // 第一項為不使用金牌的損益,取兩項中較大者 } ans = max(ans, p.back()); swap(d, p); } cout << ans << "\n"; // 印出答案 return 0; } ``` <br /><br /> --- ###### tags:`APCS`、`Python`、`C++`