--- tags: 成大高階競技程式設計 2021 image: https://i.imgur.com/IIj9BcL.png --- # 2021 高階競技程式設計課前賽 - 題解 ## [政大的女朋友](https://judge.csie.ncku.edu.tw/problems/79) - Task Provider: smallbird - Task Setter: leo ### 首殺 pmcat (00:03) ### 解法說明 #### 解法 $1$ 因為 `+` 加上 `+` 會變成 `-`,而 `-` 的絕對值很小,代表影響很小, 因此只要 `+` 的數量是奇數的話,算出來的答案就會是正數,反之亦然, 所以只要計算 `+` 的數量就可以知道答案了。 #### 解法 $2$ 我們無法從輸入得知 `-` 到底是 $-1$, $-2$ 還是 $-3$, `+` 也是一樣情況,所以不管是哪一種,答案都應該相同, 因此我們可以把 `+` 當作 $2^{31}-1$,把 `-` 當作 $-1$, 直接做計算,不過在 C++ 中,有號數溢位是 [Undefined Behavior](https://en.wikipedia.org/wiki/Undefined_behavior), 因此需先轉為無號數進行二進制加法,再將結果轉為有號數。 > Unsigned integer arithmetic is always performed modulo $2^n$ where $n$ is the number of bits in that particular integer. > When signed integer arithmetic operation overflows (the result does not fit in the result type), the behavior is undefined.[$^{[Arithmetic\ operators]}$](https://en.cppreference.com/w/cpp/language/operator_arithmetic) 而無號數加有號數,有號數會被隱性轉型為無號數。 > If the unsigned operand's conversion rank is greater or equal to the conversion rank of the signed operand, the signed operand is converted to the unsigned operand's type.[$^{[usual\ arithmetic\ conversions]}$](https://en.cppreference.com/w/cpp/language/operator_arithmetic#Conversions) ### 標準程式 #### 解法 $1$ :::spoiler ```cpp! #include <iostream> using namespace std; int main() { int cnt = 0; char a, b, c; cin >> a >> b >> c; if (a == '+') ++cnt; if (b == '+') ++cnt; if (c == '+') ++cnt; if (cnt == 1 || cnt == 3) cout << "yes\n"; else cout << "no\n"; } ``` ::: #### 解法 $2$ :::spoiler ```cpp! #include <iostream> using namespace std; int main() { unsigned res = 0; char a, b, c; cin >> a >> b >> c; if (a == '+') res += 2147483647; else res += -1; if (b == '+') res += 2147483647; else res += -1; if (c == '+') res += 2147483647; else res += -1; if (int(res) > 0) cout << "yes\n"; else cout << "no\n"; } ``` ::: ## [整理房間](https://judge.csie.ncku.edu.tw/problems/80) - Task Provider: D4nnyLee - Task Setter: D4nnyLee ### 首殺 anderson425 (00:04) ### 解法說明 #### Subtask 1 從限制可以看到 $1 \leq x_i \leq 5$,代表可以分別定義 $5$ 個變數分別紀錄 $x_i = 1$ 有幾個、$x_i = 2$ 有幾個,以此類推。 輸出的時候只要先輸出這 $5$ 個變數的值,然後再輸出 $95$ 個 $0$ 就可以通過 Subtask 1 了。 > 因為 $x_i = 6$ 以後的都不可能出現,所以後面的必定都為 $0$。 :::spoiler 參考程式 ```cpp #include <iostream> using namespace std; int main(){ int n, a = 0, b = 0, c = 0, d = 0, e = 0, x; cin >> n; while(n--){ cin >> x; if(x == 1) a++; else if(x == 2) b++; else if(x == 3) c++; else if(x == 4) d++; else if(x == 5) e++; } cout << a << " " << b << " " << c << " " << d << " " << e; for(int i = 5; i < 100; i++) cout << " 0"; cout << "\n"; } ``` ::: #### Subtask 2 現在 $x_i$ 的最大值從 $5$ 增加到 $100$ 了,如果要延續 Subtask 1 的作法, 從 $5$ 個變數增加到 $100$ 個變數,且多增加許多 `if` 條件判斷,也是可以通過。 但是這個方法光是要一直複製貼上就很累人,因此可以用陣列來簡化那些繁瑣的程式碼。 宣告一個長度為 $100$ 的陣列 `arr`,而 `arr` 的每一個元素都代表其對應的箱子裡面有多少物品。 每次讀到一個 $x_i$ 的時候就將對應的元素加 $1$,最後再將陣列輸出就可以拿到滿分了。 > 我的實作方式是將 `arr[i]` 對應到第 $i + 1$ 個箱子, > 因為題目的箱子編號是從 $1$ 開始,而陣列的索引值是從 $0$ 開始。 ### 標準程式 :::spoiler ```cpp #include <bits/stdc++.h> using namespace std; int main(void) { int n; cin >> n; array<int, 100> arr{}; for (int i = 0; i < n; ++i) { int x; cin >> x; ++arr[x - 1]; } for (int i = 0; i < 100; ++i) cout << arr[i] << ' '; cout << endl; return 0; } ``` ::: ## [石頭](https://judge.csie.ncku.edu.tw/problems/81) - Task Provider: ys - Task Setter: ys ### 首殺 ouob (00:02) ### 解法說明 一開始不曉得偉杰有幾顆石頭,於是假設偉杰目前擁有 $0$ 顆石頭 > 因為在還沒做任何操作前,偉杰**最少**可以有 $0$ 顆 而操作過程中若偉杰只有 $0$ 顆石頭,但還是要拿走 $1$ 顆石頭的話 則這個操作就是不符合規則的操作,需要假設偉杰一開始的石頭要**再多一顆**: ```cpp if(cnt < 0) cnt++; ``` 舉例來說,若操作為 `--+` 設偉杰一開始有 $x = 0$ 顆石頭,則目前有 $y = 0$ 顆石頭 下列 1、2、3 分別表示 `--+` 各個操作: 1. 由於 $y = 0$ 所以要假設 $x = 1$,則 $y = 1$。在這一步減少 $1$ 顆石頭,$y$ 變為 $0$ 2. 由於 $y = 0$ 所以要假設 $x = 2$,則 $y = 1$。在這一步減少 $1$ 顆石頭,$y$ 變為 $0$ 3. 在這一步增加 $1$ 顆石頭,$y$ 變為 $1$ ### 標準程式 :::spoiler ```cpp #include<bits/stdc++.h> using namespace std; int n; string s; int main() { cin >> n >> s; int cnt = 0; for(int i = 0; i < n; i++) { if(s[i] == '+') cnt++; if(s[i] == '-') cnt--; if(cnt < 0) cnt++; } cout << cnt; return 0; } ``` ::: ## [天啟](https://judge.csie.ncku.edu.tw/problems/82) - Task Provider: arasHi - Task Setter: arasHi ### 首殺 p76094795 (00:11) ### 解法說明 #### Subtask 1 $O(\max(a,b))$ 此子任務只要照著題目敘述實作即可得分。 :::spoiler 參考程式 ```cpp #include <iostream> using namespace std; int main() { int a, b; cin >> a >> b; while(a && b){ if(a < b) swap(a, b); a -= b; } cout << max(a, b) << "\n"; } ``` ::: #### Subtask 2 $O(\log(\max(a,b)))$ <font class="focus">請注意:這個子任務的變數範圍會超出 int 範圍, 請使用 long long 儲存數值。</font> 假設 $a>b$ 觀察一下會發現,大的數字重複減去小的數, 一直重複這個動作的話會有以下兩種可能: 1. $a=0$ 2. $a\neq0,$$a$ 會被減到小於 $b$ 若是 1. 的話則得到題目所需的答案 若是 2. 的話則將 $a,b$ 對調並繼續執行相減的操作 若一開始 $a<b$ 的話,則不會執行任何減法, 會直接導到 2. ,因此會對調 $a,b$ 後繼續執行減法, 可以得知一開始 $a,b$ 相對關係不影響此作法正確性。 你如果直接將此作法送上去會得到一個 <font color="#3498db">TLE</font>, 因為如果 $a,b$ 分別為 $1$ 與 $10^{18}$ , 則該作法會花費很長的時間執行, 因此有一個方法可以加速相減的過程。 當你要重複執行 $a-b$ 直到 $a<b$ 時, 則該操作會等價於 $a$ 對 $b$ 取模, 也就是 $a\div b$ 的餘數,在 C++ 可以用 `a % b` 得到。 如此一來就可以在很短的時間內得到答案。 此做法即為[輾轉相除法](https://zh.wikipedia.org/zh-tw/%E8%BC%BE%E8%BD%89%E7%9B%B8%E9%99%A4%E6%B3%95), 而輾轉相除法可以求出兩個數的最大公因數(GCD), 因此做出這題的各位其實已經會求兩數的最大公因數了喔。 ### 標準程式 :::spoiler ```cpp #include <iostream> using namespace std; int main() { long long a, b, t; cin >> a >> b; while(b){ t = a % b; a = b; b = t; } cout << a << "\n"; } ``` ::: ## [考試星球](https://judge.csie.ncku.edu.tw/problems/83) - Task Provider: XDEv11 - Task Setter: leo ### 首殺 felixchao (00:47) ### 解法說明 #### Subtask 1 $O(n^2)$ 對於每個人,檢查是否表現得比其他人都好,複雜度為 $O(n^2)$。 :::spoiler 參考程式 ```cpp! #include <iostream> #include <vector> #include <array> #include <string> using namespace std; void solve() { int n; cin >> n; vector<array<int, 3>> v(n); for (auto& [x1, x2, x3] : v) cin >> x1 >> x2 >> x3; for (int i{0}; i < n; ++i) { bool flag{true}; for (int j{0}; j < n; ++j) { if (j == i) continue; int w{0}; for (int k{0}; k < 3; ++k) if (v[i][k] > v[j][k]) ++w; if (w < 2) { flag = false; break; } } if (flag) { cout << i + 1 << '\n'; return ; } } cout << "-1\n"s; } int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); int t{1}; //cin >> t; while (t--) solve(); return 0; } ``` ::: #### Subtask 2 $O(n)$ 因為最多只有一個人有機會表現得比其他人都好, 可以先兩兩進行比較,淘汰掉一個人,再找下一個比較, $n$ 次後可淘汰掉 $n-1$ 個人,剩下的一個人只需要再與全部人比較一次即可, 複雜度為 $O(n)$。 ### 標準程式 :::spoiler ```cpp! #include <iostream> #include <vector> #include <array> using namespace std; void solve() { int n; cin >> n; vector<array<int, 3>> v(n); for (auto& x : v) cin >> x[0] >> x[1] >> x[2]; int cand{0}; // candidate for (int i{0}; i < n; ++i) { int w{0}; for (int k{0}; k < 3; ++k) if (v[cand][k] > v[i][k]) ++w; if (w < 2) cand = i; } for (int i{0}; i < n; ++i) { if (i == cand) continue; int w{0}; for (int k{0}; k < 3; ++k) if (v[cand][k] > v[i][k]) ++w; if (w < 2) { cout << "-1\n"s; return ; } } cout << cand + 1 << '\n'; } int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); int t{1}; //cin >> t; while (t--) solve(); return 0; } ``` ::: ## [簡單的小遊戲](https://judge.csie.ncku.edu.tw/problems/84) - Task Provider: XDEv11 - Task Setter: leo ### 首殺 dennis23314063 (01:05) ### 解法說明 #### Subtask 1 $O(t(5^{nm}\cdot nm))$ 一個格子最多只會跟 $4$ 個格子相鄰,因此數字最多為 $4$, 只要枚舉每個格子的數字($0\sim4$), 並檢驗相鄰的非 $0$ 數量以及是否大於原本的數字即可, 如果枚舉所有可能都沒找到,則輸出 `NO`。 枚舉複雜度 $O(5^{nm})$,檢驗 $O(nm)$,總複雜度 $O(5^{nm}\cdot nm)$ :::spoiler 參考程式 ```cpp #include <iostream> using namespace std; int n, m, a[3][3], tmp[3][3]; bool valid(int x, int y){ return x >= 0 && x < n && y >= 0 && y < m && tmp[x][y]; } int calc(int x, int y){ static const int dx[4] = {0, 1, 0, -1}, dy[4] = {1, 0, -1, 0}; int cnt = 0; for(int i = 0; i < 4; i++) if(valid(x + dx[i], y + dy[i])) cnt++; return cnt; } bool dfs(int x){ int r = x / m, c = x % m; if(x >= n * m){ for(int i = 0; i < n; i++) for(int j = 0; j < m; j++) if(!((!tmp[i][j] || calc(i, j) == tmp[i][j]) && tmp[i][j] >= a[i][j])) return 0; return 1; } for(int i = 0; i <= 4; i++){ tmp[r][c] = i; if(dfs(x + 1)) return 1; } return 0; } int main() { ios::sync_with_stdio(0); cin.tie(0); int t; cin >> t; while(t--){ cin >> n >> m; for(int i = 0; i < n; i++) for(int j = 0; j < m; j++) cin >> a[i][j]; if(dfs(0)){ cout << "YES\n"; for(int i = 0; i < n; i++) for(int j = 0; j < m; j++) cout << tmp[i][j] << " \n"[j == m - 1]; } else{ cout << "NO\n"; } } } ``` ::: #### Subtask 2 $O(nm)$ 根據上面所述,每個格子可分為下列三種情況: 1. 在最角落只有 $2$ 個數字與其相鄰,因此最大為 $2$ 2. 在邊邊的只有 $3$ 個數字與其相鄰,因此最大為 $3$ 3. 其餘有 $4$ 個數字與其相鄰,因此最大為 $4$ 只要一開始輸入的數字違反了上述的條件,則該筆輸入答案為 `NO`, 否則只要將數字填為可能的最大值,每個格子都是非 $0$, 所以也剛好讓數字都可以達到最大值,範例如下: ``` 2 3 3 3 2 3 4 4 4 3 3 4 4 4 3 2 3 3 3 2 ``` ### 標準程式 :::spoiler ```cpp #include <iostream> #include <vector> #include <string> using namespace std; void solve() { int n, m; cin >> n >> m; vector<vector<int>> mtx(n, vector<int>(m)); for (auto& vt : mtx) for (auto& x : vt) cin >> x; for (int i{0}; i < n; ++i) for (int j{0}; j < m; ++j) { int neb{4}; if (i == 0 || i == n - 1) --neb; if (j == 0 || j == m - 1) --neb; if (mtx[i][j] > neb) { cout << "NO\n"s; return ; } else mtx[i][j] = neb; } cout << "YES\n"s; for (auto& vt : mtx) { for (auto& x : vt) cout << x << ' '; cout << '\n'; } } int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); int t{1}; cin >> t; while (t--) solve(); return 0; } ``` ::: ## [射箭比賽](https://judge.csie.ncku.edu.tw/problems/85) - Task Provider: leo - Task Setter: leo ### 首殺 p76094795 (01:00) ### 解法說明 <font class="focus">這題主要是想藉著開學考讓大家知道有這種互動的題型, 希望寫完這題後可以大致上知道互動題的運作模式, OJ 上面也還有其他互動題可以讓各位練習 只要有 Interactive 標籤的都是互動題。</font> <font class="focus">另外,這題只要有認真讀完題敘就應該有 $12$ 分可以拿, 鼓勵各位每一題都應該要仔細閱讀, 不要因為某一題過不了而一直卡在那邊。</font> #### Subtask 1 $O(1)$ 直接把題目附的檔案上傳就可以了,因為 $N\leq2$,所以只有兩種情況: - $N=1$,無法動作,只好投降 - $N=2$,射一顆,留下最後一顆 :::spoiler 參考程式 ```cpp #include "lib0085.h" int main(){ int t, n, k, shot; t = Lets_shoot(); while(t--){ Game(&n, &k); if(n == 2) shot = Shooting(1); else Surrender(); } } ``` ::: #### Subtask 2 $O(NK)$ 你只要不斷的射一顆,直到對方投降或是輪到自己的時候剩一顆而投降 :::spoiler 參考程式 ```cpp #include "lib0085.h" int main(){ int t, n, k, x; t = Lets_shoot(); while(t--){ Game(&n, &k); if(n == 1){ Surrender(); continue; } while(n != 1 && (x = Shooting(1))){ n -= 1 + x; if(n == 1) Surrender(); } } } ``` ::: #### Subtask 3 $O(NK)$ 最終輸的條件是輪到自己時只剩 $1$ 顆,可以藉由這個推導, 如果雙方都採取最佳策略的話,那麼當氣球數量為 $K+2$ 顆時,你最多只能射 $K$ 顆, 如此一來會剩下 $2$ 顆,如果對方射掉一顆留下最後一顆,你就輸了; 若你只射 $1$ 顆的話,對方可以選擇射 $K$ 顆, 最後還是剩下最後一顆,一樣是你輸。 由於後手可以將每次雙方射掉的氣球數量控制在 $nK+n$, 從上面這邊可以推論,當輪到某人時的氣球數量為 $1,K+2,2K+3,\dots,nK+n+1(n\in\{0,\mathbb{N}\})$ 時, 該玩家在這場遊戲的狀態必輸。 因此如果開場的氣球數量除以 $K+1$ 的餘數為 $1$ 時則必輸, 否則只要每次射完後將氣球數量控制在 $nK+n+1$ 即可贏得比賽。 ### 標準程式 :::spoiler 需要注意不要射到負數個氣球,在此用 `mod_abs` 取正的餘數 ```cpp #include "lib0085.h" int mod_abs(int x, int mod){ return x < 0 ? x + mod : x; } int main(){ int t, n, k, x; t = Lets_shoot(); while(t--){ Game(&n, &k); if(n % (k + 1) == 1){ Surrender(); continue; } while(x = Shooting(mod_abs(n % (k + 1) - 1, k + 1))) n -= mod_abs(n % (k + 1) - 1, k + 1) + x; } } ``` ::: ## [跳啊](https://judge.csie.ncku.edu.tw/problems/86) - Task Provider: ys - Task Setter: ys ### 首殺 visitorckw (00:35) ### 解法說明 #### Subtask 1~2 直接模擬題目敘述中的做法 當起點試到第 $x$ 個格子時,作法如下: 1. 當 $x \le n$,可得到分數 $a_x$,並且將硬幣往右移動 $a_x$ 格 (就是移到位置 $x + a_x$) 2. 繼續第 1 步驟直到 $x > n$ 該做法的複雜度為 $O(n^2)$ :::spoiler 參考程式 ```cpp #include<bits/stdc++.h> using namespace std; int constexpr maxn = 1e6 + 10; int n, a[maxn]; int main() { cin >> n; for(int i = 1; i <= n; i++) cin >> a[i]; int best = 0; for(int i = 1; i <= n; i++) { int x = i, sum = 0; while(x <= n) sum += a[x], x += a[x]; best = max(best, sum); } cout << best << endl; return 0; } ``` ::: #### Subtask 3 假設 $s(i)$ 為硬幣移到 $i$ 格子時,目前可得到的最大分數 例如 $a = [2, 1, 3, 4]$,則 $s(3) = 2$ > 從起點 $i = 1$ 可跳到 $i = 3$ 共得到 $2$ 分 若從起點 $i = 2$ 跳至 $i = 3$ 只能得 $1$ 分 假設對於格子 $x$,已知比 $x$ 小的格子只有 $i$ 和 $j$ 格子能一步到達 $x$ > 也就是說 $x = i + a_i$ 及 $x = j + a_j$ 若想知道 $s(x)$ 為多少,則只需看 $s(i) + a_i$ 和 $s(j) + a_j$ 何者較大 然而這裡的 $s(i)$ 及 $s(j)$ 也得靠比 $i$ 或 $j$ 小的格子求得 依此類推,若想算出 $s$,需要從第 $1$ 格至 $n$ 格依序計算 實作上,利用陣列 `s` 當起點選到第 $i$ 格時,就更新 `s[i+a[i]]` 的值 ```cpp s[i+a[i]] = max(s[i+a[i]], s[i] + a[i]); ``` > 這邊要注意到當選到 $i$ 格時,`s[i]` 早已得到答案 這樣在之後選到此 `i+a[i]` 格子時,就已經計算完到該格的最大分數了 以上該做法的複雜度為 $O(n)$ ### 標準程式 :::spoiler ```cpp #include<bits/stdc++.h> using namespace std; int constexpr maxn = 1e6 + 10; int s[maxn], n, a; int main() { cin >> n; int best = 0; for(int i = 1; i <= n; i++) { cin >> a; if(i+a > n) best = max(best, s[i] + a); else s[i+a] = max(s[i+a], s[i] + a); } cout << best << endl; return 0; } ``` ::: ## [消消樂](https://judge.csie.ncku.edu.tw/problems/87) - Task Provider: D4nnyLee - Task Setter: D4nnyLee ### 首殺 pmcat (01:09) ### 解法說明 在分成兩個子任務看之前,我們需要先分析題目要的東西。 每次操作都會得到 $ax + b$ 分,也就是說做了 $k$ 次之後會獲得的分數是 $\sum^{k}_{i = 1}(ax_i + b)$。 > $x_i$ 代表第 $i$ 次操作消去的石頭數量。 上面的公式可以化簡成 $a \sum^{k}_{i = 1}{x_i} + kb$,如果石頭被消完的話,可以發現 $\sum^{k}_{i = 1}{x_i}$ 會等於石頭總數,也就是 $N$。 因此最後獲得的分數可以寫成 $aN + kb$。 從公式中可以看出,會影響最後總分大小的只有 $k$ 這個變數,也就是消完全部石頭的總操作數。 #### Subtask 1 在這個子任務中,$b$ 不會是負數,因此要得到最大分數的方法就是讓 $k$ 愈大愈好。 讓 $k$ 最大的方法就是每次操作都只消除一個石頭,所以這時 $k$ 就會等於 $N$。 答案為 $(a + b) \cdot N$。 :::spoiler 參考程式 ```cpp #include <iostream> using namespace std; int main() { int t, n, a, b; string s; cin >> t; while(t--){ cin >> n >> a >> b >> s; cout << n * (a + b) << "\n"; } } ``` ::: #### Subtask 2 現在多了 $b$ 是負數的情況,因此當 $b$ 是負數時必須要讓 $k$ 愈小愈好。 這邊將連續的一段石頭視為一個「區段」,舉例來說, `WWW` 就只有一個區段,而 `WWWBBW` 有 $3$ 個,`WBWBW` 有 $5$ 個。 消除全部的石頭就意味著要將所有的區段消除。 每次的操作都固定為從現有的區段中選出其中一個,並將這個區段中的所有石頭都消除。 > 只消除區段中的部分石頭只會多浪費一次操作,並不會讓結果更好。 假設現在有 $cnt$ 個區段,要怎麼選才會得到最小的 $k$? 可以觀察一下,如果 $cnt \geq 3$,消除第 $1$ 個或是第 $cnt$ 個區段之後會讓區段的總數少 $1$。 但是如果選擇其它的區段的話,在消除之後左右兩邊的區段會合併成一個區段,所以會讓區段的總數少 $2$。 因此最好的取法就是每次都選頭尾以外的區段來消除。 如果 $1 \leq cnt \leq 2$,那就只能花 $cnt$ 次操作把所有區段消除。 這邊很直覺的就可以寫出以下程式來算出 $k$, ```cpp int k = 0; while (cnt >= 3) { cnt -= 2; ++k; } k += cnt; ``` 但其實 $k$ 可以用一個公式就算出來,將 $cnt$ 分成奇數和偶數兩種情況去分析: * 奇數: 經過 $\frac{cnt - 1}{2}$ 次操作之後會剩下 $1$,因此 $k = \frac{cnt - 1}{2} + 1$ * 偶數: 經過 $\frac{cnt - 2}{2}$ 次操作之後會剩下 $2$,因此 $k = \frac{cnt - 2}{2} + 2 = \frac{cnt}{2} + 1$ 上面兩個公式可以總結成 $k = \lfloor \frac{cnt}{2} \rfloor + 1$。 有了 $k$ 之後就可以直接算出最後的總分了。 ### 標準程式 :::spoiler 參考程式 ```cpp #include <iostream> using namespace std; void test_case() { int n, a, b; string s; cin >> n >> a >> b >> s; if (b >= 0) cout << n * (a + b) << '\n'; else { int cnt = 1; for (int i = 1; i < n; ++i) if (s[i] != s[i - 1]) ++cnt; cout << a * n + (cnt / 2 + 1) * b << '\n'; } } int main(void) { int t = 1; cin >> t; while (t--) test_case(); return 0; } ``` ::: ## [通識選課](https://judge.csie.ncku.edu.tw/problems/88) - Task Provider: leo - Task Setter: leo ### 首殺 pmcat (00:37) ### 解法說明 #### Subtask 1 <font class="focus">$O(N^M\cdot NM)$ by 剪枝</font> 枚舉每個人最後所錄取的志願,並驗證是否符合抽籤規則, 如果枚舉過程中發現某堂課假設的錄取人數超過該堂課上限, 則不管後面的枚舉狀況如何,該枚舉必然不成立, 因此可以適當排除這些不可能的組合以加速枚舉過程, 這種技巧稱為「剪枝」 而驗證枚舉結果合法性的方式如下: 1. 如果有一門還有餘額的課程在被錄取的課程前面,則不符合規則 2. 如果有志願序較前面的錄取但是志願序較大的沒被錄取,則不符合規則 可以直接跑過所有人的第一志願,如果該志願是該學生錄取的志願,則將他錄取。 接著再跑一次第一志願看有沒有人有資格錄取卻沒被錄取。 如此一來志願較前面的人一定會優先被錄取到, 而且不會有明明前面的課程還有餘額卻沒有錄取到的狀況。 此驗證方法總複雜度為 $O(\sum K)=O(NM)$, 而枚舉的複雜度為 $O(\prod K)=O(N^M)$, 因此算法總複雜度為 $O(N^M\cdot NM)$。 要特別注意的是,有可能會有人沒有錄取任何志願,因此要記得枚舉這種狀況。 :::spoiler 參考程式 ```cpp #include <iostream> #include <vector> #include <cstring> using namespace std; const int MAXN = 8, MAXM = 8; vector<int> vul[MAXN + 1]; int n, m, stu[MAXN + 1], res[MAXM + 1], cnt[MAXN + 1]; bool dfs(int now){ if(now > m){ int admit[MAXN + 1] = {}; bool used[MAXM + 1] = {}; for(int j = 0; j < n; j++){ for(int i = 1; i <= m; i++){ if(used[i] || j >= vul[i].size()) continue; if(vul[i][j] == res[i]) used[i] = 1, admit[res[i]]++; } for(int i = 1; i <= m; i++){ if(used[i] || j >= vul[i].size()) continue; if(admit[vul[i][j]] < stu[vul[i][j]]) return 0; } } return 1; } for(int i : vul[now]){ res[now] = i; cnt[i]++; if(cnt[i] <= stu[i] && dfs(now + 1)) return 1; cnt[i]--; } res[now] = -1; return dfs(now + 1); } int main() { int x, k; cin >> n >> m; memset(res, -1, sizeof(res)); memset(cnt, 0, sizeof(cnt)); for(int i = 1; i <= n; i++) cin >> stu[i]; for(int i = 1; i <= m; i++){ cin >> k; for(int j = 1; j <= k; j++){ cin >> x; vul[i].push_back(x); } } dfs(1); for(int i = 1; i <= m; i++) cout << res[i] << " \n"[i == m]; } ``` ::: #### Subtask 2 $O(NM)$ 可以觀察 Subtask 1 驗證的過程,就可以發現以下做法: 從每個人的第一志願開始看,如果餘額還有剩就將該學生錄取進該課程, 接下來再看還沒有錄取任何課程那些人的第二志願, 接著重複以上動作,就可以完成抽籤了,總複雜度 $O(\sum{K})=O(NM)$ ### 標準程式 :::spoiler ```cpp #include <iostream> #include <queue> using namespace std; const int MAXN = 1000, MAXM = 1000; queue<int> vol[MAXM]; int main(){ ios::sync_with_stdio(0); cin.tie(0); int n, m, stu[MAXN + 1], res[MAXM] = {}, tmp, k; cin >> n >> m; for(int i = 1; i <= n; i++) cin >> stu[i]; for(int i = 0; i < m; i++){ cin >> k; while(k--){ cin >> tmp; vol[i].push(tmp); } } for(int rem = m; rem;){ for(int i = 0; i < m && rem; i++){ if(res[i]) continue; if(vol[i].empty()){ rem--; res[i] = -1; continue; } if(stu[vol[i].front()]) res[i] = vol[i].front(), stu[vol[i].front()]--, rem--; vol[i].pop(); } } for(int i = 0; i < m; i++) cout << res[i] << " \n"[i == m - 1]; } ``` ::: <style> .focus { color: red; font-weight: bold; } </style>