# 2023 ICPC Taiwan Online Programming Contest 簡易題解/提示 ###### tags: `TOPC` `ICPC` `Competitive Programming` ## Advance to Taoyuan Regional 極易 乍看之下,需要計算日期的資料,判斷出 ICPC 桃園區賽前 35 天是哪一天,並比較日期前後。但日期是固定的,可以透過人力計算,或是透過作業系統的日曆查出 9 月 16 日是最後一個可行日期。加上有觀察到 ISO-8601 格式的日期,可以直接透過字串比較大小來判斷日期的前後,那答案會非常簡單。以下為 Python 3 範例解答。 ```python= if input() <= '2023-09-16': print('GOOD') else: print('TOO LATE') ``` ## Better Chance 易 仔細讀完規則後,會發現只需要比較 $\frac{R_T-1}{S_T}$ 跟 $\frac{R_J-1}{S_J}$ 的大小關係,小的那一邊比較容易晉級。只是直接拿輸入來做計算,會因為浮點數誤差,難以判斷相等的情形。要避免浮點數誤差,一種方式是採用整數運算來避免。如範例程式的方式進行。 ```python= rt, rj, st, sj = [x for x in input().strip().split()] rt, rj = int(rt), int(rj) st = int(float(st) * 100 + 0.5) sj = int(float(sj) * 100 + 0.5) # 改用移項乘法比較大小,避免產生浮點,造成誤差 if (rt - 1) * sj < (rj - 1) * st: print('TAOYUAN') elif (rt - 1) * sj > (rj - 1) * st: print('JAKARTA') else: print('SAME') ``` 多數語言浮點數型態轉整數型態的時候,並非轉為最靠近的整數,而是捨去小數點後的資料居多,因此在後面加上 `0.5` 才會有把正小數轉換為最靠近整數的效果。另外也可採用 `round` 函數來達到轉換為最靠近整數值的效果,只是要注意 `round` 傳回值仍是 `float` 型態,要記得轉型為整數,雙精度浮點數(double-precision floating-point number)在絕對值不超過 $2^{52}$ 時,記錄的整數不會有誤差。 ## Cutting into Monotone Increasing Sequence 中偏易 本題問最少需要插入多少個逗號,讓一個數字 $x$ 變成一個單調遞增數列且滿足下列條件: 1. 最後一項不大於 $b$ 2. 逗號不能插入在 $0$ 之前 切不出來要印出 `NO WAY` 許多隊伍採取貪婪演算法,從數字最低位開始,切出比 $b$ 小的數字中,最長的一個。但是有一些特別的測資會導致切不出來。如: ``` 1234569009987999 90000 ``` 這題的作法,是使用動態規劃。假定 $x$ 有 $n$ 位,從高位到低位,將位數編號為 $0,\dots,n-1$。先定義一個輔助用的問題:「令 $x_{i,j}$ 是 $x$ 編號 $i$ 的位數開始的 $j$ 位數所代表的數字,在數列的第一個數字是 $j$ 位數的前提下,最少需要 $dp(i,j)$ 個逗號才能將 $y_i$ 變為單調遞增數列。」 $$dp(i,j)=\left\{\begin{array}{ll} 0&,i+j=n=1\mbox{ and }x\le b\\ \infty&,i+j=n=1\mbox{ and }x > b\\ \infty&,i+j>n\\ \infty&, i+j=n>1\mbox{ and } x_{i,1}=0\\ \infty&, i+j=n>1,x_{i,1}>0\mbox{ and } x_{i,j} > b\\ 0&, i+j=n>1,x_{i,1}>0\mbox{ and } x_{i,j} \le b\\ 1+\min_{x_{i,j}\le x_{i+j,k}\le b}dp(i+j,k)&,\mbox{otherwise} \end{array}\right.$$ 以上 $\infty$ 視為無解。前兩個類型,是處理特例,當 $x$ 長度只有 $1$ 時,只要看 $x$ 跟 $b$ 的大小即可知道有無解。第三個是處理長度不足 $j$ 時,無解。第四個例子是需要把逗號塞到 $0$ 前面,無解。第五個是處理 $x_{i,j}$ 比 $b$ 大,無解。第六個是 $x_{i,j}$ 剛好就是結尾 $j$ 為,且不超過 $b$,一個逗號都不用就是單調遞增數列。最後一個是其餘的通例,只要 $x_{i+j,n-i-j}$ 可以切出合乎要求的遞增數列,且第一項 $x_{i+j,k}$ 不超過 $x_{i,j}$,即代表一種可能的解是逗號塞在編號 $i+j$ 之前,取其中最小再加一就是 $dp(i,j)$。 另外,有一些特殊測資要別小心處理,比如: ``` 0 0 ``` 這部分的處理,就請參考範例程式。 ```c++= #include<cstdio> #include<cstring> #include<algorithm> using namespace std; int main() { // s 為代表 x 的字串 static char b[32], s[100001]; static int dp[100000][20]; scanf("%s %s", &s, &b); int n = strlen(s), m = strlen(b); for (int i = 0; i < n; i++) for(int j = 0; j <= m; j++) dp[i][j] = 1e7; for (int i = 1; b[i-1]; i++) dp[n-i][i] = (s[n-i] != '0') ? 0 : 1e7; if (strncmp(s+n-m, b, m) > 0) dp[n-m][m] = 1e7; if (strcmp("0", s) == 0) dp[0][1] = 0; for (int i = n-2; i >= 0; i--) { if (s[i] == '0') continue; for (int j = 1; j < m && i + j + j <= n; j++) { if (strncmp(s+i, s+i+j, j) <= 0) dp[i][j] = min(dp[i][j], dp[i+j][j] + 1); for (int k = j+1; k < m && i + j + k <= n; k++) dp[i][j] = min(dp[i][j], dp[i+j][k] + 1); if (i + j + m <= n && strncmp(s+i+j, b, m) <= 0) dp[i][j] = min(dp[i][j], dp[i+j][m] + 1); } if (i + m + m > n) continue; if (strncmp(s+i+m, b, m) > 0 || strncmp(s+i, s+i+m, m) > 0) continue; dp[i][m] = min(dp[i][m], dp[i+m][m] + 1); } int ans = 1e7; for (int i = 1; i <= m; i++) ans = min(ans, dp[0][i]); if (ans < 1e7) printf("%d\n", ans); else puts("NO WAY");; return 0; } ``` ## Divide a Convex 中偏久 依照提議算出凸多邊形之後,設計一個類似旋轉卡尺(rotating calipers)的雙指標(2-pointer)演算法的,去找出切割線段有一端點落在每一條邊上的最短切割線段長,這個長度可以透過三分搜尋法找出。答案就是過程中所找到最短的那一個。雖然實作的程式碼不短,但有準備的話,大部分是可以直接抄參考資料的。範例程式請參考[官網](http://topc2023.icpc.tw/2023-topc-sample-code.zip)。 ## Exponentiation 中偏易 有許多隊伍可能直覺想先解出 $x$ 再透過快速冪演算法求出答案,但面對分母有個 $2$ ,對合數 $m$ 取模數下的反元素沒信心就做不出來。 這邊提供兩個做法,可以迴避模反元素的計算。共同一個重要的觀察是對正整數 $p$ 和 $q$ , $$\left(x^p+\frac{1}{x^p}\right)\left(x^q+\frac{1}{x^q}\right)=x^{p+q}+x^{|p-q|}+x^{-|p-q|}+x^{-(p+q)}$$ $$x^{p+q}+\frac{1}{x^{p+q}}=\left(x^p+\frac{1}{x^p}\right)\left(x^q+\frac{1}{x^q}\right)-x^{|p-q|}-\frac{1}{x^{|p-q|}}$$ 由此,可以推出對一偶數 $2p$ , $$x^{2p}+\frac{1}{x^{2p}}=\left(x^p+\frac{1}{x^p}\right)\left(x^p+\frac{1}{x^p}\right)-1-1=\left(x^p+\frac{1}{x^p}\right)^2-2$$ 對一奇數 $2p$ , $$x^{2p+1}+\frac{1}{x^{2p+1}}=\left(x^{p+1}+\frac{1}{x^{p+1}}\right)\left(x^p+\frac{1}{x^p}\right)-x-\frac{1}{x}$$ \\ $$=\left(x^{p+1}+\frac{1}{x^{p+1}}\right)\left(x^p+\frac{1}{x^p}\right)-\alpha$$ 令答案為 $f(\alpha,\beta,m)$ ,我們可以得知: $$f(\alpha,1,m)=\alpha \mod m$$ $$f(\alpha,2p,m)=f(\alpha,p)^2-2 \mod m$$ $$f(\alpha,2p+1,m)=f(\alpha,p+1,m)f(\alpha,p,m)-\alpha\mod m$$ 如果將計算過的 $f(\alpha,p,m)$ 都記錄下來,利用 Memoization 的技法,以下的範例解答只需要 $O(\log \beta)$ 的四則運算就能做出答案。過程中會用到的 $p$ 只有 $O(\log_2\beta)$ 個。 ```python= a, b, m = [int(x) for x in input().strip().split()] lookup = {} def sol(a, b, m): if (ret := lookup.get(b)) != None: return lookup[b] if b == 0: ret = 2 % m elif b == 1: ret = a % m elif b % 2 == 0: ret = (sol(a, b//2, m) ** 2 - 2) % m else: ret = (sol(a, b//2, m) * sol(a, b-b//2, m) - a) % m lookup[b] = ret return ret print(sol(a, b, m)) ``` 另一個方法是觀察 $f(\alpha,\beta,m)=f(\alpha,\beta-1,m)f(\alpha,1,m)-f(\alpha,\beta-2,m)f(\alpha,\beta-2,m)\mod m$ 將上述等式寫成: $$\left(\begin{array}{cc}\alpha&-1\\1&0\end{array}\right)\left(\begin{array}{c}f(\alpha,\beta-1,m)\\f(\alpha,\beta-2,m)\end{array}\right)=\left(\begin{array}{c}f(\alpha,\beta,m)\\f(\alpha,\beta-1,m)\end{array}\right)$$ 可以推出利用矩陣快速冪運算的解題方法: $$\left(\begin{array}{cc}\alpha&-1\\1&0\end{array}\right)^{\beta-1}\left(\begin{array}{c}f(\alpha,1,m)\\f(\alpha,0,m)\end{array}\right)=\left(\begin{array}{cc}\alpha&-1\\1&0\end{array}\right)^{\beta-1}\left(\begin{array}{c}\alpha\\2\end{array}\right)=\left(\begin{array}{c}f(\alpha,\beta,m)\\f(\alpha,\beta-1,m)\end{array}\right)$$ ## Finding Bridges 中偏難 這個題目沒有任何強制在線的手段,可以反過來離線做:把刪除邊的操作反過來看成把邊加進來。從完全空的一張圖,把沒有被刪除的邊先一一加入,之後再反序加入被刪除的邊。加邊的過程中,維護一個與原圖有相同點集合的 undirected forest $G$,如果邊在加入的當下是橋「加入前不在原圖的同一個聯通塊」,則將該邊加入$G$,反之,不將該邊加入。所有的到這一步為止,花費對 $G$ 進行 DFS ,依照 DFS 拜訪順序,做成一個 directed forest $G'$,並將所有點標上深度。到這一步為止,花費 $O((n+m)\alpha(n+m))$。雖然 $G$ 上面的橋,在後續的邊加入之後,可能不再是橋,這部分的維護,就靠做第二輪。 接下來,把圖清空,重新把邊加回去,每次加上一條邊時,先檢查當下是否同在一個雙聯通元件,如果是,那就可以略過。如果不是,則有兩種可能: 1. 兩端點間原先就存在路徑,在對應的 undirected forest 中,同在一棵樹上,這樣要把加入後形成迴圈(loop)的邊,全部標為非橋,計數器扣除對應數量,並且把對應的雙聯通元件黏成一塊。 2. 兩端點間原先不存在路徑,那在對應的 undirected forest 中加上一條邊,並標記為樹邊。計數器加一。 黏成一塊的操作跟檢查一個點落在哪個雙聯通元件的操作可以透過 Union-Find Disjoint-Set 來實作。由於整個過程中只會出現 $O(n)$ 的樹邊,總共的 union 操作也不會超過 $O(n)$ 個。找迴圈的程式碼如果適當時做,則均攤下來,找迴圈總複雜度夠好,就不會超過時限。 ```c++= #include <bits/stdc++.h> using namespace std; constexpr int MAXN = 2000005; vector<int> G[MAXN]; int p[MAXN], depth[MAXN]; void dfs(int now, int par) { p[now] = par; for (int i : G[now]) { if (i == par) continue; depth[i] = depth[now] + 1; dfs(i, now); } } struct DSU { vector<int> p; void init(int n) { p.resize(n + 1); for (int i = 1; i <= n; i++) p[i] = i; } int Find(int x) { return p[x] == x ? x : p[x] = Find(p[x]); } void Union(int x, int y) { x = Find(x), y = Find(y); if (x == y) return; else if (depth[x] < depth[y]) { p[y] = x; } else { p[x] = y; } } } dsu; int main() { ios::sync_with_stdio(0); cin.tie(0); int n, m, q; cin >> n >> m >> q; vector<pair<int, int>> edges(m); vector<unordered_map<int, int>> edge_tag(n + 1); for (auto &[u, v] : edges) { cin >> u >> v; if (u > v) swap(u, v); edge_tag[u][v] = 0; } int cur_tag = m; for (int i = 1; i <= q; i++) { int u, v; cin >> u >> v; if (u > v) swap(u, v); edge_tag[u][v] = cur_tag--; } for (auto &m : edge_tag) for (auto &[v, idx] : m) { if (idx == 0) idx = cur_tag--; } sort(edges.begin(), edges.end(), [&](pair<int, int> e1, pair<int, int> e2) { return edge_tag[e1.first][e1.second] < edge_tag[e2.first][e2.second]; }); dsu.init(n); map<pair<int, int>, int> is_tree_edge; for (auto [u, v] : edges) { if (dsu.Find(u) != dsu.Find(v)) { dsu.Union(u, v); is_tree_edge[{u, v}] = 1; G[u].emplace_back(v); G[v].emplace_back(u); } } for (int i = 1; i <= n; i++) { if (dsu.Find(i) == i) { depth[i] = 0; dfs(i, -1); } } dsu.init(n); int cur_ans = 0; vector<int> ans = {0}; for (auto [u, v] : edges) { if (is_tree_edge[{u, v}]) cur_ans++; else { u = dsu.Find(u), v = dsu.Find(v); while (u != v) { if (depth[u] > depth[v]) { dsu.Union(u, p[u]); u = dsu.Find(p[u]); } else { dsu.Union(v, p[v]); v = dsu.Find(p[v]); } cur_ans--; } } ans.emplace_back(cur_ans); } reverse(ans.begin(), ans.end()); for (int i = 1; i <= q; i++) cout << ans[i] << '\n'; } ``` ## Gadget Construction 難 待命題人提供題解。 ## Heap Structure 中偏易 依照 Heap 的結構,我們將點從根開始編號 $1,\dots,n$ ,同一層填滿後,從下一層的最左邊繼續編號。第 $k$ 小的元素能夠放在某節點,有兩個條件要滿足: 1. 有不能有超過 $k-1$ 點在從 Root 到該節點的 Parent 最短路徑上,因此編號超過 $2^{k}-1$ 的,都不可能是答案。另一方面,不存在編號超過 $n$ 的點,因此最後一個可行的位置,編號為 $\min(n,2^k-1)$。實務上計算時,$2^k$可能很大,依照測資範圍,當 $k$ 超過 $64$ 時,取 64 即可。此部分的計算瓶頸為指數計算,但因為以二為基底,可以透過位元左移完成。 2. 該節點作為根的子樹,要有足夠多的值能夠放在該子樹的節點。即 $k,k+1,\dots,n$ 要能夠放在該子樹。子樹的大小,是隨著點編號遞減的,具單調性。可以透過二分搜尋法,找出第一個可以放 $k$ 的位置,或是說前面若干個位置,不能放 $k$。 詳細做法,請參考範例程式。 ```python= def test(n, k, pos): L, R = pos, pos cnt = 0 while L <= n: cnt += min(R, n) - L + 1 L = 2 * L R = 2 * R + 1 return n - cnt < k - 1 n, k = [int(x) for x in input().strip().split()] ok = min(n, 2 ** min(65,k) - 1) cannot = 0 step = 2 ** 63 while step > 0: if test(n, k, cannot + step): cannot += step step //= 2 print(ok-cannot) ``` ## Introversion 難 命題人提供之[題解](https://hackmd.io/@j8wK34P3RD2vfebYLWy2VA/SJ29U8qea)。 ## Java Warriors 簽到 一隊報名費 $4000$ 元,有 $n$ 隊答案就是 $4000 \times n$。 ```python= print(4000 * int(input()) ``` ## Kicks 簽到 由 ChatGPT 命題。只要對任何一種語言的字串熟悉,應該都能做。 ```python= s = input().strip() n = len(s) cnt = 0 for i in range(n-3): if s[i:i+4] == 'kick': cnt += 1 print(cnt) ``` ## Location, Location, Location 易 由 ChatGPT 命題,但調整難度為簡單題。由於曼哈頓距離總和其實是 X 軸方向跟 Y 方向可以拆開來看的。於是問題就變成在找 X 座標的下中位數 $x$ 配上 Y 座標的下中位數 $y$,答案是在 $(x,y)$ 蓋充電站。 ```python= n = int(input().strip()) x = [] y = [] for i in range(n): p, q = [int(v) for v in input().strip().split()] x.append(p) y.append(q) x.sort() y.sort() print(x[(n-1)//2], y[(n-1)//2]) ``` 至於下中位數為何是答案,首先,選在任何一個中位數,都會達成 X 分量的總距離和最短:移動所選的點,如往左會導致在選點右方的點貢獻距離,左方的點減少貢獻,每個點增減量絕對值會相等。因此最佳解必須落在中位數之上或之間,不然往某邊移動會得到更好的解。依照題目要求,下中位數是唯一合法的答案。