--- title: 競程一期中題解 tags: Editorial image: --- # 2022 Spring NYCU CP1 Midterm Exam -- Editorial [TOC] :::spoiler Problems {%pdf https://docdro.id/IcylOn8 %} ::: :::spoiler Rankings ![](https://i.imgur.com/gSSKpIa.png) ::: ## Problem A -- A Monad Is a Monoid in the Category of Endofunctors ###### Problem Setter: 黃克崴 :::spoiler Editorial 根據題敘找出符合定義的元素。 ::: :::spoiler Code (Python) ```python= n = int(input()) op = [list(map(int, input().split())) for _ in range(n)] for i in range(n): if all(op[i][j] == j + 1 == op[j][i] for j in range(n)): identity = i + 1 print(identity) invertible = [] for i in range(n): if any(op[i][j] == op[j][i] == identity for j in range(n)): invertible.append(i + 1) print(*invertible) ``` ::: :::spoiler Code (C++) ```cpp= #include <iostream> #include <vector> using namespace std; int main() { int n = 0; cin >> n; vector op(n + 1, vector(n + 1, 0)); for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { cin >> op[i][j]; } } int identity = 0; for (int i = 1; i <= n; i++) { bool is_identity = true; for (int j = 1; j <= n; j++) { if (op[i][j] != j || op[j][i] != j) { is_identity = false; } } if (is_identity) { identity = i; } } cout << identity << '\n'; for (int i = 1; i <= n; i++) { bool is_invertible = false; for (int j = 1; j <= n; j++) { if (op[i][j] == identity && op[j][i] == identity) { is_invertible = true; } } if (is_invertible) { cout << i << ' '; } } cout << '\n'; } ``` ::: ## Problem B -- Balanced Students ###### Problem Setter: 黃克崴 :::spoiler Editorial 將所有學生的分數 pair $(c_i, m_i)$ 依照 $c_i$ 排序後,他們的 Chinese ranking 會是 $n, n-1, \dots, 2, 1$。同理,依照 $m_i$ 排序後,他們的 Math ranking 會是 $n, n-1, \dots, 2, 1$。根據這兩種排序找出位置相同的學生數量即是答案。 總時間複雜度與排序的時間複雜度相同(通常是 $O(n \log n)$)。 ::: :::spoiler Code (Kotlin) ```kotlin= data class Student(val c_score: Int, val m_score: Int) fun main() { val n = readLine()!!.toInt() val scores = List<Student>(n) { val (c, m) = readLine()!!.split(" ").map{it.toInt()} Student(c, m) } val c_rank = scores.sortedWith(compareBy({it.c_score})) val m_rank = scores.sortedWith(compareBy({it.m_score})) val ans = c_rank.zip(m_rank).count{(c, m) -> c == m} println(ans) } ``` ::: :::spoiler Code (C++) ```cpp= #include <algorithm> #include <iostream> #include <vector> using namespace std; int main() { int n; cin >> n; vector<pair<int, int>> scores; for (int i = 0; i < n; i++) { int x, y; cin >> x >> y; scores.emplace_back(x, y); } auto sx = scores; auto sy = scores; sort(sx.begin(), sx.end()); sort(sy.begin(), sy.end(), [](const auto &a, const auto &b) { return a.second < b.second; }); int ans = 0; for (int i = 0; i < n; i++) { if (sx[i] == sy[i]) { ans++; } } cout << ans << endl; } ``` ::: ## Problem C -- Counting Pairs That Satisfy Some Random Requirements ###### Problem Setter: 郭軒語 :::spoiler Editorial 我們嘗試算出所有不符合第二個條件的 pair 以後,再從 $\frac{n(n-1)}{2}$ 裡面去扣。 不符合第二個條件有兩種可能:$a_j - a_i > j - i$ 和 $a_i - a_j > j - i$。整理一下式子可以得到 $a_j - j > a_i - i$ 和 $a_i + i > a_j + j$,兩個都可以用算 inversion pair 的方式算出分別符合這兩個條件的有幾個 pair。 ::: :::spoiler Code ```cpp= #include <algorithm> #include <iostream> #include <vector> using namespace std; using pii = pair<int, int>; int64_t count_pairs(vector<int> &a, int l, int r) { if (r <= l + 1) return 0LL; int m = (l + r) >> 1; int64_t ans = count_pairs(a, l, m) + count_pairs(a, m, r); int idxr = m; for (int i = l; i < m; i++) { while (idxr < r && a[idxr] <= a[i]) idxr++; ans += (r - idxr); } inplace_merge(a.begin() + l, a.begin() + m, a.begin() + r); return ans; } int main() { int n; cin >> n; vector<int> a(n); for (int &i : a) cin >> i; vector<int> tmp(n); int64_t ans = n * 1LL * (n - 1) / 2; for (int i = 0; i < n; i++) tmp[i] = a[i] - i; ans -= count_pairs(tmp, 0, n); for (int i = 0; i < n; i++) tmp[i] = a[i] + i; reverse(tmp.begin(), tmp.end()); ans -= count_pairs(tmp, 0, n); cout << ans << '\n'; } ``` ::: ## Problem D -- Deja Vu ###### Problem Setter: 郭軒語 :::spoiler Editorial 以下都令 $val(l,r) = \max\limits_{l \leq i \leq r} a_i - |b_l - b_r|$。 考慮分治。令 $f(L,R)$ 代表滿足 $L \leq l < r \leq R$ 的 $(l,r)$ 中 $val(l,r) = k$ 的數量。假設 $mid$ 是 $[L,R]$ 區間中最大值所在處,則 $f(L,R)$ 由以下三種可能組成 * $f(L,mid-1)$ * $f(mid+1,r)$ * $val(l,r) = k$,其中 $L \leq l \leq mid, mid \leq r \leq R, l \neq r$ 在計算 $f(L,R)$ 過程中,同時維護一個 set 和一個 map,set 存所有 $(a_i,i)$ 的 pair,map 存 $b_i$ 到 $i$ 的 count。 (set 的部分可以使用區間資料結構取代,例如 sparse table 或線段樹。) * 用第一個 set 可以求出 $mid$ 所在。這樣可以把當前區間切成兩半。 * 把兩半中長度比較短的另外開 set 跟 map,把屬於這個短區間的 $(a_i,i)$ 和 $b_i$ 取出分別放入新開的兩個 set 跟 map。在這裡因為啟發式合併所以每個 element 最多只會被移動 $\mathcal{O}(\log n)$ 次,所以這一步貢獻整體時間複雜度是 $\mathcal{O}(n \log^2 n)$。 * 現在我們已經固定 $\max\limits_{l \leq i \leq r} a_i$ 的值了,我們嘗試算出 $|b_l - b_r| = k - \max\limits_{l \leq i \leq r} a_i$ 的有多少。這可以在取出短區間的值的時候順便算出來。 * 接下來遞迴下去算 $f(L,mid-1)$ 和 $f(mid+1,r)$。 整題時間複雜度 $\mathcal{O}(n \log^2 n)$。 ::: :::spoiler Code ```cpp= #include <iostream> #include <map> #include <set> #include <vector> using namespace std; using pii = pair<int, int>; vector<int> a, b; int k; // [l,r) int64_t solve(int l, int r, set<pii> &s, map<int, int> &cnt) { // if len <= 1 then return if (l >= r) return 0LL; // find max and max_idx. Calculate target difference auto [max, max_idx] = *prev(s.end()); int target = max - k; if (target < 0) return 0LL; // declare base ans and shorter side set/map int64_t base_ans = 0; set<pii> ss; map<int, int> scnt; // count the pairs that one index is max_idx s.erase({max, max_idx}); cnt[b[max_idx]]--; if (cnt.find(b[max_idx] + target) != cnt.end()) base_ans += cnt[b[max_idx] + target]; if (target != 0 && cnt.find(b[max_idx] - target) != cnt.end()) base_ans += cnt[b[max_idx] - target]; // determine which side is shorter bool left_shorter = false; if (max_idx - l < r - max_idx) left_shorter = true; // count other pairs and split set and map into two int sl, sr; // range of the shorter side if (left_shorter) sl = l, sr = max_idx; else sl = max_idx + 1, sr = r; for (int i = sl; i < sr; i++) { s.erase({a[i], i}); cnt[b[i]]--; ss.insert({a[i], i}); scnt[b[i]]++; } for (int i = sl; i < sr; i++) { if (cnt.find(b[i] + target) != cnt.end()) base_ans += cnt[b[i] + target]; if (target != 0 && cnt.find(b[i] - target) != cnt.end()) base_ans += cnt[b[i] - target]; } // solve problem recursively if (left_shorter) return solve(l, max_idx, ss, scnt) + solve(max_idx + 1, r, s, cnt) + base_ans; else return solve(l, max_idx, s, cnt) + solve(max_idx + 1, r, ss, scnt) + base_ans; } int main() { int n; cin >> n >> k; a.resize(n), b.resize(n); for (int &i : a) cin >> i; for (int &i : b) cin >> i; set<pii> s; // (a_i, i) map<int, int> cnt; // count for each number for (int i = 0; i < n; i++) { s.insert({a[i], i}); cnt[b[i]]++; } cout << solve(0, n, s, cnt) << '\n'; } ``` ::: ## Problem E -- Even More Pair Counting Problems? ###### Problem Setter: 黃克崴 :::spoiler Hint 若 $A_y$ 表示序列 $a$ 中 $y$ 的數量,$B_z$ 表示序列 $b$ 中 $z$ 的數量,則對於某個值 $c$,滿足 $a_i + b_j = c$ 的 $(i, j)$ 數量為 $$ \sum_{y+z=c} A_y \times B_z $$ ::: :::spoiler Editorial 令 $N$ 表示所有序列中最大的元素。考慮多項式 $f(x) = A_0 + A_1 x + A_2 x^2 + \dots + A_N x^N$ 以及 $g(x) = B_0 + B_1 x + B_2 x^2 + \dots + B_N x^N$。他們的乘積是 $$ \begin{split} f(x) \times g(x) &= \sum_{i=0}^N \sum_{j=0}^N A_i \times B_j x^{i+j} \\ &= \sum_{k=0}^{2N} x^k \sum_{i+j=k} A_i \times B_j \end{split} $$ 所以 $f(x) \times g(x)$ 的 $c$ 次係數即是滿足 $a_i + b_j = c$ 的 $(i, j)$ 數量。算出乘積後,對於每個 $c_k$ 將乘積的 $c_k$ 次係數相加即是答案。 總時間複雜度為 $n + N \log N$(使用 FFT 做多項式乘法),或是 $n + N^{\log_23}$(使用 Karatsuba)。 ::: :::spoiler Code (FFT) ```cpp= #include <cmath> #include <complex> #include <iostream> #include <vector> using namespace std; using c = complex<double>; using poly = vector<c>; constexpr double pi = 3.141592653589793238; constexpr int N = 1 << 19; // recursive fast fourier transform void fft(poly &a, bool inv) { int n = a.size(); if (n == 1) { return; } poly a0(n / 2), a1(n / 2); for (int i = 0; i < n / 2; i++) { a0[i] = a[i * 2]; a1[i] = a[i * 2 + 1]; } fft(a0, inv); fft(a1, inv); double arg = pi * 2 / n * (inv ? -1 : 1); complex<double> w(1), wn(cos(arg), sin(arg)); for (int i = 0; i < n / 2; i++) { a[i] = a0[i] + w * a1[i]; a[i + n / 2] = a0[i] - w * a1[i]; w *= wn; } if (inv) { for (int i = 0; i < n; i++) { a[i] /= 2; } } } // multiply two polynomials of degree N - 1 poly multiply(poly f, poly g) { int n = N; f.resize(n); g.resize(n); fft(f, 0); fft(g, 0); poly res; for (int i = 0; i < n; i++) res.push_back(f[i] * g[i]); fft(res, 1); return res; } int main() { int n; cin >> n; poly a(N), b(N); for (int i = 0; i < n; i++) { int x; cin >> x; a[x] += 1; } for (int i = 0; i < n; i++) { int x; cin >> x; b[x] += 1; } poly d = multiply(a, b); long long ans = 0; for (int i = 0; i < n; i++) { int x; cin >> x; ans += round(d[x].real()); } cout << ans << '\n'; } ``` ::: :::spoiler Code (Karatsuba) ```cpp= #include <iostream> #include <vector> using namespace std; constexpr int N = 1 << 18; // karatsuba algorithm vector<long long> multiply(vector<long long> f, vector<long long> g) { int n = f.size(); vector<long long> res(2 * n); if (n <= 32) { for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { res[i + j] += f[i] * g[j]; } } return res; } int k = n / 2; vector<long long> f0(k), f1(k), g0(k), g1(k); for (int i = 0; i < k; i++) { f0[i] = f[i]; f1[i] = f[i + k]; g0[i] = g[i]; g1[i] = g[i + k]; } vector<long long> a = multiply(f0, g0); vector<long long> b = multiply(f1, g1); for (int i = 0; i < k; i++) { f0[i] += f1[i], g0[i] += g1[i]; } vector<long long> c = multiply(f0, g0); for (int i = 0; i < n; i++) { res[i] += a[i]; res[i + n] += b[i]; res[i + k] += c[i] - a[i] - b[i]; } return res; } int main() { int n; cin >> n; vector<long long> a(N), b(N); for (int i = 0; i < n; i++) { int x; cin >> x; a[x] += 1; } for (int i = 0; i < n; i++) { int x; cin >> x; b[x] += 1; } vector<long long> d = multiply(a, b); long long ans = 0; for (int i = 0; i < n; i++) { int x; cin >> x; ans += d[x]; } cout << ans << '\n'; } ``` ::: ## Problem F -- Function With Hashing Functionality ###### Problem Setter: 郭軒語 :::spoiler Editorial $n$ 個 pair 當中,令 $a_i = \lfloor \frac{x_i}{2^{32}}\rfloor, b_i = x_i \bmod 2^{32}$,則可以知道 $h'(a_i) = h(x_i) \bmod 2^{32}, h'(b_i) = \lfloor \frac{h(x_i)}{2^{32}}\rfloor$。 用一個類似紅黑樹的資料結構紀錄所有知道的 $h'(\cdot)$ 值,之後每筆詢問就能根據公式推算出答案。可以使用各自語言中實作好的資料結構,例如在 C++ 中可以使用 `std::map`, 而在 Python 中可以使用 `dict`。 注意到因為這題是使用 hexadecimal number 做輸出入,所以也可以用字串處理的方式做 mapping。 ::: :::spoiler Code ```cpp= #include <iostream> #include <map> using namespace std; int main() { int n, q; cin >> n >> q; map<string, string> hf; for (int i = 0; i < n; i++) { string x, y; cin >> x >> y; hf[x.substr(0, 8)] = y.substr(8, 8); hf[x.substr(8, 8)] = y.substr(0, 8); } while (q--) { string x; cin >> x; if (hf.find(x.substr(0, 8)) == hf.end() || hf.find(x.substr(8, 8)) == hf.end()) { cout << "-1\n"; } else { cout << hf[x.substr(8, 8)] + hf[x.substr(0, 8)] << '\n'; } } } ``` ::: ## Problem G -- Grievous Light Bulb Decoration ###### Problem Setter: 范釗維 :::spoiler Hint 1 一個區間 $[L, R]$ 可以被選的前提是 $|\{a_L, a_{L+1}, \ldots, a_R\}| \le k$。 ::: :::spoiler Hint 2 對每個左界 $L$,都可以找到一個最大的右界 $R$ 使得 $R = n$ 或 $|\{a_L, a_{L+1}, \ldots, a_R\}| = k$。 而且只要 $L$ 遞增,$R$ 也會跟著遞增。 ::: :::spoiler Editorial 用 two-pointer 維護 $L$、$R$,時間複雜度 $\mathcal{O}(n)$。 ~~不要吃毒寫二分搜 la~~ ::: :::spoiler Code ```cpp= #include <bits/stdc++.h> using namespace std; void solve() { int N, K; cin >> N >> K; vector<int> A(N); for (int &x : A) cin >> x, --x; int type = 0, ans = 0; vector<int> cnt(N, 0); for (int L = 0, R = 0; R < N; ++R) { type += (cnt[A[R]]++ == 0); while (type > K) type -= (--cnt[A[L]] == 0), ++L; if (R-L+1 > ans) ans = R-L+1; } cout << ans << "\n"; } int32_t main() { ios_base::sync_with_stdio(0), cin.tie(0); int t; cin >> t; for (int _ = 1; _ <= t; ++_) { solve(); } return 0; } ``` ::: ## Problem H -- Harmonic Heap Merging ###### Problem Setter: 范釗維 :::spoiler Hint 回顧之前學 Disjoint Set 的時候講到的「啟發式合併」。 ::: :::spoiler Editorial 合併兩個 heap 的時候把 size 小的丟到 size 大的,這樣每個元素至多只會被丟 $\mathcal{O}(\log q)$ 次。 需要用到的空間再開就好。 如果你使用的資料結構是 `std::set` 或 `std::priority_queue`,時間複雜度就是 $\mathcal{O}(q \log^2 q)$。 另一種解法是寫一個可併堆 or 左偏樹(詳見 pbds),可以達到 $\mathcal{O}(q \log q)$ 的時間複雜度。 ::: :::spoiler Code ```cpp= #include <bits/stdc++.h> using namespace std; template <typename T> using prior = priority_queue<T, vector<T>, greater<T>>; #define ee emplace #define SZ(x) ((int)(x).size()) int32_t main() { ios_base::sync_with_stdio(0), cin.tie(0); int N, Q; cin >> N >> Q; int tok = 0; vector<prior<int>> heaps(2*Q); map<int, int> inv; auto _create = [&](int x) {inv[x] = tok, heaps[tok].ee(x), ++tok;}; auto _add = [&](int x, int to) {inv[x] = to, heaps[to].ee(x);}; for (int q = 1; q <= Q; ++q) { string op; cin >> op; if (op == "merge") { int x, y; cin >> x >> y; if (!inv.count(x)) _create(x); if (!inv.count(y)) _create(y); if (inv[x] == inv[y]) {cout << -1 << "\n"; continue;} prior<int> &px = heaps[inv[x]], &py = heaps[inv[y]]; if (SZ(px) < SZ(py)) {while (SZ(px)) _add(px.top(), inv[y]), px.pop();} else {while (SZ(py)) _add(py.top(), inv[x]), py.pop();} cout << SZ(heaps[inv[x]]) << "\n"; } else if (op == "take") { int x; cin >> x; if (!inv.count(x)) {cout << x << "\n"; continue;} int val = heaps[inv[x]].top(); heaps[inv[x]].pop(); _create(val); cout << val << "\n"; } } return 0; } ``` ::: :::spoiler Code using pbds ```cpp= #include <bits/stdc++.h> #include <ext/pb_ds/priority_queue.hpp> using namespace std; template <typename T> using prior = __gnu_pbds::priority_queue<T, greater<T>, __gnu_pbds::pairing_heap_tag>; struct DSU { vector<int> p; void init(int N) { p.resize(N), iota(begin(p), end(p), 0); } int Find(int x) {return x ^ p[x] ? p[x] = Find(p[x]) : x;} int Union(int x, int y) {x = Find(x), y = Find(y); return x ^ y ? p[y] = x, 1 : 0;} }; int32_t main() { ios_base::sync_with_stdio(0), cin.tie(0); int N, Q; cin >> N >> Q; int tok = 0; DSU dsu; dsu.init(2*Q); vector<prior<int>> heaps(2*Q); unordered_map<int, int> inv; auto _create = [&](int x) {inv[x] = tok, heaps[tok].push(x), ++tok;}; for (int q = 1; q <= Q; ++q) { string op; cin >> op; if (op == "merge") { int x, y; cin >> x >> y; if (!inv.count(x)) _create(x); if (!inv.count(y)) _create(y); x = dsu.Find(inv[x]), y = dsu.Find(inv[y]); if (x == y) {cout << -1 << "\n"; continue;} heaps[x].join(heaps[y]), dsu.Union(x, y); cout << heaps[dsu.Find(x)].size() << "\n"; } else if (op == "take") { int x; cin >> x; if (!inv.count(x)) {cout << x << "\n"; continue;} x = dsu.Find(inv[x]); int val = heaps[x].top(); heaps[x].pop(); _create(val); cout << val << "\n"; } } return 0; } ``` ::: ## Problem I -- Interval Deletion ###### Problem Setter: 黃克崴 & 范釗維 :::spoiler Hint 1 固定 $c$ 的話你會做嗎? 顯然 $a_1$ 一定要被一個區間蓋到,所以就先選 $[a_1, a_1 + c]$。 顯然 $a_{x = \arg\min_k\{a_k > a_1 + c\}}$ 一定要被一個區間蓋到,所以就再選 $[a_x, a_x + c]$。 顯然 $a_{y = \arg\min_k\{a_k > a_x + c\}}$ 一定要被一個區間蓋到,所以就再選 $[a_y, a_y + c]$。 以此類推,greedy 的取就好。 ::: :::spoiler Hint 2 如果 $c$ 是一個解,那對於 $c' > c$ 的 $c'$ 也是一個解。 ::: :::spoiler Editorial 對答案二分搜,找到第一個符合條件的 $c$。 ::: :::spoiler Code ```cpp= #include <bits/stdc++.h> using namespace std; const int INF = 1'000'000'000; int32_t main() { ios_base::sync_with_stdio(0), cin.tie(0); int N, K; cin >> N >> K; vector<int> A(N); for (int &x : A) cin >> x; auto calc = [&](int len) -> bool { int cnt = 0, lst = 0; for (int x : A) {if (x > lst) lst = x + len, ++cnt;} return (cnt <= K); }; int lo = 0, hi = INF, mi; while (lo < hi) { mi = (lo + hi) >> 1; if (calc(mi)) hi = mi; else lo = mi + 1; } cout << lo << "\n"; return 0; } ``` :::