--- tags: 成大高階競技程式設計 2021 image: https://i.imgur.com/IIj9BcL.png --- # 2021 高階競程 Contest #5 - 題解 ## [forbid patrn](https://judge.csie.ncku.edu.tw/problems/109) - Task Provider: GY - Task Setter: ys ### 首殺 team21_32 (00:06) ### 解法說明 觀察到 $n = 3$ 的情況 - `100` - `110` - `111` - `000` 這幾種字串不管向右 shift 幾次,都會是合法的字串 前 $2$ 種都能靠 shift 得到 $3$ 個不重複的字串,所以共有 $2 \times 3 + 2 = 8$ 個字串 > 其中向右 shift 一次表示最右邊的字元移到最左邊 再觀察到 $n = 4$ 的情況 - `1000` - `1100` - `1110` - `1111` - `0000` 這幾種字串不管向右 shift 幾次,都會是合法的字串 前 $3$ 種都能靠 shift 得到 $4$ 個不重複的字串,所以共有 $3 \times 4 + 2 = 14$ 個字串 依此類推不管 $n$ 為多少,總共會有 $(n-1)\cdot n +2$ 個字串 ### 標準程式 :::spoiler ```cpp #include<cstdio> using namespace std; long long n; int main() { scanf("%lld", &n); printf("%lld\n", n*(n-1)+2); return 0; } ``` ::: ## [交友遊戲](https://judge.csie.ncku.edu.tw/problems/110) - Task Provider: D4nnyLee - Task Setter: D4nnyLee ### 首殺 team21_21 (00:41) ### 解法說明 因為每送一封信,寄件人跟收件人都會收到 $1$ 塊錢,因此每個人收到的錢的總數一定是偶數。 所以如果每個人的目標金額加起來是奇數的話就可以直接輸出 $-1$。 要注意到題目中沒有限制寄件人跟收件人一定要是不同人, 因此可以自己寄給自己,並且得到 $2$ 塊錢。 所以只要總和為偶數,我們就可以依序用以下的步驟達成目標: 1. 找出所有目標金額為奇數的人,將他們兩兩配成一對,並且讓其中一人寄信給另一人 2. 所有人都自己寄信給自己,直到達到目標金額 在步驟 $1$ 中,因為總和為偶數,所以目標金額為奇數的人一定有偶數個,也就代表奇數的人一定可以找到其他人跟他配。 ### 標準程式 :::spoiler ```cpp #include <iostream> #include <vector> #include <array> using namespace std; int main() { int n; cin >> n; vector<int> c(n); for (auto &ci : c) cin >> ci; array<vector<int>, 2> arr{}; for (int i = 0; i < n; ++i) arr[c[i] & 1].push_back(i); if (arr[1].size() % 2) { cout << "-1\n"; return 0; } vector<pair<int, int>> ans; for (int i{1}, len = arr[1].size(); i < len; i += 2) ans.emplace_back(arr[1][i - 1], arr[1][i]); for (int i{0}; i < n; ++i) { while (c[i] > 1) { ans.emplace_back(i, i); c[i] -= 2; } } cout << ans.size() << '\n'; for (auto [u, v] : ans) cout << u + 1 << ' ' << v + 1 << '\n'; return 0; } ``` ::: ## [超時空回歸](https://judge.csie.ncku.edu.tw/problems/111) - Task Provider: arashi87 - Task Setter: arashi87 ### 首殺 team21_12 (01:28) ### 解法說明 這題是很裸的拓墣排序,唯一變化的只有需要套上優先隊列來獲取最小的編號 (不過因為 $N$ 很小,也有人暴搜 AC)。每筆輸入都會給定 $D1$ 和 $D2$,所以可以知道 $D2$ 前面至少還有一個編號,因此 $D2$ 入度加一,接者把 $M$ 筆操作都這樣做一遍就可以得到一個所有點的入度陣列。 有了入度陣列後就可以很容易地知道入度為 $0$ 的點可以隨意排列,唯一需要遵循的只有按照大小排序而已,接著排序完所有入度為 $0$ 的點後需要將所有與之相連的點的入度也都減一 (相當於 $D1$ 已經確定位置了,所以 $D2$ 可以少顧慮一個點),所以前面輸入時也需要維護一個陣列用來確認哪些點相連,接著只需要重複上述步驟直到所有點排序完畢。 需要注意的是有很多人想試著用權重加 sort 通過,不過這是行不通的,在兩個點入度相等的時候他們還是會有先後順序,一定會有其中一個點的入度先拔完,所以行不通。 ### 標準程式 :::spoiler ```cpp #include <queue> #include <stdio.h> #include <string.h> #define maxN 507 using namespace std; int n, m; int mp[maxN][maxN], in[maxN]; priority_queue<int, vector<int>, greater<int>> pq; int main() { while (~scanf("%d%d", &n, &m)) { memset(mp, 0, sizeof mp); memset(in, 0, sizeof in); for (int i = 1, x, y; i <= m; i++) scanf("%d%d", &x, &y), mp[x][y] = 1, in[y]++; for (int i = 1; i <= n; i++) if (!in[i]) pq.push(i); while (!pq.empty()) { int tp = pq.top(); pq.pop(); printf("%d ", tp); for (int i = 1; i <= n; i++) { if (mp[tp][i]) { in[i]--; if (!in[i]) pq.push(i); } } } puts(""); } } ``` ::: ## [眼神殺 - 羔羊](https://judge.csie.ncku.edu.tw/problems/112) - Task Provider: HDU 5115 - Task Setter: raiso ### 首殺 -- (-:-) ### 解法說明 本題目的為尋找最佳決策,使用的策略為「枚舉最後一次決策」。 在區間 $[0, n-1]$ 的口試委員中,誰最後走傷害最小?在區間 的口試委員中,誰最後走傷害最小? 這就是一種 Top-Down 的思路,這樣我們需要做的就是「記憶化搜索」,避免重複計算。 在一個區間中,走的人可能有三種「最左、中間與最右」,轉移式: 將`dfs(L, R)` 定義成:當區間 $[L,R]$ 都離開後,受到的最小傷害值。 枚舉這個區間中最後一個走的人是誰就好了。 ``` ans = min(ans, dfs(L+1, R)+A[L]); ans = min(ans, dfs(L, R-1)+A[R]); for(int i = L+1; i < R; i++) //"i" is the last one leaving. ans = min(ans, dfs(L, i-1)+dfs(i+1, R)+A[i]); ``` ### 標準程式 :::spoiler ```cpp #include <bits/stdc++.h> #define Int long long using namespace std; vector<Int> A, B; int n; vector< vector< Int > > his; vector< vector< bool > > vis; Int dfs(int L, int R) { //[L, R] leaving if(vis[L][R] == 1) return his[L][R]; vis[L][R] = 1; Int ans = LLONG_MAX; //w=1 if(L == R) ans = A[L]; //w>1 if(L+1 <= R) ans = min(ans, dfs(L+1, R)+A[L]); if(L <= R-1) ans = min(ans, dfs(L, R-1)+A[R]); for(int i = L+1; i < R; i++) //"i" is the last one leaving. ans = min(ans, dfs(L, i-1)+dfs(i+1, R)+A[i]); //eye-damage if(L-1 >= 0) ans += B[L-1]; if(R+1 < n) ans += B[R+1]; return his[L][R] = ans; } int main() { cin >> n; A.resize(n); B.resize(n); his.assign(n, vector<Int>(n)); vis.assign(n, vector<bool>(n, 0)); for(auto& it: A) cin >> it; for(auto& it: B) cin >> it; Int ans = dfs(0, n-1); cout << ans << endl; return 0; } ``` ::: ## [壞掉的電腦](https://judge.csie.ncku.edu.tw/problems/113) - Task Provider: baluteshih - Task Setter: leo ### 首殺 team21_34 (02:22) ### 解法說明 你可以透過對所有電腦查詢某台電腦是否為壞掉的, 例如標準程式中詢問所有人 $1$ 號是否為壞掉的, 如果是壞掉的話就可以直接輸出答案, 否則就可以對所有電腦二分搜, 並利用該電腦(即上面的 $1$ 號)來驗證區間是否存在壞掉的電腦, 因為題目保證會有壞掉的電腦,所以如果 $[l,m]$ 不存在壞掉的電腦, 則 $[m+1,r]$ 一定有壞掉的電腦,即可找出答案 ### 標準程式 :::spoiler ```cpp #include "lib0113.h" int main() { int n, l, r, m, cnt = 0; n = Init(); for(int i = 1; i <= n; i++) cnt += (Query(i, 1, 1) ? 1 : -1); if(cnt > 0) Fix(1); for(l = 1, r = n; l < r;){ m = l + r >> 1; if(Query(1, l, m)) r = m; else l = m + 1; } Fix(l); } ``` ::: ## [推甄季](https://judge.csie.ncku.edu.tw/problems/114) - Task Provider: XDEv11 - Task Setter: ys ### 首殺 -- (-:-) ### 解法說明 我們可以把每個研究所的面試開始與結束時間當成二個事件,並用時間軸的方式儲存。 之後在每天的開始前,我們就將那天會開始開放面試的研究所加入; 在每天結束後,將那天結束開放面試的研究所移除; 並在每天的開放時段,去尋找可以面試並擁有前 $K$ 大炫耀資本的研究所。 而研究所若是以炫耀資本為觀點儲存, 在每次加入時,只要找到目前炫耀資本第 $K$ 大的研究所,並檢查是否需要替換; 而在每次移除時,則要找到目前炫耀資本第 $K+1$ 大的研究所,檢查是否補上; 這部分就是在 Range Query 時講過的 [K-th one](https://hackmd.io/@XDEv11/NCKU-AdvCP-2021-Range_Query#%E7%AC%AC-k-%E5%80%8B-1%EF%BC%88K-th-one%EF%BC%89)。 > 若移除的研究所炫耀資本為前 $K$ 大,它的炫耀資本應當比第 $K+1$ 大的研究所還大, > 在移除後則由第 $K+1$ 大的研究所補上。 ### 標準程式 :::spoiler ```cpp #include <iostream> #include <vector> #include <algorithm> using namespace std; class SGT { int n; vector<long long> t; int left(int tv) { return tv + 1; } int right(int tv, int tl, int tm) { return tv + 2 * (tm - tl); } void modify(int p, long long x, int tv, int tl, int tr) { if (tl == tr - 1) t[tv] += x; else { int tm{(tl + tr) / 2}; if (p < tm) modify(p, x, left(tv), tl, tm); else modify(p, x, right(tv, tl, tm), tm, tr); t[tv] = t[left(tv)] + t[right(tv, tl, tm)]; } } long long query(int l, int r, int tv, int tl, int tr) { if (l == tl && r == tr) return t[tv]; int tm{(tl + tr) / 2}; if (r <= tm) return query(l, r, left(tv), tl, tm); else if (l >= tm) return query(l, r, right(tv, tl, tm), tm, tr); else return query(l, tm, left(tv), tl, tm) + query(tm, r, right(tv, tl, tm), tm, tr); } public: explicit SGT(int _n) : n{_n}, t(2 * n - 1) {} void modify(int p, long long x) { modify(p, x, 0, 0, n); }; long long query(int l, int r) { return query(l, r, 0, 0, n); } int ps_lower_bound(long long ps) { /* binary search on tree */ if (ps > t[0]) return -1; int tv{0}, tl{0}, tr{n}; while (tr - tl > 1) { int tm{(tl + tr) / 2}; if (t[right(tv, tl, tm)] >= ps) tv = right(tv, tl, tm), tl = tm; else ps -= t[right(tv, tl, tm)], tv = left(tv), tr = tm; } return tl; } }; void solve() { int D, N, K; cin >> D >> N >> K; vector<vector<int>> vb(D), ve(D); for (int i{0}; i < N; ++i) { int c, b, e; cin >> c >> b >> e, --b, --e; vb[b].push_back(c); ve[e].push_back(c); } SGT sgt{300001}; long long ans{0}, cnt{0}; for (int i{0}; i < D; ++i) { for (auto& x : vb[i]) { int kth{sgt.ps_lower_bound(K)}; if (x > kth) { if (kth != -1) cnt -= kth; cnt += x; } sgt.modify(x, 1); } ans = max(ans, cnt); for (auto& x : ve[i]) { int kth{sgt.ps_lower_bound(K + 1)}; if (x > kth) { cnt -= x; if (kth != -1) cnt += kth; } sgt.modify(x, -1); } } cout << ans << '\n'; } int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); int t{1}; //cin >> t; while (t--) solve(); return 0; } ``` ::: ## 國軍弟兄們,上戰場囉![Easy](https://judge.csie.ncku.edu.tw/problems/115), [Hard](https://judge.csie.ncku.edu.tw/problems/116) - Task Provider: ICPC Taipei 2020 E - Task Setter: raiso ### Easy 首殺 audit21_05 (02:55) ### Hard 首殺 -- (-:-) ### 解法說明 我們先從最後兩步驟看這個問題,如:`AAABBBA`,我們可以將這些弟兄看成三段$L(1,2,3),M(4,5,6),R(7)$,在這次的案例中,若要可以成功派出這些弟兄,$M$為可派出,$L$與$R$合併後可派出。 在一般化問題後,我們可以將問題換成,若區段$S$可派出,可能可以全派出,或是其中一段派出後,剩餘可派出,此段可能在$S$靠左、靠右或是都不靠。 `xxxxxxxxx` `xxxx.....` `...xxx...` `.....xxxx` :::spoiler ```cpp #include <bits/stdc++.h> #define pb push_back using namespace std; string S; int X; bool sand(string S) { for(auto it: S) if(it != S.back()) return 0; if(S.size() < X) return 0; return 1; } bool dfs(string S) { //1 part if(sand(S)) return 1; bool isok = 0; int n = S.size(); //2 part for(int i = 1; !isok && i < n; i++) { //LL->[0, i-1], RR->[i, n-1] string LL = S.substr(0, i); string RR = S.substr(i, n-i); if(!isok && sand(LL) && dfs(RR)) isok = 1; if(!isok && dfs(LL) && sand(RR)) isok = 1; } //3 part for(int i = 1; !isok && i < n; i++) for(int j = i+1; !isok && j < n; j++) { //LL->[0, i-1], MM->[i, j-1], RR->[j, n-1] string LL = S.substr(0, i); string MM = S.substr(i, j-i); string RR = S.substr(j, n-j); if(!isok && sand(MM) && dfs(string(LL+RR))) isok = 1; } return isok; } int main() { cin >> S >> X; bool isok = dfs(S); cout << (isok? "Yes": "No") << endl; } ``` ::: (未完待續,之後再詳細的解釋) ### 標準程式 :::spoiler ```cpp #include <bits/stdc++.h> #define pb push_back #define mp make_pair #define x first #define y second using namespace std; string S; int X, n; int main() { cin >> S >> X, n = S.size(); vector< pair<char, int> > A{mp(S[0], 0)}; for(auto it: S) { if(it == A.back().x) A.back().y++; else A.pb(mp(it, 1)); } n = A.size(); vector< vector<int> > dp(n, vector<int>(n, INT_MIN)); //w=1 for(int i = 0; i < n; i++) dp[i][i] = A[i].y; //w=2 for(int i = 0; i < n-1; i++) { if(A[i].x == A[i+1].x) dp[i][i+1] = dp[i][i] + dp[i+1][i+1]; else if(dp[i+1][i+1] >= X) dp[i][i+1] = dp[i][i]; } //w>2 for(int w = 3; w <= n; w++) for(int L = 0; L < n-w+1; L++) { int R = L+w-1; auto& now = dp[L][R]; //2 part for(int i = L+1; i <= R; i++) if(dp[L][i-1] > 0 && dp[i][R] > 0){ //[L, i-1], [i, R] if(A[L].x == A[i].x) now = max(now, dp[L][i-1] + dp[i][R]); else if(dp[i][R] >= X) now = max(now, dp[L][i-1]); } //3 part for(int i = L+2; i <= R; i++) if(A[L].x == A[i].x && dp[L+1][i-1] >= X && dp[i][R] > 0) { //[L, L], [L+1, i-1], [i, R] now = max(now, dp[L][L]+dp[i][R]); } } cout << (dp[0][n-1] >= X? "Yes": "No") << endl; return 0; } ``` :::