# 【Uva 題庫解題】CPE 二顆星選集 - part1 [TOC] 本筆記及該系列主要以每篇 5 題為主,也僅供筆者個人學習用途,斟酌參考。 若你喜歡本筆記,不妨為文章按下一顆愛心,或是追蹤我的個人公開頁,感謝您點入本篇筆記。 CPE 二顆星選集來源:https://par.cse.nsysu.edu.tw/~advprog/star.php ## Uva 151 - Power Crisis PDF Source:https://onlinejudge.org/external/1/151.pdf Uva Online Judge:https://onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=87 Zerojudge:https://zerojudge.tw/ShowProblem?problemid=q891 該題為約瑟夫問題的變體。 題目架構: - 共 $N$ 個地區( $1$ 到 $N$ ) - 地區 $1$ 總是第一個被切斷。 - 剩下 $N-1$ 個地區,從地區 $2$ 開始,每數到第 $m$ 個地區就切斷它。 - 目標:找出最小的 $m$,使得地區 13 是最後一個被切斷的。 轉化成約瑟夫問題的形式: 首先由於地區 $1$ 都是第一個被切斷的,只需考慮 $N - 1$ 的地區即可,因此就變成長度為 $N - 1$ 的約瑟夫問題。 另外為方便計算,將地區給重新編號: - 地區 $2 \rightarrow 0$ - 地區 $3 \rightarrow 1$ - $\cdots$ - 地區 $13 \rightarrow 11$ 這樣就需要找到一個 $m$,使得在這 $N-1$ 個人的環中,最後被切斷的索引為 11。 約瑟夫問題公式:$$f(n, m) = \begin{cases} 0 & \text{if } n = 1 \\ (f(n-1, m) + m) \% n & \text{if } n > 1 \end{cases}$$ 範例程式碼: ```cpp= #include <iostream> using namespace std; int ans(int n, int k){ int s = 0; for (int i = 2; i <= n; ++i){ s = (s + k) % i; } return s; } int main(){ ios::sync_with_stdio(false), cin.tie(NULL); int N; while (cin >> N && N != 0){ int m = 1; while (true){ int last = ans(N - 1, m); if (last == 11){ cout << m << '\n'; break; } m++; } } } ``` ## Uva 245 - Uncompress PDF Source:https://onlinejudge.org/external/2/245.pdf Uva Online Judge:https://onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=181 Zerojudge:https://zerojudge.tw/ShowProblem?problemid=e569 題目翻譯: 有一種用來建立文檔壓縮版本的簡單方法,可用於不包含任何數字字元的檔案。此壓縮方法需要先建立一份「未壓縮檔中出現過的單字」清單。 當在未壓縮檔中遇到非字母字元時,就直接將該字元複製到壓縮檔中。 當在未壓縮檔中遇到一個單字時,只有在該單字第一次出現時,才會將它直接複製到壓縮檔;並在此情況下,把該單字放到清單的最前端。 若不是第一次出現,則不把該單字複製到壓縮檔;改為把它在清單中的位置(編號)寫入壓縮檔,並將該單字移到清單最前端。清單位置的編號從 1 開始。 請撰寫一個程式,輸入為一個壓縮檔,輸出為原本未壓縮檔的還原結果。 解題思路: 基本上就是按照題目要求做事。 1. 遇到單字時:直接印出該單字,把這個單字放到清單的最上面,原本清單裡的單字全部往後擠一位。 2. 遇到數字時(如 3):數字 3 表示到清單裡面去找第 3 個單字,接著把那單字印出來,最重要的是,用完之後要把單字移到清單最上面去。 3. 遇到標點符號或空格時:直接印出即可。 輸入方式可用 `cin.get(ch)`,其他方式有可能會將標點符號或空格給讀掉(例如 `sstream`)。 透過 `isalpha()` 判斷字元是否為字母,以及 `isdigit()` 判斷字元是否為數字。 演算法流程(供參): 1. 建立一個字串 `buffer` 以及單字清單 `wlist`。 2. 用 `cin.get(ch)` 一個一個讀入字元。 3. 判斷為字母 -> `buffer += ch`。 4. 判斷為數字 -> `buffer += ch`。 5. 以上皆非,則先判斷 `buffer` 是否為空。 6. `buffer` 非空 -> 表示裡面有單字或是數字之類的。 - 透過 `isalpha()` 判斷 `buffer` 開頭是否為字母,得知是否為單字。是的話直接印出,並 `wlist.insert(wlist.begin(), buffer)` 把單字放到最前面去。 - 若不是字母,就表示他是一個數字,可用 `stoi(buffer)` 轉整數,判斷是否為 0,是的話就結束,否則繼續。 - 若要繼續,則印出 `wlist[數字 - 1]` 這個單字,接著把它刪了。 `wlist.erase(wlist.begin() + (數字 - 1))` ,然後再把單字插入到最前面。 7. 上述流程跑完後清空 `buffer`。 8. 若上述沒有一個條件成立,就表示是標點符號或空格,直接輸出即可:`cout << ch`。 範例程式碼: ```cpp= #include <bits/stdc++.h> using namespace std; int main(){ ios::sync_with_stdio(false), cin.tie(nullptr); vector <string> wlist; char ch; string buffer = ""; while (cin.get(ch)){ if (isalpha(ch)){ buffer += ch; } else if (isdigit(ch)){ buffer += ch; } else{ if (!buffer.empty()){ if (isalpha(buffer[0])){ cout << buffer; wlist.insert(wlist.begin(), buffer); } else{ int index = stoi(buffer); if (index == 0) return 0; string s = wlist[index - 1]; cout << s; wlist.erase(wlist.begin() + (index - 1)); wlist.insert(wlist.begin(), s); } buffer = ""; } cout << ch; } } } ``` ## Uva 255 - Correct Move PDF Source:https://onlinejudge.org/external/2/255.pdf Uva Online Judge:https://onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=191 Zerojudge:https://zerojudge.tw/ShowProblem?problemid=e601 題目翻譯: 我們有一個 64 個格子的正方形棋盤,從 0 到 63 編號,如圖 1 所示。 有兩個棋子:國王和皇后。國王所在格子與皇后所在格子這一對位置稱之為「國家」(state)。 - 如果兩個棋子不在同一個格子上,則該狀態是合法的(legal)。 - 國王與皇后輪流移動。 - 國王可以在水平方向或垂直方向走一步,只要它不會走到皇后所在的位置。 - 皇后可以在水平方向或垂直方向走一步或多步,只要它在移動途中不會碰到國王。 - 所有這些移動都稱為合法移動(legal)。 - 請注意,棋子不能沿對角線移動。 ![image](https://hackmd.io/_uploads/r1_neOEIbl.png) 圖 1。 例如,假設我們有一個狀態:國王在第 17 格、皇后在第 49 格,如圖 2 所示。 國王的合法走法可以到第 9、16、18、25 格;而皇后合法地可以走到第 25、33、41、48、50、51、52、53、54、55 或 57 格。 不過,我們另外加上一個額外限制:棋子不可以移動到「另一個棋子也能移動到」的位置。 滿足這個限制的合法走法稱為「允許」(allowed)的走法。 在圖 2 中,國王與皇后各自透過一個允許走法所能到達的所有位置,分別用空心圓(○)與實心點(●)表示。 在圖 3 中,國王不能移動,它被困住了(locked in)。 ![image](https://hackmd.io/_uploads/HkjeZdEUbe.png) 圖 2。 ![image](https://hackmd.io/_uploads/HyVfWONU-l.png) 圖 3。 此問題要你寫一個程式,對女王的移動進行一些檢查。 解題思路: 一個 $8 \times 8$ 棋盤,可以用以下的方法來做表示: - `r = pos / 8` - `c = pos % 8` 接著就是去設計每一個條件的程式: 1. Illegal state:當 `k == q`,國王跟皇后在同一格,表示狀態不合法。 2. Illegal move:皇后走法不合法。 - `q == q2` 皇后都沒動,違反「至少動一步」的規則。 - 判斷是否同列(`qr == nr`)、同行(`qc == nc`),若皆非表示是斜著走,因此也不合法。 - 若同列或同行,沿移動方向(如 `step = (nr > qr ? 1 : -1)`)一格一格掃過路徑(從下一格開始,含終點),若掃到 `k` 表示皇后會碰到國王,因此也不合法。 3. Move not allowed:不能走到國王能走的格子。 - 若皇后新位置 q2 恰好是國王從 k 出發「上下左右一步」能到的格(即曼哈頓距離為 1),則輸出 Move not allowed。 4. Continue / Stop:當皇后成功走到 q2(而且是 allowed)後,輪到國王行動,此時要判斷國王是否至少有一個 allowed 的一步。 - 國王能走的只有四步 `(-1,0), (1,0), (0,-1), (0,1)`,因此窮舉這幾步。(`r = kr + d[0], c = kc + d[1]`) - 建立 `k2 = r*8 + c` 表示新狀態的國王。 1. 檢查 k2 是否出界。 2. 檢查是否 `k2 == q2` 3. 檢查 `k2` 是否為皇后在 `q2` 的位置可以走到的格(用 `queen_attacks(q2, k, k2)` 判斷) - 皇后能攻擊/到達的格:同列或同行,且從 `q2` 走到該格的路徑上不能先撞到國王(國王會擋住皇后的直線)。 5. 若上述條件皆成立,表示 `Stop` 國王被卡死了,否則 `Continue` 國王可繼續走。 範例程式碼: ```cpp= #include <bits/stdc++.h> using namespace std; bool queen_attacks(int qpos, int kpos, int target){ if (target == qpos) return false; int qr = qpos / 8, qc = qpos % 8; int tr = target / 8, tc = target % 8; if (qr == tr){ int step = (tc > qc ? 1 : -1); for (int c = qc + step; c != tc + step; c += step){ int cur = qr * 8 + c; if (cur == kpos) return false; if (cur == target) return true; } } else if (qc == tc){ int step = (tr > qr ? 1 : -1); for (int r = qr + step; r != tr + step; r += step){ int cur = r * 8 + qc; if (cur == kpos) return false; if (cur == target) return true; } } return false; } int main(){ ios::sync_with_stdio(false), cin.tie(nullptr); int k, q, q2; while (cin >> k >> q >> q2){ if (k == q){ cout << "Illegal state\n"; continue; } int kr = k / 8, kc = k % 8; int qr = q / 8, qc = q % 8; int nr = q2 / 8, nc = q2 % 8; if (q == q2){ cout << "Illegal move\n"; continue; } bool illegal = false; if (qr == nr){ int step = (nc > qc ? 1 : -1); for (int c = qc + step; c != nc + step; c += step){ if (qr == kr && c == kc) illegal = true; } } else if (qc == nc){ int step = (nr > qr ? 1 : -1); for (int r = qr + step; r != nr + step; r += step) if (qc == kc && r == kr) illegal = true; } else{ illegal = true; } if (illegal){ cout << "Illegal move\n"; continue; } if (abs(kr - nr) + abs(kc - nc) == 1){ cout << "Move not allowed\n"; continue; } bool allowed = false; int dir[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; for (auto &d : dir){ int r = kr + d[0], c = kc + d[1]; if (r < 0 || r >= 8 || c < 0 || c >= 8) continue; int k2 = r*8 + c; if (k2 == q2) continue; if (queen_attacks(q2, k, k2)) continue; allowed = true; break; } cout << (allowed ? "Continue\n" : "Stop\n"); } } ``` ## Uva 294 - Divisors PDF Source:https://onlinejudge.org/external/2/294.pdf Uva Online Judge:https://onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=230 Zerojudge:https://zerojudge.tw/ShowProblem?problemid=d366 題目翻譯: 數學家喜歡研究各種數字的奇特性質。 例如,他們認為 945 是個有趣的數,因為它是第一個其因數總和大於它本身的奇數。 為了幫助他們尋找有趣的數字,你要撰寫一個程式,掃描一段數字範圍,並找出在該範圍內擁有最多因數的數字。 不幸的是,數字本身的大小以及範圍的大小,使得過於簡單的做法可能需要花太多時間才能跑完。 因此,請確保你的演算法足夠聰明,能在短短幾秒內處理最大的可能範圍。 筆者註:Divisors 稱為除數,也稱為我們最熟悉的因數。 解題思路: 要最快計算一個數字 $n$ 的因數個數,最快的方法是使用「質因數分解」(Prime Factorization)。 假設 $n$ 的標準分解式為:$$n = p_1^{a_1} \times p_2^{a_2} \times \dots \times p_k^{a_k}$$ 則 $n$ 的因數個數 $d(n)$ 為:$$d(n) = (a_1 + 1) \times (a_2 + 1) \times \dots \times (a_k + 1)$$ 個數就是拿原本質因數的指數來做計算,至於 + 1 部分,則是表示指數的選法(0 ~ $a_k$)。 解題步驟: 1. 建立質數表(用埃拉托斯特尼篩法, Sieve of Eratosthenes):因為 $U$ 最大為 $10^9$,只需測到 $\sqrt{10^9} \approx 31622$ 的質數即可,先預先篩選出 $1$ 到 $32000$ 之間的所有質數。 2. 區間掃描:對每組測資 $L$ 和 $U$,遍歷 $i$ 從 $L$ 到 $U$。 3. 計算因數個數:對區間內的每個數字 $i$: - 用預存的質數表進行試除。 - 統計每個質因數的冪次(exponent)。 - 套用公式計算總因數個數。 - 若試除完所有 $\le \sqrt{i}$ 的質數後,$i$ 仍然大於 1,代表剩下的 $i$ 本身就是一個大質數,因數個數需再乘以 $(1+1)=2$。 4. 找出最大值:若因數個數相同,題目要求輸出較小的那個數。 範例程式碼: 質數篩法優化部分: ```cpp= for (int i = p * p; i <= MAXSQRT; i += p) is_prime[i] = false; ``` 這段優化目的在於把目前質數 $p$ 的所有倍數都標記為非質數(false)。 `i += p` ($p$ 的倍數)即當找到質數 $p$ 後,刪掉所有$p$ 的倍數:$2p, 3p, 4p, \dots$ 等等。 這樣做的想法是如果 $i$ 是 $p$ 的倍數(且 $i > p$),那 $i$ 肯定不是質數,因為能被 $p$ 整除。 `int i = p * p`:在上層迴圈就將比 $p^2$ 小的倍數都篩選過了,因此到這邊從 $p^2$ 開始是合理的,不用從頭到尾遍歷一次,縮短耗時。 ```cpp= #include <bits/stdc++.h> using namespace std; const int MAXSQRT = 32000; vector <int> prime; // 埃拉托斯特尼篩法 void sieve(){ vector <bool> is_prime(MAXSQRT + 1, true); is_prime[0] = is_prime[1] = false; for (int p = 2; p * p <= MAXSQRT; ++p){ if (is_prime[p]){ for (int i = p * p; i <= MAXSQRT; i += p) is_prime[i] = false; } } for (int p = 2; p <= MAXSQRT; ++p) if (is_prime[p]) prime.push_back(p); } // 數有幾個因數 int countDivisor(int n){ int total = 1; for (int p : prime){ // 若質數平方大於 n 就不用再除 if (p * p > n) break; if (n % p == 0){ int count = 0; while (n % p == 0){ count ++; n /= p; } // 當 n 無法被 p 整除時 // count 即為 p 的指數 total *= (count + 1); } } if (n > 1) total *= 2; return total; } int main(){ ios::sync_with_stdio(false), cin.tie(nullptr); sieve(); int T; cin >> T; while (T--){ int L, U; cin >> L >> U; int max_num = -1; int max_num_count = -1; for (int i = L; i <= U; ++i){ int d = countDivisor(i); if (d > max_num_count){ max_num_count = d; max_num = i; } } cout << "Between " << L << " and " << U << ", " << max_num << " has a maximum of " << max_num_count << " divisors.\n"; } } ``` ## Uva 337 - Interpreting Control Sequences PDF Source:https://onlinejudge.org/external/3/337.pdf Uva Online Judge:https://onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=273 No Zerojudge 題目翻譯: 幾乎所有的文字模式終端(text-mode terminal)都是專用電腦系統,包含一個序列埠(serial port,用於與數據機或其他電腦系統通訊)、鍵盤、陰極射線管顯示器(CRT,cathode-ray tube),當然還有一顆微處理器(microprocessor)、一些隨機存取記憶體(RAM)與存放控制程式的唯讀記憶體(ROM)。 當一個字元從鍵盤或序列埠送到終端時,終端的軟體會將它分類為要顯示在 CRT 上的顯示字元(display character),或是引入控制序列(control sequence)的字元。控制序列用來指示終端執行特殊動作,例如清除螢幕、以指定方式移動游標,或改變字型等。 在此題中,假設你正在為一個小型終端撰寫軟體,該終端有一個 10 行 × 10 列的顯示(例如用於銷售點終端,point-of-sale terminal)。行(row)與列(column)編號皆為 0 到 9。引入控制序列的字元為「∧」(circumflex,插入符號)。緊接在該控制序列引導字元之後的一個字元(或在一種情況下的兩個字元)會指示你的軟體執行特殊功能。以下為你需要解讀的完整控制序列清單: ∧b 將游標移到當前行的開頭;游標的行(row)不變。 ∧c 清除整個螢幕;游標的行與列皆不變。 ∧d 若可能,將游標下移一行;游標的列(column)不變。 ∧e 擦除游標所在行中游標列及其右側的字元;游標的行與列皆不變。 ∧h 將游標移到第 0 行、第 0 列;螢幕內容不改變。 ∧i 進入插入模式(insert mode,見下)。 ∧l 若可能,將游標左移一列;游標的行不變。 ∧o 進入覆寫模式(overwrite mode,見下)。 ∧r 若可能,將游標右移一列;游標的行不變。 ∧u 若可能,將游標上移一行;游標的列不變。 ∧∧ 在目前游標位置寫出一個脫字符「∧」,其行為與該字元並非特殊字元時相同;此寫入仍受目前模式(插入或覆寫)的影響。 ∧## 將游標移到所指定的行與列;# 代表一個十進位數字;第一個 # 表示新的行號,第二個 # 表示新的列號。 系統保證不會送來非法的控制序列。游標不得移出允許的螢幕位置範圍(亦即介於第 0 行第 0 列到第 9 行第 9 列之間)。 當一個一般字元(非控制序列的一部分)送到終端時,其在螢幕上的顯示方式取決於終端的模式。 當終端處於覆寫模式(開機時即為覆寫模式)時,接收到的字元會取代游標位置處的字元。 若處於插入模式時,游標位置及其右方的字元會整體向右移一列,然後將新字元放在游標位置;游標所在行中最右邊的字元會因此遺失。 無論是哪一種模式,游標都會在可能的情況下向右移一列。 **Input** 輸入資料將包含多組對你終端軟體的測試。 每一組測試以一行整數 N 開始,接下來會有 N 行資料;這些資料中的每一個字元,都必須依照讀取的順序,視同輸入到你的終端軟體中來處理。 輸入資料中不會包含定位字元(tab),而輸入中的換行符號必須被忽略。 請注意,輸入資料中的空白字元(blank)是一般字元,必須顯示在終端螢幕上。 最後一組測試之後,會接著出現一行只包含整數 0 的資料,表示輸入結束。 任何控制序列都不會被拆分到兩行輸入資料之中。 在每一個測資開始時,請假設終端螢幕是清空的(也就是整個畫面都填滿空白字元)、終端處於覆寫模式(overwrite mode),且游標位於螢幕的第 0 行、第 0 列。 **Output** 對於每一個輸入測資,請輸出一行,包含該測資的編號(自 1 起依序編號),以及在完成該測資所有資料處理後,螢幕最終呈現的畫面內容。請將螢幕畫面以一個「方框」包圍;所需的輸出格式請參考下方範例說明。 **Sample Input** ``` 7 This is bad^h^c ^05^^ ^14/ \^d^b / \ ^u^d^d^l^l^l^l^l^l^l^l^l ^r^r< ACM >^l^l^d/^b \ ^b^d \ / ^d^l^lv 7 ^i9^l8^l7^l6^l5^l4^l3^l2^l1^l0 ^o^d^lThis is #1^d^bThis is #2 ^d^bThis is #3^d^bThis is #4 ^d^bThis is #5^d^bThis is #6 ^d^bThis is #7^d^bThis is #8 ^i^d^bThis is #9^d^bThis is #10 ^54^e Hello^d^l^l^l^lWorld 0 ``` **Sample Output** ``` Case 1 +----------+ | ^ | | / \ | | / \ | | < ACM > | | \ / | | \ / | | v | | | | | | | +----------+ Case 2 +----------+ |0123456789| |This is #1| |This is #2| |This is #3| |This is #4| |This Hello| |This World| |This is #7| |This is #8| |This is #0| +----------+ ``` 解題思路: 這題其實沒有很難,就是題目長了點,基本上都按照題目的去做即可。 在思路上,可以先設定一個 `vector <string> scr(10, string(10, ' '))` ,表示一個 $10 \times 10$ 的螢幕畫面。 接著 `int r = 0, c = 0` 表示目前的游標(cursor)位置。 可以建立兩個函數:`putChar(ch)`、`clearScreen()`,把程式碼包裝起來,後續就不用全擠在一個 for 迴圈裡面。 範例程式碼: ```cpp= #include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false), cin.tie(nullptr); int N, test_case = 1; while (cin >> N && N != 0) { cin.ignore(); vector<string> scr(10, string(10, ' ')); int r = 0, c = 0; bool insertMode = false; // [&](char ch) 匿名函數 // putChar 本身就會是一個函數 // clearScreen 也一樣 // [&] 即為捕捉到的變數以 reference 的方式傳遞 auto putChar = [&](char ch){ if (!insertMode){ scr[r][c] = ch; } else{ // 進入插入模式 // 在位置 (r, c) 做插入的話 // 其位置後續字元都要往後挪移 // 因而用 for(int j = 9; j > c; --j) // e.g. : abcdefg, 插入 x -> abcdxefg // x 插入在其中, efg 要往後挪移 for (int j = 9; j > c; --j) scr[r][j] = scr[r][j - 1]; scr[r][c] = ch; } if (c < 9) c++; // 邊界檢查 }; auto clearScreen = [&]() { // assign 為 string 標頭檔的方法 // row.assign(10, ' ') // 即在字串 row 中, 用長度 10 的 ' ' 空白字元 // 覆蓋掉原本的內容 for (auto &row : scr) row.assign(10, ' '); }; for (int k = 0; k < N; ++k){ string line; getline(cin, line); for (int i = 0; i < line.size(); ++i){ char ch = line[i]; if (ch != '^'){ putChar(ch); continue; } char cmd = line[++i]; // ++i 取得 ^ 的下一個字元 if (cmd == '^'){ putChar('^'); } else if (isdigit(cmd)){ int nr = cmd - '0'; int nc = line[++i] - '0'; r = nr, c = nc; } else{ switch (cmd){ case 'b': c = 0; break; case 'c': clearScreen(); break; case 'd': if (r < 9) ++r; break; case 'e': for (int j = c; j < 10; ++j) scr[r][j] = ' '; break; case 'h': r = 0, c = 0; break; case 'i': insertMode = true; break; case 'l': if (c > 0) --c; break; case 'o': insertMode = false; break; case 'r': if (c < 9) ++c; break; case 'u': if (r > 0) --r; break; } } } } cout << "Case " << test_case++ << '\n'; cout << "+----------+\n"; for (int i = 0; i < 10; ++i) cout << "|" << scr[i] << "|\n"; cout << "+----------+\n"; } } ```