# 2021年9月APCS題解
## [1. 七言對聯](https://zerojudge.tw/ShowProblem?problemid=g275)
注意到每一句只有第二、四、六、七個字會用到,而且可以把句子第二、四、六個字的平仄轉成整數來判斷。第一個規則代表上下聯一定要是${010}_2(={2}_{10})$或${101}_2(={5}_{10})$;第二個規則可以直接判斷;第三個規則則是上下聯bitwise xor要是$7_{10}(={111}_{2})$,也就是每個位置都不同。
:::spoiler code:
```cpp=
#include <bits/stdc++.h>
using namespace std;
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
int n, upend, downend, up, down, x, f; cin >> n;
for (int i = 0; i < n; i++) {
up = down = 0;
for (int j = 0; j < 3; j++) {
cin >> x >> x;
(up <<= 1) += x;
}
cin >> upend;
for (int j = 0; j < 3; j++) {
cin >> x >> x;
(down <<= 1) += x;
}
cin >> downend;
f = 1;
if ((up != 2 && up != 5) || (down != 2 && down != 5)) {
cout << 'A';
f = 0;
}
if ((!upend) || (downend)) {
cout << 'B';
f = 0;
}
if ((up ^ down) != 7) {
cout << 'C';
f = 0;
}
if (f) {
cout << "None\n";
}
else {
cout << '\n';
}
}
return 0;
}
```
:::
## [2. 魔王迷宮](https://zerojudge.tw/ShowProblem?problemid=g276)
我把魔王包進一個結構,然後把移動和檢查出界和檢查消失的函式封裝在裡面,先放炸彈再讓魔王移動,判斷有沒有出界或踩到炸彈,如果踩到炸彈就把炸彈的位置放到一個`queue`裡面,等魔王全部移動完再移除炸彈,最後魔王全部消失的時候遍歷一次棋盤算還有幾個炸彈。
:::spoiler code:
```cpp=
#include <bits/stdc++.h>
#define F first
#define S second
using namespace std;
using p2i = pair<int, int>;
struct boss
{
bool ok;
int r, c, s, t;
boss(): ok(true), r(), c(), s(), t() {}
inline void move() {
r += s;
c += t;
}
inline bool check(const int& n, const int& m) { //判斷出界
return (ok = ((r >= 0 && r < n) && (c >= 0 && c < m)));
}
explicit inline operator bool() const { //用顯式轉型成bool判斷是否已經消失
return ok;
}
};
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
int n, m, k, cnt, ans = 0; cin >> n >> m >> k;
cnt = k;
vector<vector<int> > board(n, vector<int>(m, 0));
vector<boss> v(k);
for (boss& b : v) {
cin >> b.r >> b.c >> b.s >> b.t;
}
queue<p2i> clrpos;
p2i tmp;
while (cnt) {
for (int i = 0; i < k; i++) {
if (!v[i]) continue;
board[v[i].r][v[i].c] = 1;
}
for (int i = 0; i < k; i++) {
if (!v[i]) continue;
v[i].move();
if (v[i].check(n, m)) {
if (board[v[i].r][v[i].c]) {
v[i].ok = false;
--cnt;
clrpos.push(p2i(v[i].r, v[i].c));
}
}
else {
--cnt;
}
}
for (; !clrpos.empty(); clrpos.pop()) {
tmp = clrpos.front();
board[tmp.F][tmp.S] = 0;
}
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (board[i][j]) ++ans;
}
}
cout << ans << '\n';
return 0;
}
```
:::
## [3. 幸運數字](https://zerojudge.tw/ShowProblem?problemid=g277)
區間最小位置我是用稀疏表(因為這題不用修改,不然其實線段樹也可以用),區間和用前綴和陣列,然後我的code裡面的編號都是0-based,然後左閉右開。
時間複雜度:sparse table build:$O(n{\log}n)$,找幸運號碼最差$O(n)$,所以是$O(n{\log}n)$。
:::spoiler code:
```cpp=
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
struct sparse_table
{
int n;
vector<int> data;
vector<vector<int> > table;
sparse_table(const int& _n, vector<i64>& _pre) :n(_n), data(_n, 0), table((__lg(_n) + 1), vector<int>(n, 0)) {
iota(table[0].begin(), table[0].end(), 0);
for (int i = 0; i < n; i++) {
cin >> data[i];
_pre[i + 1] = _pre[i] + data[i];
}
for (int i = 1; (1 << i) <= n; i++) {
for (int j = 0; j + (1 << i) <= n; j++) {
table[i][j] = (data[table[i - 1][j]] < data[table[i - 1][j + (1 << (i - 1))]] ? table[i - 1][j] : table[i - 1][j + (1 << (i - 1))]);
}
}
}
inline int operator() (const int& l, const int& r) {
static int ansL, ansR, lglen;
lglen = __lg(r - l);
ansL = table[lglen][l];
ansR = table[lglen][r - (1 << lglen)];
return (data[ansL] < data[ansR] ? ansL : ansR);
}
};
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
int n; cin >> n;
vector<i64> pre(n + 1, 0);
sparse_table st(n, pre);
function<i64(const int&, const int&)> calc = [&pre] (const int& _l, const int& _r) -> i64 {
return pre[_r] - pre[_l];
};
int l = 0, r = n, mid;
i64 lsum, rsum;
while (l < r - 1) {
mid = st(l, r);
lsum = calc(l, mid), rsum = calc(mid + 1, r);
if (rsum >= lsum) {
l = mid + 1;
}
else {
r = mid;
}
}
cout << st.data[l] << '\n';
return 0;
}
```
:::
## [4. 美食博覽會](https://zerojudge.tw/ShowProblem?problemid=g278)
這是一題~~看數字範圍猜複雜度~~$O(nk)$的DP(或$O(n{\log}C)$,如果你是外星人),在DP之前需要做預處理,定義`v[i]`為以第`i`個數結尾的最長無重複元素的連續子區間長度,`dp[j][i]`為第`j`個人選第`1~i`個位置結尾的前綴最大值,則轉移式會長這樣:`dp[j][i] = max(dp[j][i - 1], dp[j - 1][i - v[i]] + v[i])`,實作為了方便所以用1-based,然後我發現DP陣列只會從上一個人轉移,所以我把DP陣列滾動掉了。
時間複雜度:$O(nk)$
空間複雜度:$O(n)$
:::spoiler code:
```cpp=
#include <bits/stdc++.h>
using namespace std;
int pos[100001]; //存每個數字第一次出現的位置
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
int n, k; cin >> n >> k;
vector<vector<int> > dp(2, vector<int>(n + 1, 0));
vector<int> v(n + 1, 0);
for (int i = 1, x; i <= n; i++) {
cin >> x;
v[i] = min(i - pos[x], v[i - 1] + 1); //找以$a_i$為結尾的最長無重複元素的連續子區間長度
pos[x] = i;
}
for (int j = 1; j <= k; j++) {
for (int i = 1; i <= n; i++) {
dp[j & 1][i] = max(dp[j & 1][i - 1], dp[(j & 1) ^ 1][i - v[i]] + v[i]);
}
}
cout << dp[k & 1][n] << '\n';
return 0;
}
```
:::
這題在zerojudge上有k值加大的版本([h926](https://zerojudge.tw/ShowProblem?problemid=h926)),需要用Aliens優化才能過,有關Aliens優化的東西可以去看[這篇](https://cp.wiwiho.me/aliens/),~~雖然我不會證明這題用Aliens優化會是好的,但是這題的tag上就有個aliens~~
:::spoiler Aliens優化版本code:
```cpp=
#include <bits/stdc++.h>
#define F first
#define S second
using namespace std;
using p2i = pair<int, int>;
int pos[100001];
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
function<bool(const p2i&, const p2i&)> cmp = [] (const p2i& a, const p2i& b) -> bool {
return ((a.F < b.F) || (a.F == b.F && a.F > b.F));
};
int n, k; cin >> n >> k;
vector<p2i> dp(n + 2, p2i(0, 0));
vector<int> v(n + 1, 0);
for (int i = 1, x; i <= n; i++) {
cin >> x;
v[i] = min(i - pos[x], v[i - 1] + 1);
pos[x] = i;
}
function<void(const int&)> check = [&] (const int& p) -> void {
for (int i = 1; i <= n; i++) {
dp[n + 1] = dp[i - v[i]];
dp[n + 1].F += v[i] - p;
++dp[n + 1].S;
dp[i] = max(dp[i - 1], dp[n + 1], cmp);
}
};
int l = -1, r = (n + (n % k)) / k, mid;
while (l < r - 1) {
mid = (l + r) >> 1;
check(mid);
if (dp[n].S <= k) r = mid;
else l = mid;
}
check(r);
cout << dp[n].F + k * r << '\n';
return 0;
}
```
:::