--- 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$ 秒左右跑完。