# APCS實作題2021年9月第4題:美食博覽會 > 日期:2024年8月23日 > 作者:王一哲 > [ZeroJudge 題目連結](https://zerojudge.tw/ShowProblem?problemid=g278) > [ZeroJudge 題目連結(時間放寬版)](https://zerojudge.tw/ShowProblem?problemid=h117) ## 題目 ### 問題描述 在一個美食博覽會上,有 $n$ 個攤位在販售美食,已知每個攤位只會販售一種美食,且他們販售的美食依序是 $a_1, a_2, \dots, a_n$,其中可能會有某些攤位販售相同種類的美食。 國王及大臣們總共 $k$ 人要依序品嚐所有美食,已知每位品嚐員會選擇一段連續的攤位進行試吃,而每個人都不想要試吃到同一種自己曾經吃過的美食,因此一位品嚐員所選到的範圍不能有同一種美食重複出現。另外,品嚐員們都不喜歡被別人打擾用餐,所以任意兩個品嚐員所選到的連續區間必須是沒有重疊的。 給你 $n, k$,以及這 $n$ 個攤位分別販售的美食編號,請計算出這些試吃員們總共最多可以吃到幾攤的美食? <br /> ### 輸入說明 第一行輸入兩個正整數 $n, k~(1 \leq n \leq 10^6,~ 1 \leq k \leq 20,~ 1 \leq n \times k \leq 5 \times 10^6)$,代表有 $n$ 個攤位和 $k$ 個試吃員。 接下來有 $n$ 個數字代表每個攤位各別賣哪一種美食,$a_1, a_2, \dots, a_n~ (1 \leq a_i \leq 10^5)$。 配分 - 50%:$k = 1$ - 50%:無其它限制 <br /> ### 輸出說明 輸出 $k$ 個試吃員總共最多可以吃到幾個攤位。 <br /> ### 範例輸入1 ``` 5 1 1 2 1 3 1 ``` ### 範例輸出1 ``` 3 ``` <br /> ### 範例輸入2 ``` 10 3 1 7 1 3 1 4 4 2 7 4 ``` ### 範例輸出2 ``` 8 ``` <br /> ## Python 程式碼 從吳邦一教授的 APCS 解題影片〈[PyAp45 Python APCS 題解:美食博覽會(AP202109_4), ZeroJudge 編號 g278.](https://youtu.be/6ON6VyRTV2Q?si=3HsDn8K-snt81gZh)〉中看到的寫法。在原版題目下,第10筆測資開始超時。在時限放寬版,第2筆測資開始超時。 ```python= n, k = map(int, input().split()) # 讀取攤位數量 n,人數 k stall = [0] + list(map(int, input().split())) # 讀取攤位資料,最左側加上 0 left = [0]*(n+1) # left[i] 為第 i 個攤位為終點,最左側的起點,[left[i], i] 區間沒有重複的食物 last = [0]*100001 # last[x] 為第 x 種食物最後一次出現的位置,題目限制最多有 100000 種食物 """ 第1部分:用滑動視窗找不重複的區間 每次由 i 向右滑到 i+1,如果 stall[i+1] 不在 [left[i], i] 區間內,left 不同,即 left[i+1] = left[i];如果 stall[i+1] 在 [left[i], i] 區間內,移動 left,即 left[i+1] = last[stall[i+1]] + 1; 取 left[i] 及 last[stall[i+1]]+1 較右側者,即 left[i+1] = max(left[i], last[stall[i+1]]+1) """ for i in range(1, n+1): # 掃過第 1 ~ n 個攤位 left[i] = max(left[i-1], last[stall[i]]+1) # 更新 left last[stall[i]] = i # 更新 last """ 第2部分:用滾動陣列找 k 個人 f(j, i) 是在第 i 個攤位前選 j 個區間的最長總和,j 最後要等於 k 1. 沒有取任何區間 j = 0 或是沒有任何攤位 i = 0,f(j, i) = 0 2. 第 i 個攤位不在解之中,f(j, i) = f(j, i-1) 3. 第 i 個攤位在解之中,最後一個區間是以 i 為終點,向左端越遠越好,即 left[i] 左側要取 j-1 個區間,遞迴式為 f(j, i) = f(j-1, left[i]-1) + (i - left[i] + 1) ,第二項為 [left[i], i] 區間長度 """ p = [0]*(n+1) # 滾動陣列,現在的解 d = [0]*(n+1) # 滾動陣列,前一個解 for j in range(k): # k 個人,執行 k 次 for i in range(1, n+1): # 掃過第 1 ~ n 個攤位 p[i] = max(p[i-1], d[left[i]-1]+i-left[i]+1) # 第一項為第 i 個攤位不在解之中,第二項為第 i 個攤位在解之中 p, d = d, p # 交換 print(d[n]) # 印出答案 ``` <br /><br /> ## C++ 程式碼 在原版題目下,費時最久約為 0.1 s,使用記憶體最多約為 16 MB,通過測試。在時限放寬版,費時最久約為 0.2 s,使用記憶體最多約為 16 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> stall (n+1, 0); for(int i=1; i<=n; i++) cin >> stall[i]; // 讀取攤位資料,最左側加上 0 vector<int> left (n+1, 0); // left[i] 為第 i 個攤位為終點,最左側的起點,[left[i], i] 區間沒有重複的食物 vector<int> last (100001, 0); // last[x] 為第 x 種食物最後一次出現的位置,題目限制最多有 100000 種食物 /* 第1部分:用滑動視窗找不重複的區間 * 每次由 i 向右滑到 i+1,如果 stall[i+1] 不在 [left[i], i] 區間內,left 不同,即 * left[i+1] = left[i];如果 stall[i+1] 在 [left[i], i] 區間內,移動 left,即 left[i+1] = last[stall[i+1]] + 1; * 取 left[i] 及 last[stall[i+1]]+1 較右側者,即 left[i+1] = max(left[i], last[stall[i+1]]+1) */ for(int i=1; i<=n; i++) { // 掃過第 1 ~ n 個攤位 left[i] = max(left[i-1], last[stall[i]]+1); // 更新 left last[stall[i]] = i; // 更新 last } /* 第2部分:用滾動陣列找 k 個人 * f(j, i) 是在第 i 個攤位前選 j 個區間的最長總和,j 最後要等於 k * 1. 沒有取任何區間 j = 0 或是沒有任何攤位 i = 0,f(j, i) = 0 * 2. 第 i 個攤位不在解之中,f(j, i) = f(j, i-1) * 3. 第 i 個攤位在解之中,最後一個區間是以 i 為終點,向左端越遠越好,即 left[i] 左側要取 j-1 個區間,遞迴式為 f(j, i) = f(j-1, left[i]-1) + (i - left[i] + 1) ,第二項為 [left[i], i] 區間長度 */ vector<int> p (n+1, 0), d (n+1, 0); // 滾動陣列,現在的解,前一個解 for(int j=0; j<k; j++) { // k 個人,執行 k 次 for(int i=1; i<=n; i++) // 掃過第 1 ~ n 個攤位 p[i] = max(p[i-1], d[left[i]-1]+i-left[i]+1); // 第一項為第 i 個攤位不在解之中,第二項為第 i 個攤位在解之中 swap(p, d); // 交換 } cout << d[n] << "\n"; // 印出答案 return 0; } ``` <br /><br /> --- ###### tags:`APCS`、`Python`、`C++`