<style> .reveal .slides { font-size: 28px; } </style> # TPR 30 題解 based on SCIST 演算法練習賽 #1 ---- ![](https://hackmd.io/_uploads/Syh_ZxrSo.png) ---- ## 預期難度 ### A < B < C < D < E < F ---- ## 實際難度 ### B < A < C < D < E < F --- ## PA. CDC 的九宮格政策 原題發想:Ateto ---- ### 子任務 1(20 %) $n, m \le 1000, x = 1$ ---- - 每個人確診之後都只有自己需要隔離 - 答案就是 $n \times m$ ---- ### 子任務 2(50 %) $n, m \le 1000$ ---- - 假設最後需要 $a \times b$ 個人確診 - 使用圖解法,可以發現: - 當 $x \mid n$ 時,$a = \frac{n}{x}$ - 否則,$a = \frac{n}{x} + 1$ - $b$ 的算法也同理 ---- 所以我們可以得出一個結論: $(a, b) = (\lceil \frac{n}{x} \rceil, \lceil \frac{m}{x} \rceil)$ 至於算法的部分,你可以使用: - if 判斷式 - bool 轉 int - 數學解 ---- #### if 判斷式 ```cpp= int n, m, x, a, b; cin >> n >> m >> x; a = n / x, b = m / x; if (n % x) a++; if (m % x) b++; cout << a * b << '\n'; ``` ---- #### bool 轉 int ```cpp= int n, m, x, a, b; cin >> n >> m >> x; a = n / x + (n % x != 0); b = m / x + (m % x != 0); cout << a * b << '\n'; ``` ---- #### 數學解 ```cpp= int n, m, x; cin >> n >> m >> x; cout << ((n + x - 1) / x) * ((m + x - 1) / x) << '\n'; ``` ---- ### 子任務 3(30 %) 無額外限制 ---- - 注意到原題的範圍:$1 \le n, m, x \le 10^9$ - 因此最終答案會超過 32-bit int 所能儲存的範圍 - 需要使用 long long(64-bit int) ---- ```cpp= #include <iostream> #include <bits/stdc++.h> using namespace std; signed main() { long long n, m, x; cin >> n >> m >> x; cout << ((n + x - 1) / x) * ((m + x - 1) / x) << '\n'; return 0; } ``` --- ## PB. 必做題勾選 原題發想:fishhh ---- 這其實是一個很經典的賽局理論 - Nim game ---- 但是如何推導出必勝策略並不是我們希望考的 因此我們把策略都寫在了範測裡 ---- 只需要觀察到一個性質: 當 $3 \mid n$ 時,先手必輸,否則先手必贏 因此我們只需要寫一個判斷式就好了 ---- ```cpp= int n; cin >> n; if (n % 3) cout << "Yes" << '\n'; else cout << "No" << '\n'; ``` ---- 或是用更高級的三元運算子 ```cpp= int n; cin >> n; cout << (n % 3 ? "Yes" : "No") << '\n'; ``` ---- ```cpp= #include <iostream> using namespace std; int main() { int n; cin >> n; if (n % 3) cout << "Yes" << '\n'; else cout << "No" << '\n'; return 0; } ``` --- ## PC. 交換禮物 原題發想:fishhh ---- 給定每個人準備的禮物編號(編號不重複),求經過 $m$ 次交換後,有幾個人拿到的禮物是自己原本準備的 ---- ### 子任務 1(23 %) $a_i = i, m = 1$ ---- 紀錄 $p_{1, j} = j$ 的數量即可 ---- ### 子任務 2(11 %) $m = 1$ ---- 做法跟子任務 1 基本一致,不過由於 $a_i \neq i$,因此需要額外紀錄 $a_i$ 最終紀錄 $p_{1, j} = a_j$ 的數量即可 ---- ### 子任務 3(15 %) $a_i = i$ ---- ~~這個子任務其實沒什麼用~~ ---- - 開一個二維陣列 $gift[n][m]$,$gift[i][j]$ 代表第 $i$ 輪結束後第 $j$ 人的編號 - $gift[0][j]$ 代表最一開始的禮物編號 - 利用陣列模擬交換的過程 - 最後紀錄 $gift[m][j] = j$ 的數量 ---- ```cpp= #include <iostream> using namespace std; const int MAXN = 1005; int a[MAXN], gift[MAXN][MAXN]; int main() { int n, m; cin >> n >> m; for (int i = 1; i <= n; i++) { cin >> a[i]; gift[0][i] = a[i]; } for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { int p; cin >> p; gift[i][p] = gift[i - 1][j]; } } int ans = 0; for (int i = 1; i <= n; i++) { if (gift[m][i] == i) ans++; } cout << ans << '\n'; return 0; } ``` ---- ### 子任務 4(51 %) 無額外限制 ---- 作法基本上跟子任務 3 一樣,只是最後是用禮物編號來判斷 ---- ```cpp= #include <iostream> using namespace std; const int MAXN = 1005; int a[MAXN], gift[MAXN][MAXN]; int main() { int n, m; cin >> n >> m; for (int i = 1; i <= n; i++) { cin >> a[i]; gift[0][i] = a[i]; } for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { int p; cin >> p; gift[i][p] = gift[i - 1][j]; } } int ans = 0; for (int i = 1; i <= n; i++) { if (gift[m][i] == a[i]) ans++; } cout << ans << '\n'; return 0; } ``` ---- ### 補充技巧:滾動陣列 ---- - 可以發現到,第 $i$ 次操作其實只跟第 $i-1$ 次操作的結果有關 - 所以可以不用開到 $n \times m$ 的陣列 - 只需要紀錄第 $i-1, i$ 次操作的結果即可 - 維護 $gift[0][j]$ 為上一次操作的結果,$gift[1][j]$ 為這一次 - 操作完之後再將 $gift[1][j]$ 的內容搬到 $gift[0][j]$ 即可 - 這個技巧可以讓陣列大小幾乎少了一維 ---- ```cpp= #include <iostream> using namespace std; const int MAXN = 1005; int a[MAXN], gift[2][MAXN]; int main() { int n, m; cin >> n >> m; for (int i = 1; i <= n; i++) { cin >> a[i]; gift[0][i] = a[i]; } for (int i = 0; i < m; i++) { for (int j = 1; j <= n; j++) { int p; cin >> p; gift[1][p] = gift[0][j]; } for (int j = 1; j <= n; j++) gift[0][j] = gift[1][j]; } int ans = 0; for (int i = 1; i <= n; i++) { if (gift[0][i] == a[i]) ans++; } cout << ans << '\n'; return 0; } ``` ---- ### 小趣事 其實輸入的第一行根本沒有用 hehe 剛剛電老鼠提醒我才發現的 orz --- ## PD. 恐龍語言分析 原題發想:fishhh ---- 主要有三個步驟: 1. 將大寫轉換為小寫 2. 去除多餘的字母 3. 計算 yee 的次數 不過 2、3 其實是可以合併進行的 ---- #### 將大寫轉換為小寫 - 可以使用 `isupper(c)` 函式,或是使用 ascii 碼判斷是否為大寫(`'A' <= c && c <= 'Z'`) - 轉為小寫可以使用 `tolower(c)`,或是 `c = 'a' + c - 'A'` ---- #### 計算答案 - 由左而右一個一個掃過去 - 用類似"收集"的概念,紀錄目前收集到 "yee" 中的第幾個了 - 如果符合下一個要收集的,就記錄下來 - 當三個字元都收集完之後,答案 +1、重新記錄收集到的字元 ---- - 有不少人都以為是連續的子字串才算 - 但其實不是,根據題目提到的 "過濾",其實要求的是子序列 ---- ```cpp= #include <iostream> #include <string> using namespace std; int main() { string s; cin >> s; int ans = 0, now = 0; string tar = "yee"; for (int i = 0; i < s.size(); i++) { if (isupper(s[i])) // if ('A' <= s[i] && s[i] <= 'Z') s[i] = tolower(s[i]); // s[i] = s[i] - 'A' + 'a' now += (s[i] == tar[now]); ans += now / 3; now %= 3; } cout << ans << endl; return 0; } ``` --- ## PE. 取餘遊戲 原題發想:Jun-an ---- ~~這題其實是數學題~~ ---- ### 子任務 1(15 %) $b = 0$ ~~這個子任務其實沒有意義~~ ---- ### 子任務 2(20 %) $a = 2, P = 10$ ---- 紀錄 $2^1 \sim 2^4 \mod P$,如果洽有一個為 $b$,代表有答案,否則輸出 $-1$ ---- ### 子任務 3(20 \%) $a = 3, P = 10$ ---- 做法跟子任務 2 一樣,就不多說了 ---- ### 子任務 4(45 %) 無額外限制 ---- 這部分就要有一些數論的知識了 ---- 首先我們要先了解幾個性質: 1. $a \mod P$ 至多只會有 $P$ 個餘數 2. $(a \times b) \mod P = ((a \mod P) \times (b \mod P) )\mod P$ 根據以上兩個性質,我們可以得出一個結論: **對於所有$1 \le i \le P, i \in \mathbb{N}^+$ 以內,a^i 一定會循環** ---- 意思就是,我們只需要找 $a^1 \sim a^P$,如果沒找到的話,答案就是 $-1$ 有了這個結論,就很簡單了 ---- ```cpp= #include <iostream> using namespace std; int main() { int a, b, P; cin >> a >> b >> P; int tmp = 1; bool find_ans = false; for (int i = 1; i <= P; i++) { tmp = (tmp * a) % P; if (tmp == b) { cout << i << '\n'; find_ans = true; break; } } if (!find_ans) cout << -1 << '\n'; return 0; } ``` --- ## PF. 一個二維迷宮 原題發想:Koying ---- 很明顯的實作題(O ---- ### 子任務 1(5 %) $n \le 750, m = 0$ ---- - 當沒有任何道具時,唯一的路徑就是按照順序走 - 所以答案就是 $n \times n$ ---- ### 子任務 2(20 \%) $n \le 750$ ---- 主要會有兩大實作重點: 1. 建立地圖(每格的編號) 2. 走迷宮 ---- #### 建立地圖 - 我們可以先建立一個二維陣列,分別為往下、左、上、右走一格的移動量 ```cpp= int p[4][2] = { {1, 0}, {0, -1}, {-1, 0}, {0, 1} }; ``` - 接著一直重複下左上右的步驟,每次遇到邊界或是有標過的格子就轉彎,直到標到 $n \times n$ 格為止 - 如此一來就可以將每一格標上編號了! ---- ```cpp= int now = 0; int x = -1, y = n - 1; while (now < n * n) { for (int i = 0; i < 4 && now < n * n; i++) { while (1) { int tmpx = x + p[i][0], tmpy = y + p[i][1]; if (tmpx >= 0 && tmpx < n && tmpy >= 0 && tmpy < n && !maze[tmpx][tmpy]) { x = tmpx; y = tmpy; now++; maze[x][y] = now; } else break; } } } ``` ---- #### 走迷宮 - 從起點開始,每次往四個方位搜索 - 接著有兩種情況 - 沒道具:往編號為目前編號 +1 的格子走 - 有道具:往最大編號的格子走 - 走的同時記錄格數,就完成了! ---- ```cpp= int ans = 0; x = 0, y = n - 1; while (maze[x][y] != n * n) { ans++; int mx = 0, pos = 0; for (int i = 0; i < 4; i++) { int tmpx = x + p[i][0], tmpy = y + p[i][1]; if (tmpx >= 0 && tmpx < n && tmpy >= 0 && tmpy < n) { if ((maze[tmpx][tmpy] > mx && spe[maze[x][y]]) || (maze[tmpx][tmpy] == maze[x][y] + 1 && maze[tmpx][tmpy] > mx)) { mx = maze[tmpx][tmpy]; pos = i; } } } x += p[pos][0]; y += p[pos][1]; } ``` ---- ### 子任務 3(15 %) $m = 0$ ---- 一樣輸出 $n \times n$ 就好了 ---- ### 子任務 4(60 %) ---- 做法跟子任務 2 一模一樣 只是範圍比較大 ---- #### 為什麼只是範圍有差還要另外出一個子任務? ---- - 其實這並不是為了要卡你們 TLE - 如果你將陣列開在了 main 裡面,應該會得到 RE 的結果 - 這是因為區域變數會有大小限制 - 如果開在全域就沒問題了! - 因為這個問題在各大比賽都可能會遇到,所以特別設計了這個範圍 ---- 完整程式碼: ```cpp= #include <iostream> using namespace std; const int MAXN = 7505; int maze[MAXN][MAXN]; bool spe[MAXN * MAXN]; int p[4][2] = { {1, 0}, {0, -1}, {-1, 0}, {0, 1} }; signed main() { ios::sync_with_stdio(0), cin.tie(0); int n, m; cin >> n >> m; for (int i = 0; i < m; i++) { int k; cin >> k; spe[k] = true; } int now = 0; int x = -1, y = n - 1; while (now < n * n) { for (int i = 0; i < 4 && now < n * n; i++) { while (1) { int tmpx = x + p[i][0], tmpy = y + p[i][1]; if (tmpx >= 0 && tmpx < n && tmpy >= 0 && tmpy < n && !maze[tmpx][tmpy]) { x = tmpx; y = tmpy; now++; maze[x][y] = now; } else break; } } } int ans = 0; x = 0, y = n - 1; while (maze[x][y] != n * n) { ans++; int mx = 0, pos = 0; for (int i = 0; i < 4; i++) { int tmpx = x + p[i][0], tmpy = y + p[i][1]; if (tmpx >= 0 && tmpx < n && tmpy >= 0 && tmpy < n) { if ((maze[tmpx][tmpy] > mx && spe[maze[x][y]]) || (maze[tmpx][tmpy] == maze[x][y] + 1 && maze[tmpx][tmpy] > mx)) { mx = maze[tmpx][tmpy]; pos = i; } } } x += p[pos][0]; y += p[pos][1]; } cout << ans + 1 << endl; return 0; } ``` --- ## 總結 ---- - 題目好像有點太難了 \\|/ - 大家都不怎麼撈部分分 QQ - 其實題目都有一些關鍵性質,如果能觀察到就會變很簡單 - 還有一堆人都忘記看題目範圍,沒開 long long - 希望大家聽完題解有豁然開朗,也希望有學到新東西 >< - 為了讓我們可以辦更好的比賽,請幫我們填寫回饋表單 - https://forms.gle/qGH4BTQUStVzyp2a7 ---- ### 程式碼釋出 本次比賽的所有程式碼(官解、生成器)都會放在 GitHub 上 歡迎大家參考(~~也可以幫我們案一顆星星~~) 連結:https://github.com/NHDK-Ten-Point-Round/ten-point-round-30
{"metaMigratedAt":"2023-06-17T13:38:00.078Z","metaMigratedFrom":"YAML","title":"TPR 30 題解","breaks":true,"slideOptions":"{\"transition\":\"slide\",\"transitionSpeed\":\"fast\"}","contributors":"[{\"id\":\"73093035-99f8-4a9b-b033-c17f49e2d490\",\"add\":10495,\"del\":40}]"}
    834 views
   Owned this note