---
# System prepended metadata

title: 東區模擬賽 2021 題解
tags: [' north1_110_sim', Editorial]

---

---
tags: Editorial, north1_110_sim
---

# 東區模擬賽 2021 題解

[TOC]

## 上午場

### A - 防疫破口 (Peko)

- Idea：Ccucumber12
- 出題人：Ccucumber12
- 題目敘述：Ccucumber12
- 首殺：6:50 by 蔡平樂
- AC 人數：6 / 9
- 平均分數：17.56 / 25

$\texttt{struct}$ 排序實作題

把每筆資料包在一個 $\texttt{struct}$ 裡面並對 $t_i$ 排序。接著依序檢查每筆資料，並以 $cnt$ 紀錄目前的室內人數。若 $op_i = 1$ 則將 $cnt - 1$；若 $op_i = 0$ 則將 $cnt + 1$，同時如果 $cnt > k$ 則將 $name_i$ 記錄於任意容器內。最終將該容器排序輸出即可。

第一子題不需任何排序，第二子題須對 $\texttt{string}$ 排序，第三子題須對自定義 $\texttt{struct}$ 排序。


### B - 礦砂採集 (Ore)

- Idea：SorahISA
- 出題人：SorahISA
- 題目敘述：SorahISA
- 首殺：11:10 by 蔡平樂
- AC 人數：7 / 9
- 平均分數：20.11 / 25

首先，往左邊走以及挖左邊的礦砂堆是完全沒用的操作，因為一直往右走一定最好。所以以下敘述都不會包含到往右走的 $N-1$ 次操作。

如果 $a_i \ge a_{i+1}$，那麼可以直接往右走；如果 $a_i < a_{i+1}$，那麼往右走會需要花費 $a_{i+1} - a_i$ 單位時間，而且會讓 $a_{i+1}$ 變成 $a_i$。注意到往下挖的操作只會讓後面需要挖的東西變多，所以不應該在不必要的時候往下挖。

要移動到第 $N$ 坨礦砂堆上面，挖東西的花費就是 $$\sum_{i=1}^{N}{\left(a_i - \min_{j=1}^{i}{a_j}\right)}$$
可以在 $O(N^2)$ 或 $O(N)$ 時間計算完成。

要特別注意的情況有：沒開 $\texttt{long long}$ 只會拿到 $15$ 分、到第 $N$ 坨礦砂堆之後需要往下挖到高度 $0$。


### C - A \(C\) B Problem (AcB)

- Idea：Ccucumber12、SorahISA
- 出題人：SorahISA
- 題目敘述：SorahISA
- 首殺：44:54 by 安馬林
- AC 人數：3 / 9
- 平均分數：9.22 / 25

先考慮 $C = 111 \dots 111$ 的情況，每次都取 $A$ 跟 $B$ 的個位數，將他們加起來並存到一個陣列（或字串）裡面，最後再倒著輸出就可以了。第一個 Subtask 可以用 $\texttt{int}$、第二個 Subtask 可以用 $\texttt{long long}$ 做掉，第三個 Subtask 就需要使用字串來存了。

如果 $C$ 的數字可以任意，首先先把 $c_i = 0$ 的情況判掉，因為字串為空不用管它。接下來是處理字串跟數字的轉換的部分，切出一段字串可以使用 $\texttt{str.substr(start_pos, length)}$、轉數字可以使用 $\texttt{stoi(str)}$、轉回字串可以使用 $\texttt{to_string(number)}$。

剩下的部分就是實作了，要特別注意的情況有：可能會取到超出字串位置的地方，以及可能兩個位置加起來結果是 $0$。


### D - 分居 (Partition)

- Idea：Ccucumber12
- 出題人：SorahISA
- 題目敘述：catJam
- 首得分：85:28 by 蔡平樂 (12/25)
- 最高分：153:46 by 蔡平樂 (18/25)
- AC 人數：0 / 9
- 平均分數：3.67 / 25

對於第一個 Subtask，只要輸出

```
1
1
1
2
```

就能拿到 $1$ 分了。

首先，「把東西分成兩邊使得同一組之內沒有邊」可以被達成若且唯若給定的圖是二分圖，以下以黑色跟白色來表示二分圖的兩塊獨立集。

先考慮圖**連通**的狀況：從任意的點開始 DFS，對於所有相鄰的點，如果它還沒有被上色，就把它標示為另一個不同的顏色並走訪；如果它已經被上色了，就去檢查是不是跟當前的點顏色不相同，**如果顏色相同即無解**。

在上述的走訪過程中我們會將所有點塗上黑或白兩種顏色之一，接著就去判斷應該要把黑色還是白色給 $\mathrm{Cat}$ 就可以了。

如果只考慮圖連通的狀態會拿到 $12$ 分。

對於圖**不連通**的狀況，先觀察到兩個不在同一個連通塊的點不會互相影響到對方，所以可以分開做。只要每次對一個連通塊 DFS 完都找出應該要把它的黑色還是白色給 $\mathrm{Cat}$，就能通過本題。

