---
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>