--- tags: tutorial --- # 2022 校內資訊學科能力競賽初選初賽 題解 ## p1. GT 教社交 `tags: sorting` ### Subtask 1: (10%) 開個長度為 $L$ 的陣列,並且對於 $N$ 個人花 $O(L)$ 的時間維護好陣列即可 時間複雜度: $O(NL)$ ### Subtask 2: (20%) 可以發現子題一的複雜度很糟糕,這時候就需要知道[差分](https://oi-wiki.org/basic/prefix-sum/#%E5%B7%AE%E5%88%86)的技巧。 時間複雜度: $O(N + L)$ ### Subtask 3: (70%) 想一下能知道我們其實不需用開長度 $O(L)$ 的陣列,因為最多只會有 $O(N)$ 個需要被記錄的值,我們能開一個 `vector<pair<int, int>>`,裡面存 `pair<要更改的位子, 要更改的值>`,排序 `vec` 後,好好判斷哪些元素是要同時處理,就能知道該位子的值是多少。 需要注意的細節是 Benson 只能在 [1, L] , 有看清楚題敘應該就能輕鬆 AC 此題。 ## p2. Benson 打排球 `tags: dp` 背包問題變化 ### Subtask 1: (10%) 因為 $K=L$ 代表不管那天增加了多少疲勞度,只要不肌肉拉傷都會歸零,而 $a_i$ 為正整數,所以這個 Subtask 可以直接 greedy 對於每個位置求當前位置 $a_i$ 加上前綴不爆疲勞度的總和,最後取 max 就會通過這筆測資。 ### Subtask 2: (20%) $b_i<L$ 其實沒什麼意義,但 $K=0$ 代表 dp 前不用平移,除了可以超出 capacity 一次之外就是一個標準的背包問題,只要額外記錄當前不爆上限的最大值 + 當前值就會通過。 ### Subtask 3: (70%) 滿分解轉移式: $dp[i][j]$ 為考慮前 $i$ 天的練習及疲勞度為 $j$ 時候的最大社交度,其中 $j=L$ 代表所有爆疲勞度的情況,因此注意不會轉移到其他疲勞度的情況。 $\forall j<L$ 選擇該天的練習 $dp[i+1][\max(0,j-K)]=\max(dp[i+1][\max(0,j-K)],dp[i][j])$ 選擇該天不練習 $dp[i+1][\min(L,b[i]+\max(0,j-K))]=\max(dp[i+1][\min(L,b[i]+\max(0,j-K))],dp[i][j]+a[i])$ 滿分解要注意的是, $K>0$ 的時候不是 1 對 1 的轉移,所以像一般背包問題一樣壓成一個一維 array 比較困難。這時候可以開兩個 array 交替使用。 :::spoiler **code** by *konchin.shih* ```cpp= #include<iostream> #include<algorithm> #include<vector> using namespace std; template<typename T> using V=vector<T>; using ll=long long; constexpr ll infll=0x3f3f3f3f3f3f3f3f; inline void solve(){ int n,k,l;cin>>n>>k>>l; V<ll> a(n),b(n); for(auto& i:a) cin>>i; for(auto& i:b) cin>>i; V<V<ll>> dp(2,V<ll>(l+1,-infll)); dp[0][0]=0; for(int i=0;i<n;i++){ fill(dp[~i&1].begin(),dp[~i&1].end(),-infll); dp[~i&1][l]=dp[i&1][l]; for(int j=0;j<l;j++){ auto from=dp[i&1][j]; if(from==-infll) continue; auto& to1=dp[~i&1][max(0,j-k)]; auto& to2=dp[~i&1][min(max(0,j-k)+b[i],(ll)l)]; to1=max(to1,from); to2=max(to2,a[i]+from); } } cout<<*max_element(dp[n&1].begin(),dp[n&1].end())<<endl; } int main(){ solve(); return 0; } ``` ::: ## p3. 青椒小徑 `tags: bfs` 這題就是非常裸的 bfs 題,只要好好判能走到的格子、是否走訪過,或是會不會超出邊界、採到障礙物,就能AC了。 實作細節因人而異,大部分人走相鄰的格子會這樣寫 ```cpp const int dx[] = {0, 0, 1, -1}; const int dy[] = {1, -1, 0, 0}; int curX, curY; for(int i = 0; i < 4; ++i) { int nextX = curX + dx[i]; int nextY = curY + dy[i]; if(isvalid(nextX, nextY)) { .... } } ``` 走小徑(其他的邊)就是 ```cpp // pair<int, int> -> X, Y vector<pair<int, int>> G[/*X*/][/*Y*/]; void add_edge(int x, int y, int tox, int toy) { // undirected edge G[x][y].emplace_back(tox, toy); G[tox][toy].emplace_back(x, y); } void bfs() { //... //... for(auto& nxt : G[curX][curY]) { // nxt.first is nxtX // nxt.second is nxtY if(isvaild(nxt.first, nxt.second)) { // ... } } } ``` 再次強調實作細節因人而異,找到自己習慣且穩定的寫法就好。 ## p4. 樹上最大值 `tags: tree, dp` 換根 dp 經典題(其實出題前我根本不知道QQ)。 ### Subtask 1: (20%) 對每一個點做一次 dfs,算 max 即可,時間複雜度為 $O(n^2)$。 ### Subtask 2: (80%) 以範例測資舉例: ![](https://i.imgur.com/BNDPhKF.png) 仔細觀察即可發現,令原本的答案為 $ans$,若從編號為 $u$ 為根節點轉移到 $v$ 時,更新的答案為 $ans'=ans-\sum a_i\in subtree\{v\}+\sum a_i\in subtree\{u\}$。好好對任意一點做一次 dfs 紀錄答案,再從該點去對每一個鄰近的點做 dfs 轉移答案,即可在 $O(n)$ 的時間複雜度完成。 ## p5. GT 教遊戲 `tags: binary_search` ### Greedy的想法 (?) 假設我們在第 $i - 1$ 回合選取某玩家後,可以獲得目前最大的$\frac{X_0}{Y_0}$,那麼是否能運用算出來的 $\frac{X_0}{Y_0}$ 去檢查在第 $i$ 回合,該選取誰作為代表呢 ? 我們可以令在第 $i$ 回合我們將要選取的玩家,手上的兩張牌為$a, b\; (a \geq b)$,並且在第 $i - 1$ 回合我們在選取某個玩家後能得到 $\frac{X_1}{Y_1} \lt \frac{X_0}{Y_0}$ 的結果,那麼在第 $i$ 回合,可能會有 $\frac{X_0 + a}{Y_0 + b}$ 及 $\frac{X_1 + a}{Y_1 + b}$ 這兩個結果。 可以發現 $\frac{X_0 + a}{Y_0 + b}$ 不一定會大於 $\frac{X_1 + a}{Y_1 + b}$。 (e.g. $\frac{X_0}{Y_0} = \frac{10}{1}$, $\frac{X_1}{Y_1} = \frac{30}{5}$, $\frac{a}{b} = \frac{11}{10}$) 因此沒辦法透過記錄最大值來進行 greedy QQ ### Subtask 1: (10%) > $N \leq 10,\; C \leq 10$ 暴力搜尋每回合選三個不同玩家作為代表的可能,假設被選取作為代表的玩家,手上的兩張牌為$a, b\; (a \geq b)$,把 $a$ 加進 $X$ , $b$ 加進 $Y$ 即可。 時間複雜度: $O(3^N)$ ### Subtask 2: (20%) > $N \leq 20,\; C \leq 50$ > 賽中題敘: $N \leq 100,\; C \leq 100$ (忘記修正題敘真的很抱歉 QQ) 因為 $\frac{X}{Y} \leq N \times C$,並且精度只要求到小數後 4 位,所以對於子題二而言,可以從 1000.0000 枚舉到 0.0000 ( $N \times C \times 10^4 = 10^7$ 次),檢查 $\frac{X}{Y}$ 是否有可能出現。 假設有一個長 $N$ 的陣列 $choose$,$choose[i]$ 代表在第 $i$ 回合選擇了哪位玩家作為代表,那麼依據陣列 $choose$ 所得出的最後 $\frac{X}{Y}$ $$\frac{X}{Y} = \frac{ \sum{choose[i]_{i1}} }{ \sum{choose[i]_{i2}}}$$ 也就是說如果要檢查一個浮點數 $K$ 是否有可能出現,只要找到一個陣列 $choose$,滿足 $$K \leq \frac{ \sum{choose[i]_{i1}} }{ \sum{choose[i]_{i2}}}$$ 移項後可得 $$0 \leq \sum{choose[i]_{i1}} - \sum{choose[i]_{i2}} \times K$$ 透過式子可以知道每個回合的選擇是分開的,假設目前是第 $i$ 回合,那我們的目標就是最大化 $choose[i]_{i1} - choose[i]_{i2} \times K$,因此只要計算完所有玩家的點數,代入算式後的結果後,就能知道要選誰了。 時間複雜度: $O(N^2 \cdot C \cdot 10^4)$ ### Subtask 3: (70%) > $N \leq 10^5,\; C \leq 10^8$ 如果會做子題二的話,不難發現我們要檢查的浮點數 $K$ 是具有單調性的。 好好計算要浮點數二分搜的次數後 (或是檢查 r - l < eps),即可AC 時間複雜度: $O(\; N \; log( N \cdot C \cdot 10^4)\;)$