Extra：分成數量盡量平均的兩堆，一樣平均的狀況就使 $\mathrm{Cat}$ 的字典序最小。


### E - 井字遊戲 (TicTacToe)

- Idea：Ccucumber12
- 出題人：Ccucumber12
- 題目敘述：catJam
- 首殺：127:38 by 蔡平樂
- AC 人數：1 / 9
- 平均分數：6 / 25

回朔法尋找所有可能性

利用一個 $3\times 3$ 陣列紀錄目前盤面，並透過 DFS 去搜尋每一步的可能性。在每次 DFS 前，先檢查目前盤面是否已有人獲勝，若有的話則記錄獲勝方並 return；反之則開始尋找該步可以下在哪裡。並對所有的可能性繼續往下 DFS，直到下完九步若仍無人獲勝，則紀錄為平手並 return。

細節上可以將盤面初始化為 $-1$，並利用 $1$、$0$ 分別代表 $\texttt{o}$ 和 $\texttt{x}$，此時 $\texttt{step & 1}$ 就可以代表該步的下棋方。檢查的過程中可以傳入前次的下棋方（該次的反方），因為只有他有可能在這步之前獲勝。如此一來便可以輕易的判斷每行／列／斜線是否等於該方，並以迴圈簡化實作量。


## 下午場

### F - 代理股王 (Agent)

- Idea：SorahISA
- 出題人：SorahISA
- 題目敘述：catJam
- 首殺：8:55 by 蔡平樂
- AC 人數：5 / 9
- 平均分數：20.22 / 25

可以把題目轉化成以下樣貌

> 定義一個位置 $i$ 的「完美值」$p_i$ 是指對所有 $i - p_i \le j < i$ 都有 $a_j < a_{j+1}$，且對所有 $i < j \le i + p_i$ 都有 $a_j < a_{j-1}$ 的最大值。
> 給定一長度為 $N$ 的數列，求其最高的完美值、以及此完美值的出現次數。

接著就可以直接枚舉每個中間點去尋找兩邊的長度最長能到多長。

要特別注意的地方是答案可能為 $0$，有不少人都栽在這上面。

Extra：$N \le 1\,000\,000$。


### G - 字母對消 (Alphabet)

- Idea：Ccucumber12
- 出題人：Ccucumber12
- 題目敘述：Ccucumber12
- 首殺：37:24 by 吳畬
- AC 人數：5 / 9
- 平均分數：15 / 25

貪心＋動態規劃

首先考慮選取字母對的可能性，選取距離越遠的字母對會消除更多的字母，因此我們會希望能選取越接近的越好。所以對於任意的字母，我們必然只會選擇他與上一次出現或下一次出現的位置相配對，中間不可能再出現相同的字母。（ex. $\texttt{a__a__a}$：我們只會選擇 $(s_1, s_4)$ 或者 $(s_4, s_7)$，而不會選擇 $(s_1, s_7)$。因此在最佳選擇下，我們最多只有 $\mathcal{O}(|s|)$ 個字母對可以選擇，而這些字母對就相當於 $\mathcal{O}(|s|)$ 個線段。

此時我們的題目等價於給定多個帶權重線段，請選出一些線段使得任兩個線段不相交且權重總和最大。對於這種題目我們可以考慮動態規劃：$\texttt{dp}[i] = [s_1, s_i]$ 所能產生的最大權重和。對於字母 $s_i = c$，找到 $c$ 上次出現的位置 $j$，並考慮
$$
\texttt{dp}[i] = \max\left\{ \underbrace{\texttt{dp}[j] + V(c)}_{選取該線段}, \underbrace{\texttt{dp}[i-1]}_{不選取該線段} \right\}
$$

而在尋找 $c$ 上次出現的位置，我們可以開一個 $26$ 格長的陣列記錄每個字母最後出現的位置，並在轉移 $\texttt{dp}$ 值後更新。此時的時間複雜度為：$\mathcal{O}(|s|)$ 狀態 $\times$ $\mathcal{O}(1)$ 轉移 $=$ $\mathcal{O}(|s|)$。

然而如果我們暴力往後尋找上次出現的位置，考慮平均下來每個位置的時間複雜度：假設我們平均往後走 $k$ 格，則這 $k$ 格內每個字母最多出現一次，否則那個重複的字母長度就會 $< k$。所以我們可以得知 $k\leq|C|$，其中 $|C| = 26$ 代表字母集的大小。因此在這題我們的時間複雜度就會是：$\mathcal{O}(|s|)$ 狀態 $\times$ $\mathcal{O}(|C|)$ 轉移 $=$ $\mathcal{O}(|C||s|)$，是一個好的時間複雜度。（題目測資並沒有出爛，~~暴力本來就可能出奇蹟~~）

另外一種分析方法如下：先對每個字母分開來看，如果我們把他的所有位置都抓出來，那麼最後一個位置會往回跑到倒數第二個位置，倒數第二個位置會往回跑到倒數第三個位置。以此類推，第二個位置會往回跑到第一個位置，而第一個位置會往回跑到字串開頭的位置，總共跑的距離和最多是 $\mathcal{O}(|s|)$。
因為字元集大小只有 $|\{\texttt{a}, \texttt{b}, \dots, \texttt{z}\}| = 26$，所以暴力的做法只要適當地加上 break 也可以在 $\mathcal{O}(26 |s|)$ 的時間跑完。

