# APCS實作題2024年6月第4題:最佳選擇 > 日期:2024年8月19日 > 作者:王一哲 > [ZeroJudge 題目連結](https://zerojudge.tw/ShowProblem?problemid=o079) ## 題目 ### 問題描述 給一個長度為 $n$ 的正整數序列 $a_1, a_2, \dots, a_n$,你可以執行多次操作 (包含0次),每次操作只能選擇這個序列的第一個或最後一個數字,再將這個數字從序列中刪除並自己搜集起來。 求滿足總和不超過 $k$ 且搜集的數字奇數和偶數個數相同的條件下,所能搜集的數字總和最大為多少。 <br /> ### 輸入說明 第一行輸入兩個正整數 $n, k~(1 \leq n \leq 3 \times 10^5, 1 \leq k \leq 10^9)$。 第二行有 $n$ 個正整數 $a_1, a_2, \dots, a_n ~(1 \leq a_i \leq 5 \times 10^3)$。 保證 $\forall i \in [1, n]$,前 $i$ 個數字的奇數和偶數數量差距不超過 $2 \times 10^3$。 子題配分 - 20分:$n = 10^3$ - 80分:無限制 <br /> ### 輸出格式 輸出滿足題目需求的最大值。 <br /> ### 範例輸入1 ``` 8 22 1 5 2 2 9 3 5 8 ``` ### 範例輸出1 ``` 16 ``` ### 範例輸入2 ``` 7 20 7 7 8 2 8 9 5 ``` ### 範例輸出2 ``` 0 ``` <br /><br /> ## Python 程式碼 ### 寫法1 費時最久約為 1.1 s,使用記憶體最多約為 49.4 MB,通過測試。 ```python= from collections import defaultdict n, k = map(int, input().split()) # 讀取數量 n,總和上限 k a = list(map(int, input().split())) # 讀取資料 a """ 處理前綴 """ psum = 0 # 前綴和 pdif = defaultdict(list) # 前綴部分,奇數數量 - 偶數數量對應的前綴和,如果 key 值不存在對應到空串列 dif = 0 # 從左到右,奇數數量 - 偶數數量 for i in range(n): # 從左到右掃過 a psum += a[i] # 前綴和第 i 位的前綴和 dif += 1 if a[i]&1 else -1 # 若 a[i] 為奇數 dif 加 1,反之減 1 pdif[dif].append(psum) # 將 psum 加入對應的 key 值資料 """ 處理特例 """ if k > psum: k = psum # 如果 k 大於全部的總和,將 k 更新為 psum while pdif[0] and pdif[0][-1] > k: pdif[0].pop() # 全部資料皆由左向右選,由最後一位開始,如果超出上限則移除此資料 ans = pdif[0][-1] if pdif[0] else 0 # 答案預設為全部資料皆由左向右選取的最大值或 0 """ 處理後綴 """ ssum = 0 # 後綴和 sdif = 0 # 從右到左,偶數數量 - 奇數數量 for i in range(n-1, 0, -1): # 依序檢查 a[n-1] ~ a[1] ssum += a[i] # 後綴和 sdif += 1 if a[i]&1 == 0 else -1 # 若 a[i] 為偶數 dif 加 1,反之減 1 if abs(sdif) > 2000: continue # 偶數數量 - 奇數數量大於 2000,超出題目範圍,找下一位 cand = pdif[sdif] # 由左到右可能的前綴和 if not cand: continue # 如果沒有對應的前綴和,找下一位 while cand and ssum + cand[-1] > k: cand.pop() # 從 cand 最後一位開始向左找,總和只會越來越小,結束時可能是空串列或符合條件的最大值 if cand: ans = max(ans, ssum + cand[-1]) # 更新答案 """ 結束 for 迴圈 """ print(ans) # 印出答案 ``` <br /> 如果在第11、12行之間加入 ```python if psum > k: continue ``` 刪除第15行 ```python while pdif[0] and pdif[0][-1] > k: pdif[0].pop() ``` 可以少存一些資料,費時最久約為 1.1 s,使用記憶體最多約為 49.1 MB,通過測試。 ```python= from collections import defaultdict n, k = map(int, input().split()) # 讀取數量 n,總和上限 k a = list(map(int, input().split())) # 讀取資料 a """ 處理前綴 """ psum = 0 # 前綴和 pdif = defaultdict(list) # 前綴部分,奇數數量 - 偶數數量對應的前綴和,如果 key 值不存在對應到空串列 dif = 0 # 從左到右,奇數數量 - 偶數數量 for i in range(n): # 從左到右掃過 a psum += a[i] # 前綴和第 i 位的前綴和 dif += 1 if a[i]&1 else -1 # 若 a[i] 為奇數 dif 加 1,反之減 1 if psum > k: continue # psum 超出上限,找下一位 pdif[dif].append(psum) # 將 psum 加入對應的 key 值資料 """ 處理特例 """ if k > psum: k = psum # 如果 k 大於全部的總和,將 k 更新為 psum ans = pdif[0][-1] if pdif[0] else 0 # 答案預設為全部資料皆由左向右選取的最大值或 0 """ 處理後綴 """ ssum = 0 # 後綴和 sdif = 0 # 從右到左,偶數數量 - 奇數數量 for i in range(n-1, 0, -1): # 依序檢查由右到左取 a[n-1] ~ a[1] ssum += a[i] # 後綴和 sdif += 1 if a[i]&1 == 0 else -1 # 若 a[i] 為偶數 dif 加 1,反之減 1 if abs(sdif) > 2000: continue # 偶數數量 - 奇數數量大於 2000,超出題目範圍,找下一位 cand = pdif[sdif] # 由左到右可能的前綴和 if not cand: continue # 如果沒有對應的前綴和,找下一位 while cand and ssum + cand[-1] > k: cand.pop() # 從 cand 最後一位開始向左找,總和只會越來越小,結束時可能是空串列或符合條件的最大值 if cand: ans = max(ans, ssum + cand[-1]) # 更新答案 """ 結束 for 迴圈 """ print(ans) # 印出答案 ``` <br /><br /> ### 寫法2,用二分搜尋法從 cand 中找符合條件的最大值 沒想到比寫法1還慢,費時最久約為 2.4 s,使用記憶體最多約為 49.1 MB,通過測試。 ```python= from collections import defaultdict from bisect import bisect_right n, k = map(int, input().split()) # 讀取數量 n,總和上限 k a = list(map(int, input().split())) # 讀取資料 a """ 處理前綴 """ psum = 0 # 前綴和 pdif = defaultdict(list) # 前綴部分,奇數數量 - 偶數數量對應的前綴和,如果 key 值不存在對應到空串列 dif = 0 # 從左到右,奇數數量 - 偶數數量 for i in range(n): # 從左到右掃過 a psum += a[i] # 前綴和第 i 位的前綴和 dif += 1 if a[i]&1 else -1 # 若 a[i] 為奇數 dif 加 1,反之減 1 if psum > k: continue # psum 超出上限,找下一位 pdif[dif].append(psum) # 將 psum 加入對應的 key 值資料 """ 處理特例 """ if k > psum: k = psum # 如果 k 大於全部的總和,將 k 更新為 psum ans = pdif[0][-1] if pdif[0] else 0 # 答案預設為全部資料皆由左向右選取的最大值或 0 """ 處理後綴 """ ssum = 0 # 後綴和 sdif = 0 # 從右到左,偶數數量 - 奇數數量 for i in range(n-1, 0, -1): # 依序檢查由右到左取 a[n-1] ~ a[1] ssum += a[i] # 後綴和 sdif += 1 if a[i]&1 == 0 else -1 # 若 a[i] 為偶數 dif 加 1,反之減 1 if abs(sdif) > 2000: continue # 偶數數量 - 奇數數量大於 2000,超出題目範圍,找下一位 cand = pdif[sdif] # 由左到右可能的前綴和 if not cand: continue # 如果沒有對應的前綴和,找下一位 low, high = 0, bisect_right(cand, k-ssum) while high > low: mid = (high - low) // 2 + low if ssum + cand[mid] > k: high = mid else: low = mid + 1# 從 cand 最後一位開始向左找,總和只會越來越小,結束時可能是空串列或符合條件的最大值 if low <= 0: continue ans = max(ans, ssum + cand[low-1]) # 更新答案 """ 結束 for 迴圈 """ print(ans) # 印出答案 ``` <br /><br /> ## C++ 程式碼 ### 寫法1,與上方的 Python 程式碼原理相同,用 unordered_map 儲存奇、偶數差對應的前綴和 第17筆測資超時。 ```cpp= #include <iostream> #include <unordered_map> #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> a (n, 0); // 儲存資料用的陣列 a /* 處理前綴 */ unordered_map<int, vector<int>> pdif; // 前綴部分,奇數數量 - 偶數數量對應的前綴和 int dif = 0, psum = 0; // dif 從左到右,奇數數量 - 偶數數量;psum 前綴和 for(int i=0; i<n; i++) { // 執行 n 次 cin >> a[i]; // 讀取資料 a psum += a[i]; // 第 i 位的前綴和 dif += a[i]&1 ? 1 : -1; // 若 a[i] 為奇數加 1,反之減 1 if (psum > k) continue; // psum 超出上限,找下一位 pdif[dif].push_back(psum); // 將 psum 加入對應的 key 值資料 } /* 處理特例 */ if (k > psum) k = psum; // 如果 k 大於全部的總和,將 k 更新為 psum int ans = !pdif[0].empty() ? pdif[0].back() : 0; // 答案預設為全部資料皆由左向右選取的最大值或 0 /* 處理後綴 */ int ssum = 0, sdif = 0; // 後綴和,從右到左偶數數量 - 奇數數量 for(int i=n-1; i>0; i--) { // 依序檢查 a[n-1] ~ a[1] ssum += a[i]; // 後綴和 sdif += a[i]&1 ? -1 : 1; // a[i] 為奇數減 1,反之加 1 if (abs(sdif) > 2000) continue; // 偶數數量 - 奇數數量大於 2000,超出題目範圍,找下一位 vector<int> cand = pdif[sdif]; // 由左到右可能的前綴和 if (cand.empty()) continue; // 如果沒有對應的前綴和,找下一位 while(!cand.empty() && ssum + cand.back() > k) cand.pop_back(); // 從 cand 最後一位開始向左找,總和只會越來越小,結束時可能是空陣列或符合條件的最大值 if (!cand.empty()) ans = max(ans, ssum + cand.back()); // 更新答案 } /* 結束 for 迴圈 */ cout << ans << "\n"; // 印出答案 return 0; } ``` <br /><br /> ### 寫法2,與寫法1原理相同,改用二維陣列儲存奇、偶數差對應的前綴和 還是會在第17筆測資超時。 ```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> a (n, 0); // 儲存資料用的陣列 a /* 處理前綴 */ int m = 2000; // 奇數數量 - 偶數數量最多差 2000 // 前綴部分,奇數數量 - 偶數數量對應的前綴和,奇數數量、偶數數量相等的資料位於索引值 2000,奇數多放前面,偶數多放後面 vector<vector<int>> pdif (m+m+1, vector<int> ()); pdif[m].push_back(0); // 於奇數數量、偶數數量相等處先放入 0 int dif = 0, psum = 0; // dif 從左到右,奇數數量 - 偶數數量;psum 前綴和 for(int i=0; i<n; i++) { // 執行 n 次 cin >> a[i]; // 讀取資料 a psum += a[i]; // 第 i 位的前綴和 dif += a[i]&1 ? 1 : -1; // 若 a[i] 為奇數加 1,反之減 1 if (psum > k) continue; // psum 超出上限,找下一位 pdif[m-dif].push_back(psum); // 將 psum 加入對應的索引值 } /* 處理特例 */ if (k > psum) k = psum; // 如果 k 大於全部的總和,將 k 更新為 psum int ans = !pdif[m].empty() ? pdif[m].back() : 0; // 答案預設為全部資料皆由左向右選取的最大值或 0 /* 處理後綴 */ int ssum = 0, sdif = 0; // 後綴和,從右到左偶數數量 - 奇數數量 for(int i=n-1; i>0; i--) { // 依序檢查 a[n-1] ~ a[1] ssum += a[i]; // 後綴和 sdif += a[i]&1 ? -1 : 1; // a[i] 為奇數減 1,反之加 1 if (abs(sdif) > 2000) continue; // 偶數數量 - 奇數數量大於 2000,超出題目範圍,找下一位 vector<int> cand = pdif[m-sdif]; // 由左到右可能的前綴和 if (cand.empty()) continue; // 如果沒有對應的前綴和,找下一位 while(!cand.empty() && ssum + cand.back() > k) cand.pop_back(); // 從 cand 最後一位開始向左找,總和只會越來越小,結束時可能是空陣列或符合條件的最大值 if (!cand.empty()) ans = max(ans, ssum + cand.back()); // 更新答案 } /* 結束 for 迴圈 */ cout << ans << "\n"; // 印出答案 return 0; } ``` <br /><br /> ### 寫法3,用二分搜尋法從 cand 中找符合條件的最大值 費時最久約為 0.3 s,使用記憶體最多約為 3.6 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<int> a (n, 0); // 儲存資料用的陣列 a /* 處理前綴 */ int m = 2000; // 奇數數量 - 偶數數量最多差 2000 // 前綴部分,奇數數量 - 偶數數量對應的前綴和,奇數數量、偶數數量相等的資料位於索引值 2000,奇數多放前面,偶數多放後面 vector<vector<int>> pdif (m+m+1, vector<int> ()); pdif[m].push_back(0); // 於奇數數量、偶數數量相等處先放入 0 int dif = 0, psum = 0; // dif 從左到右,奇數數量 - 偶數數量;psum 前綴和 for(int i=0; i<n; i++) { // 執行 n 次 cin >> a[i]; // 讀取資料 a psum += a[i]; // 第 i 位的前綴和 dif += a[i]&1 ? 1 : -1; // 若 a[i] 為奇數加 1,反之減 1 if (psum > k) continue; // 如果 psum 大於 k,超過上限,找下一個 pdif[m-dif].push_back(psum); // 將 psum 加入對應的索引值 } /* 處理特例 */ if (k > psum) k = psum; // 如果 k 大於全部的總和,將 k 更新為 psum //while(!pdif[m].empty() && pdif[m].back() > k) pdif[m].pop_back(); // 全部資料皆由左向右選,由最後一位開始,如果超出上限則移除此資料 int ans = !pdif[m].empty() ? pdif[m].back() : 0; // 答案預設為全部資料皆由左向右選取的最大值或 0 /* 處理後綴 */ int ssum = 0, sdif = 0; // 後綴和,從右到左偶數數量 - 奇數數量 for(int i=n-1; i>0; i--) { // 依序檢查 a[n-1] ~ a[1] ssum += a[i]; // 後綴和 sdif += a[i]&1 ? -1 : 1; // a[i] 為奇數減 1,反之加 1 if (abs(sdif) > 2000) continue; // 偶數數量 - 奇數數量大於 2000,超出題目範圍,找下一位 vector<int> cand = pdif[m-sdif]; // 由左到右可能的前綴和 if (cand.empty()) continue; // 如果沒有對應的前綴和,找下一位 int low = 0, high = upper_bound(cand.begin(), cand.end(), k-ssum) - cand.begin(); // 從 cand 找目標值 k-ssum 的索引值下限 low、上限 high while(high > low) { // 二分搜尋法 int mid = (high - low)/2 + low; // 中間值 int tmp = ssum + cand[mid]; // 總和 if (tmp > k) high = mid; // 如果 tmp 大於 k,向左找,降低 high else low = mid + 1; // 反之,向右找,提升 low } if (low <= 0) continue; // 沒有找到目標值 if (ssum + cand[low-1] <= k) ans = max(ans, ssum + cand[low-1]); // 更新答案 } /* 結束 for 迴圈 */ cout << ans << "\n"; // 印出答案 return 0; } ``` <br /><br /> --- ###### tags:`APCS`、`Python`、`C++`