# 0-20 set 講義不是我寫的,網址在此 https://emanlaicepsa.github.io/2020/10/21/0-index/ 我只是將自己寫的練習題程式碼記錄下來。 最後更新日期:2024年10月10日 ## [TIOJ: 1911. 雲端列印](https://tioj.ck.tp.edu.tw/problems/1911) ### Python 程式碼 這個題目輸入的工作優先權有重複的數字,如果用解題 Python 不能使用 set,但如果用 list 需要在每次加入資料時再 sort 一次,會拖慢執行的時間,在第24、25、26、27、29筆測資會超時。 ```python= data = list(map(int, input().split())) task = [] ans = [] for d in data: if d == 0: break if d > 0: task.append(d) task.sort() else: if not task: continue if d == -1: ans.append(task.pop(0)) elif d == -2: ans.append(task.pop()) print(*ans) ``` 比較好的作法應該是用 [bisect](https://docs.python.org/3/library/bisect.html) 函式庫中的 insort_left,將新的資料 d 插入串列 task 並保持資料由小到大排序,這樣的寫法效率較好,只剩下第25、29筆測資會超時。 ```python= from bisect import insort_left data = list(map(int, input().split())) task = [] ans = [] for d in data[:-1]: if d == 0: break if d > 0: insort_left(task, d) else: if not task: continue if d == -1: ans.append(task.pop(0)) elif d == -2: ans.append(task.pop()) print(*ans) ``` 如果想要在 Python 使用附帶排序功能的容器,需要另外安裝 [sortedcontainers](https://grantjenks.com/docs/sortedcontainers/) 函式庫,安裝指令為 ```shell pip3 install sortedcontainers ``` 但是通常 on-line judge 的機器不支援這個函式庫。 ```python= data = list(map(int, line.split())) task = SortedList() ans = [] for d in data: if d == 0: break if d > 0: task.add(d) else: if not task: continue if d == -1: ans.append(task.pop(0)) elif d == -2: ans.append(task.pop()) print(*ans) ``` ### C++ 程式碼 執行時間共 636 ms,使用記憶體最多約為 18196 kB,通過測試。 ```cpp= #include <iostream> #include <set> #include <vector> using namespace std; int main() { ios::sync_with_stdio(0); cin.tie(0); int d; // 暫存資料用的變數 d multiset<int> task; // 儲存列印工作的 nultiset,資料由大到小排序 while(cin >> d) { if (d == 0) break; if (d > 0) { task.insert(d); } else { if (task.empty()) continue; if (d == -1) { cout << *task.begin() << " "; task.erase(task.begin()); } else if (d == -2) { auto it = task.end(); it--; cout << *it << " "; task.erase(it); } } } cout << "\n"; return 0; } ``` ## [ZeroJudge: b938. kevin 愛殺殺](https://zerojudge.tw/ShowProblem?problemid=b938) 這題用 set 會太慢。 ### Python 程式碼 第10筆測資開始遇到 MemoryError。 ```python= n, m = map(int, input().split()) # 讀取人數 n,要淘汰的人數 m kill = list(map(int, input().split())) # 要淘汰指定編號的下一個人 p = [-1] + [i+1 for i in range(1, n)] + [-1] # 每個人對應的淘汰目標編號,最前面及第n個人的目標為 -1 for k in kill: # 依序讀取名單 if p[k] != -1: # 如果編號 k 的人還沒有被淘汰且後方有人 t = p[k] # 要淘汰的目標編號 t print(t) # 印出 t p[k] = p[t] # 將編號 k 的目標改為編號 t 的目標 p[t] = -1 # 編號 t 被淘汰,目標改為 -1 else: # 否則印出 0u0 ...... ? print("0u0 ...... ?") ``` ### C++ 程式碼 執行時間最久約為 0.3 s,使用記憶體最多約為 8 MB,通過測試。 ```cpp= #include <iostream> using namespace std; int main() { ios::sync_with_stdio(0); cin.tie(0); int n, m; cin >> n >> m; // 讀取人數 n,要淘汰的人數 m int kill[m]; // 要淘汰指定編號的下一個人 for(int i=0; i<m; i++) cin >> kill[i]; // 讀取 m 筆資料 int p[n+1]; // 每個人對應的淘汰目標編號,長度開 n+1,索引值對應到人的編號,自己已經被淘汰或後方沒人則目標為 -1 for(int i=1; i<=n; i++) p[i] = i+1; // 每個人目標預設為下一個 p[0] = -1; // 先設定編號 0 的人目標為 -1 p[n] = -1; // 先設定編號 n 的人目標為 -1 for(int i=0; i<m; i++) { // 依序讀取名單 int k = kill[i]; // 編號 k if (p[k] != -1) { // 如果編號 k 的人還沒有被淘汰且後方有人 int t = p[k]; // 要淘汰的目標編號 t cout << t << "\n"; // 印出 t p[k] = p[t]; // 將編號 k 的目標改為編號 t 的目標 p[t] = -1; // 編號 t 被淘汰,目標改為 -1 } else { // 否則印出 0u0 ...... ? cout << "0u0 ...... ?\n"; } } return 0; } ``` 改用 cstdio 函式庫的 scanf 及 printf,執行時間最久約為 0.3 s,使用記憶體最多約為 7.7 MB,通過測試。 ```cpp= #include <cstdio> using namespace std; int main() { int n, m; scanf("%d %d", &n, &m); // 讀取人數 n,要淘汰的人數 m int kill[m]; // 要淘汰指定編號的下一個人 for(int i=0; i<m; i++) { int k; scanf("%d", &k); kill[i] = k; // 讀取 m 筆資料 } int p[n+1]; // 每個人對應的淘汰目標編號,長度開 n+1,索引值對應到人的編號,自己已經被淘汰或後方沒人則目標為 -1 for(int i=1; i<=n; i++) p[i] = i+1; // 每個人目標預設為下一個 p[0] = -1; // 先設定編號 0 的人目標為 -1 p[n] = -1; // 先設定編號 n 的人目標為 -1 for(int i=0; i<m; i++) { // 依序讀取名單 int k = kill[i]; // 編號 k if (p[k] != -1) { // 如果編號 k 的人還沒有被淘汰且後方有人 int t = p[k]; // 要淘汰的目標編號 t printf("%d\n", t); // 印出 t p[k] = p[t]; // 將編號 k 的目標改為編號 t 的目標 p[t] = -1; // 編號 t 被淘汰,目標改為 -1 } else { // 否則印出 0u0 ...... ? printf("0u0 ...... ?\n"); } } return 0; } ``` ## [TOJ: 275 / 2015南市賽校內模擬賽 - Problem 3](https://toj.tfcis.org/oj/pro/275/) ### Python 程式碼 執行時間最久約為 1925 ms,使用記憶體最多約為 9748 kB,通過測試。 ```python= from bisect import insort_left big, small = [], [] # 分成大、小兩堆數字 n = int(input()) # 輸入資料行數 for i in range(1, n+1): # 讀取 n 行資料 x = int(input()) # 讀取資料存入 x if i == 1 or x < small[-1]: # 如果是第1筆資料或是小於 small 之中的最大值(最後一筆) insort_left(small, x) # 將 x 加入 small 並由小到大排序 else: # 反之將 x 加入 big 並由小到大排序 insort_left(big, x) while len(big) > i//2: small.append(big.pop(0)) while len(big) < i//2: big.insert(0, small.pop()) if i&1: ans = float(small[-1]) else: ans = (small[-1] + big[0]) / 2.0 print("{:.6f}".format(ans)) ``` ### C++ 程式碼 執行時間最久約為 124 ms,使用記憶體最多約為 6776 kB,通過測試。 ```cpp= #include <iostream> #include <set> #include <iomanip> using namespace std; multiset<unsigned int> big, small; // 輸入資料可能會重複,要用 multiset void pop_small(); void pop_big(); int main() { ios::sync_with_stdio(0),cin.tie(0); unsigned int n; cin >> n; for(size_t i=1, x; i<=n; i++) { cin >> x; if (i == 1 || x < *small.rbegin()) small.insert(x); else big.insert(x); while(big.size() > i/2) pop_big(); while(big.size() < i/2) pop_small(); double ans; cout << fixed << setprecision(6); if (i&1) ans = *small.rbegin() * 1.0; else ans = (*small.rbegin() + *big.begin())*1.0/2.0; cout << ans << "\n"; } return 0; } void pop_small() { big.insert(*small.rbegin()); small.erase(*small.rbegin()); } void pop_big() { small.insert(*big.begin()); big.erase(*big.begin()); } ``` ## [TIOJ: 1161. 4.虛擬番茄online](https://tioj.ck.tp.edu.tw/problems/1161) ### Python 程式碼 程式碼運作邏輯與底下的 C++ 版本相同,但是無法通過測試,第4筆測資超過記憶體上限。 ```python= from bisect import insort_left T = int(input()) # 測試資料組數 for _ in range(T): s = [] # 儲存目前需要的能力值,由小到大排序 n, k = map(int, input().split()) # 可以學的技能總數 n,要學的技能總數 k sa = [] # 學習各技能需要的能力值 s、a for _ in range(n): sa.append(list(map(int, input().split()))) # 讀取資料給 sa sa.sort() # 由小到大排序 ans = 1000000001 # 答案先預設為很大的值 for i in range(n): # 依序讀取 sa 的資料 insort_left(s, sa[i][1]) # 於 s 插入 sa[i][1] 並保持順序 if len(s) > k: s.pop() # 如果 s 的數量大於 k,移除最後一項 if len(s) == k: # 如果 s 的數量等於 k,更新 ans 為 ans 及 sa[i][0] + s[-1] 較小值 ans = min(ans, sa[i][0] + s[-1]) print(ans) ``` ### C++ 程式碼 執行時間共約為 920 ms,使用記憶體最多約為 34780 kB,通過測試。 ```cpp= #include <iostream> #include <set> #include <utility> #include <algorithm> using namespace std; int main() { ios::sync_with_stdio(0); cin.tie(0); int T; cin >> T; // 測試資料組數 for(int i=0; i<T; i++) { multiset<int, greater<int>> s; // 儲存目前需要的能力值,由大到小排序 int n, k; cin >> n >> k; // 可以學的技能總數 n,要學的技能總數 k pair<int, int> sa[n]; // 學習各技能需要的能力值 s、a for(int i=0; i<n; i++) cin >> sa[i].first >> sa[i].second; // 讀取資料給 sa sort(sa, sa+n); // 由小到大排序 int ans = 1000000001; // 答案先預設為很大的值 for(int i=0; i<n; i++) { // 依序讀取 sa 的資料 s.insert(sa[i].second); // 於 s 插入 sa[i].second 並保持順序 if (s.size() > (size_t)k) s.erase(s.begin()); // 如果 s 的數量大於 k,移除第一項 if (s.size() == (size_t)k) // 如果 s 的數量等於 k,更新 ans 為 ans 及 sa[i].first + *s.begin() 較小值 ans = min(ans, sa[i].first + *s.begin()); } cout << ans << "\n"; } return 0; } ``` ## [TOJ: 512 / F. 樓層交換](https://toj.tfcis.org/oj/pro/512/) ### Python 程式碼 第4筆測資超時。 ```python= n, q = map(int, input().split()) # 樓層數 n,事件數 q floor = [i for i in range(n+1)] # 各高度的樓層編號,前方加入一個用不到的 0 correct = floor.copy() # 編號i樓層目前所在的高度 for _ in range(q): # 執行 q 次 t, a, b = map(int, input().split()) # 事件種類 t,目標高度a、b if t == 1: # 第1種事件,高度a、b樓層互換 correct[floor[a]] = b # 原來於高度a的樓層編號移到高度b correct[floor[b]] = a # 原來於高度b的樓層編號移到高度a floor[a], floor[b] = floor[b], floor[a] # 交換編號 elif t == 2: # 第2種事件,復原高度a到b的樓層 ans = 0 # 答案 for i in range(a, b+1): if floor[i] != i: # 有效交換,高度 i 的樓層編號不等於 i ans += 1 # 答案加 1 j = correct[i] # 編號 i 目前位於高度 j correct[i] = i # 編號 i 回到高度 i correct[floor[i]] = j # 目前高度 i 的樓層編號移到 j floor[i], floor[j] = i, floor[i] # 高度 i 的樓層編號為 i,高度 j 的樓層編號為原來高度 i 的樓層編號 print(ans) # 印出答案 ``` 改用 set,第4筆測資超時。 ```python= wrong = set() # 高度錯誤的樓層 n, q = map(int, input().split()) # 樓層數 n,操作次數 q arr = [i for i in range(n+1)] # 各高度的樓層編號 arr correct = arr.copy() # 各編號的樓層現在的高度 correct def change(x, y): # 自訂函式,交換高度 x、y 的樓層 wrong.discard(x) # 從 wrong 移除 x,要用 discard 才不會在 x 不存在時回傳錯誤訊息 wrong.discard(y) # 從 wrong 移除 y arr[x], arr[y] = arr[y], arr[x] # 交換高度 x、y 對應的樓層編號 correct[arr[x]] = x # 編號 arr[x] 的樓層目前位於高度 x correct[arr[y]] = y # 編號 arr[y] 的樓層目前位於高度 y if arr[x] != x: wrong.add(x) # 如果 arr[x] 編號不等於 x,編號 x 放錯高度,加入 wrong if arr[y] != y: wrong.add(y) # 如果 arr[y] 編號不等於 y,編號 y 放錯高度,加入 wrong def recover(x, y): # 自訂函式,回復高度 x、y 的樓層,印出有效操作次數 ans = 0 # 有效操作次數 for i in range(x, y+1): # 依序處理高度 x 到 y 的樓層 if not wrong or i > max(wrong): break # 如果 wrong 是空的或 i 大於 wrong 的最大值,不會再有有效操作,中止迴圈 elif i in wrong: # 如果高度 i 在 wrong 之中 ans += 1 # 答案加 1 change(correct[i], i) # 呼叫 change,交換兩者 return ans # 回傳答案 for _ in range(q): # 執行 q 次 op, a, b = map(int, input().split()) # 讀取操作種類 op,高度 a、b if op == 1: change(a, b) else: print(recover(a, b)) ``` 如果要維持資料由小到大或由大到小排序,在 C++ 可以用 set,在 Python 要用 list 及 bisect.insort_left。第4筆測資超時。 ```python= from bisect import bisect_left, insort_left wrong = [] # 高度錯誤的樓層 n, q = map(int, input().split()) # 樓層數 n,操作次數 q arr = [i for i in range(n+1)] # 各高度的樓層編號 arr correct = arr.copy() # 各編號的樓層現在的高度 correct def change(x, y): # 自訂函式,交換高度 x、y 的樓層 if x in wrong: wrong.remove(x) # 從 wrong 移除 x if y in wrong: wrong.remove(y) # 從 wrong 移除 y arr[x], arr[y] = arr[y], arr[x] # 交換高度 x、y 對應的樓層編號 correct[arr[x]] = x # 編號 arr[x] 的樓層目前位於高度 x correct[arr[y]] = y # 編號 arr[y] 的樓層目前位於高度 y if arr[x] != x: insort_left(wrong, x) # 如果 arr[x] 編號不等於 x,編號 x 放錯高度,加入 wrong if arr[y] != y: insort_left(wrong, y) # 如果 arr[y] 編號不等於 y,編號 y 放錯高度,加入 wrong def recover(x, y): # 自訂函式,回復高度 x、y 的樓層,印出有效操作次數 ans = 0 # 有效操作次數 for i in range(x, y+1): # 依序處理高度 x 到 y 的樓層 idx = bisect_left(wrong, i) # 於 wrong 搜尋 i if idx == len(wrong): break # 如果 i 大於 wrong 的最大值,不會再有有效操作,中止迴圈 elif wrong[idx] == i: ans += 1 # 答案加 1 change(correct[i], i) # 呼叫 change,交換兩者 return ans # 回傳答案 for _ in range(q): # 執行 q 次 op, a, b = map(int, input().split()) # 讀取操作種類 op,高度 a、b if op == 1: change(a, b) else: print(recover(a, b)) ``` ### C++ 程式碼 第4筆測資超時。 ```cpp= #include <iostream> #include <algorithm> using namespace std; int main() { ios::sync_with_stdio(0); cin.tie(0); int n, q; cin >> n >> q; // 樓層數 n,事件數 q int floor[n+1], correct[n+1]; // 樓層編號,前方加入一個用不到的 0;第i樓層目前所在的高度 for(int i=1; i<=n; i++) floor[i] = correct[i] = i; for(int k=0; k<q; k++) { // 執行 q 次 int t, a, b; cin >> t >> a >> b; // 事件種類 t,目標高度a、b if (t == 1) { // 第1種事件,高度a、b樓層互換 correct[floor[a]] = b; // 原來於高度a的樓層編號移到高度b correct[floor[b]] = a; // 原來於高度b的樓層編號移到高度a swap(floor[a], floor[b]); // 交換編號 } else { // 第2種事件,復原高度a到b的樓層 int ans = 0; // 答案 for(int i=a; i<=b; i++) { if (floor[i] != i) { // 有效交換,高度 i 的樓層編號不等於 i ans++; // 答案加 1 int j = correct[i]; // 編號 i 目前位於高度 j correct[i] = i; // 編號 i 回到高度 i correct[floor[i]] = j; // 目前高度 i 的樓層編號移到 j floor[j] = floor[i]; // 高度 j 的樓層編號為原來高度 i 的樓層編號 floor[i] = i; // 高度 i 的樓層編號為 i } } cout << ans << "\n"; // 印出答案 } } return 0; } ``` 因為 C++ 的 set erase 回傳值是移除的資料數量,如果要移除的資料不在 set 之中會回傳 0,不會有錯誤訊息。如果用講義上的程式碼寫法比較好,執行時間最久約為 587 ms,使用記憶體最多約為 25720 kB,通過測試。 ```cpp= #include <iostream> #include <set> #include <algorithm> // for swap using namespace std; set<int> wrong; // 高度錯誤的樓層 int n, q; // 樓層數 n,操作次數 q int arr[1000005], correct[1000005]; // 各高度的樓層編號 arr,各編號的樓層現在的高度 correct void change(int x, int y) { // 自訂函式,交換高度 x、y 的樓層 wrong.erase(x); // 從 wrong 移除 x wrong.erase(y); // 從 wrong 移除 y swap(arr[x], arr[y]); // 交換高度 x、y 對應的樓層編號 correct[arr[x]] = x; // 編號 arr[x] 的樓層目前位於高度 x correct[arr[y]] = y; // 編號 arr[y] 的樓層目前位於高度 y if(arr[x] != x) wrong.insert(x); // 如果 arr[x] 編號不等於 x,編號 x 放錯高度,加入 wrong if(arr[y] != y) wrong.insert(y); // 如果 arr[y] 編號不等於 y,編號 y 放錯高度,加入 wrong } int recover(int x, int y) { // 自訂函式,回復高度 x、y 的樓層,印出有效操作次數 int ans = 0; // 有效操作次數 while(true) { // 無窮迴圈 auto it = wrong.lower_bound(x); // 於 wrong 搜尋 x,要用 set 的 lower_bound if(it == wrong.end() || *it > y) break; // 如果沒有找到 x 或是 *it 大於 y,不會再有有效操作,中止迴圈 ans++; // 答案加 1 change(correct[*it], *it); // 呼叫 change,交換兩者 } return ans; // 印出答案 } int main() { ios::sync_with_stdio(0); cin.tie(0); cin >> n >> q; for(int i=1; i<=n; i++) arr[i] = correct[i] = i; for(int i=1, op, a, b; i<=q; i++){ cin >> op >> a >> b; if(op == 1) change(a, b); else cout << recover(a, b) << "\n"; } return 0; } ``` ## [TOIJ: 1941. 直升機抓寶 (Helicopter)](https://tioj.ck.tp.edu.tw/problems/1941) ### Python 程式碼 從第24筆測資開始超時。 ```python= from bisect import bisect_right, insort_left s = [] # 目前能捕捉的寶可夢重疊區域 n = int(input()) # 地圖範圍為 (1, 1) ~ (n, n),共有 n 隻寶可夢 for _ in range(n): # 檢測 n 次 le, ri = map(int, input().split()) # 可以捕捉第 i 隻寶可夢的範圍 insort_left(s, le) # 將 le 插入 s 並保持順序 idx = bisect_right(s, ri) # 找出於 s 插入 ri 的索引值,若有同樣的資料加在最右側 if idx != len(s): s.pop(idx) # 如果 idx 沒有出界,移除 s[idx] print(len(s)) ``` ### C++ 程式碼 **注意事項:要用 set 內部的 upper_bound**,不能使用 algorithm 的 upper_bound。總執行時間約為 11711 ms,使用記憶體最多約為 17212 kB,通過測試。 ```cpp= #include <iostream> #include <set> using namespace std; int main() { ios::sync_with_stdio(0); cin.tie(0); multiset<int> s; // 目前能捕捉的寶可夢重疊區域 int n; cin >> n; // 地圖範圍為 (1, 1) ~ (n, n),共有 n 隻寶可夢 for(int i=0, le, ri; i<n; i++) { // 檢測 n 次 cin >> le >> ri; // 可以捕捉第 i 隻寶可夢的範圍 s.insert(le); // 將 le 插入 s 並保持順序 auto it = s.upper_bound(ri); // 找出於 s 插入 ri 的索引值,若有同樣的資料加在最右側 if (it != s.end()) s.erase(it); // 如果 it 沒有出界,移除 it } cout << s.size() << "\n"; return 0; } ``` ------ ###### tags:`演算法`、`APCS`、`Python`、`C++`