Extra：選取一個端點相同的區間消除時，可以獲得的分數改為【區間內出現與端點同字母的數量】
乘上【該字母的權重】。


### H - 石油王與遺跡考古 (Relics)

- Idea：mmi366127
- 出題人：SorahISA
- 題目敘述：SorahISA
- 首得分：80:12 by 李尚哲 (1/25)
- 最高分：106:50 by 蔡平樂 (7/25)
- AC 人數：0 / 9
- 平均分數：0.89 / 25

TL;DR：暴搜剪枝 + bitset 優化

前兩個 Subtask 比較簡單，可以 $\mathcal{O}(2^N)$ 枚舉所有子集，檢查他的大小是不是 $\ge K$（實際上只要檢查 $= K$ 的狀態），再去看所有子集的狀態取 $\texttt{bitwise-AND}$ 的結果，並把該結果的所有子集都丟進答案裡。這樣可以拿到前兩個 Subtask 共 $3$ 分。

實作上我們讓 $\texttt{sets}[i]$ 是一個長度為 $C$ 的 $01$-字串，$\texttt{sets}[i][j]$ 代表第 $i$ 個集合有沒有包含數字 $j$。

那如果使用暴搜呢？每次搜一個數字集合有沒有出現 $\ge K$ 次，如果有就把他存下來，繼續加一個數字搜尋下去。

我們來分析一下暴搜的複雜度：首先，如果遞迴到了一個出現次數 $< K$ 的狀態，那就不會再繼續下去，否則答案數量一定會至少 $+1$，所以你至多只會遞迴進 $ans = 300\,000$ 個節點，且在每個節點內至多會對 $C = 3\,000$ 個數字進行判斷，如果判斷有幾個集合內存在數字的複雜度是 $\mathcal{O}(N)$，直接實作的複雜度就會是 $\mathcal{O}(ans \times C \times N)$，可以通過三到四個 Subtask，拿到 $7 \sim 15$ 分。

具體搜尋方法如下，要注意如果直接在 $11 \sim 15$ 行那裡就宣告 $\texttt{nxt_match}$，那麼宣告 $\texttt{nxt_match}$ 的次數會從 $ans$ 退化成 $ans \times C$：

```cpp=
// in_sets[i][j] = 1 代表數字 i 存在在集合 j 內
vector<vector<bool>> in_sets(C, vector<bool>(N));
vector<vector<int>> answers; // 目前的所有答案
vector<int> choose; // 當前搜尋的答案

// match[i] = 1 代表集合 i 有包含 choose
void dfs(int now, vector<bool> match) {
    answers.push_back(choose); // 把目前搜尋的狀態加進答案
    for (int i = now+1; i <= C; ++i) {
        // 計算加入數字 i 之後有幾個集合包含 choose
        int nxt_match_count = 0;
        for (int j = 1; j <= N; ++j) {
            // 如果【其他 choose 內的在集合 j 內】且【數字 i 在集合 j 內】就加一
            nxt_match_count += match[j] & in_sets[i][j];
        }
        // 如果有超過 K 個集合包含 choose 則遞迴下去
        if (nxt_match_count >= K) {
            // 把包含 choose ∪ {i} 的集合構出來
            vector<bool> nxt_match = match;
            for (int j = 1; j <= N; ++j) {
                nxt_match[j] = nxt_match[j] & in_sets[i][j];
            }
            choose.push_back(i); // choose <- choose ∪ {i}
            dfs(i, nxt_match);   // 遞迴下去處理
            choose.pop_back();   // choose <- choose - {i}
        }
    }
}
```

最後的分數需要使用 $\texttt{std::bitset}$ 來優化。這是一個常見的優化技巧，如果你要儲存的值只包含 $0$ 跟 $1$，且在計算上常常使用到 $\texttt{bitwise-AND}$、$\texttt{bitwise-OR}$、或是 $\texttt{XOR}$ 就可以使用他，他的常數會比正常實作還要小 $32 \sim 64$ 倍。

觀察到 $\texttt{vector<bool>}$ 可以使用 $\texttt{bitset}$ 代替掉，所以 code 會變成這樣，實作上也非常簡潔：

```cpp=
bitset<N> in_sets[C];
vector<vector<int>> answers;
vector<int> choose;

void dfs(int now, bitset<N> match) {
    answers.push_back(choose);
    for (int i = now+1; i <= C; ++i) {
        if ((int)(match & in_sets[i]).count() >= K) {
            choose.push_back(i);
            dfs(i, match & in_sets[i]);
            choose.pop_back();
        }
    }
}
```

複雜度是 $\mathcal{O}(ans \times C \times \frac{N}{\text{bitset}}) \approx 1.7 \times 10^9$，而實際上常數還會比這個小好幾倍，可以在 $1.5$ 秒左右跑完。

