# 113學年度建國中學校內資訊能力競賽 初試題解 ---- ## 難度 $A<B<C \approx D \approx E$ --- # pA Hangman with Artificial Idiot ## Setter : pacybwoah ---- ### Subtask 1:$A=B$ 字串都一樣的時候會發生什麼事情? ---- ### Subtask 1:$A=B$ 假設大家的字串都是 sass A : sass B : sass ---- ### Subtask 1:$A=B$ 你猜了 s 之後 A : <span style="color: #32a852;">s</span>a<span style="color: #32a852;">ss</span> B : <span style="color: #32a852;">s</span>a<span style="color: #32a852;">ss</span> ---- ### Subtask 1:$A=B$ 你猜了 a 之後 A : <span style="color: #32a852;">sass</span> B : <span style="color: #32a852;">sass</span> ---- ### Subtask 1:$A=B$ 不管怎麼樣都會平手 => cout << "No\n"; ---- ### Subtask 2:$n, m \leq 100$,$A,B$ 由 a~h 的小寫字母組成 對每種猜字母的方法用 next_permutation 暴力模擬,複雜度 $O(n \cdot |c|!)$ ---- ### Subtask 3:無特別限制 模擬遊戲的過程中應該會發現一件事:字母出現的次數與答案沒關係,只需要在乎有沒有出現 ---- ### Subtask 3:無特別限制 如果所有出現在 $A$ 裡的字母都出現在 $B$ 中,那當你猜完 $B$ 的所有字母,$A$ 的所有字母也會被猜中,因此不可能贏 ---- ### Subtask 3:無特別限制 如果一個字母出現在 $A$ 內但沒有出現在 $B$ 內,那你猜中 $B$ 時 $A$ 可能有字母還沒被猜到,因此有可能贏 ---- ### Subtask 3:無特別限制 開一個布林陣列紀錄字母有沒有出現在兩個字串並檢查上述條件,複雜度 $O(n + |c|)$ ---- ### Code ``` #include<iostream> #include<vector> #include<string> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(0); int n, m; cin >> n >> m; string a, b; cin >> a >> b; vector<bool> visa(26), visb(26); for(int i = 0; i < n; i++) visa[a[i] - 'a'] = 1; for(int i = 0; i < m; i++) visb[b[i] - 'a'] = 1; for(int i = 0; i < 26; i++){ if(visa[i] == 1 && visb[i] == 0){ cout << "Yes\n"; return 0; } } cout << "No\n"; return 0; } ``` ---- --- # pB 小祥的生日禮物 ## Setter : becaido ---- ### Subtask 1:$a_i>0$($1\le i\le n$) ---- 可以選 $1$,直接把所有點都標記起來。這樣子字典序會是最大的,因為不管怎麼選都沒辦法超過總和。 ---- ### Subtask 2:$a_i=0$($1\le i\le n$) ---- 注意到 $[0,0,0,0]$ 的字典序大於 $[0,0,0]$。也就是說,$0$ 越多字典序越大。 ---- 如果從 $n$ 開始標記到 $1$,那每次都只會標記到自己,最後的陣列中會有 $n$ 個 $0$,字典序最大。 ---- ### Subtask 3:恰好有一個 $i$($1\le i\le n$),使得 $a_i>0$ ---- 想要把 $a_i$ 標記走,只能選他或是他的因數。因為留下來越多 $0$ 越好,所以應該標記 $a_i$,再把剩下還沒被標記的 $0$ 從大到小標記。 ---- ### Subtask 4:無特別限制 ---- 因為可以直接選 $1$ 標記走所有數字,所以第一個選的數字一定要能把所有大於 $0$ 的數字都標記走! ---- 解法 1: 對於一個數字來說,只要被選的數字是他的因數,這個數字就會被標記到。因此,第一個選的數字一定會是所有大於 0 的數字位置的最大公因數。 ---- 使用內建的 gcd 函數,可以在 $O(n+logn)$ 的時間內得到第一個數字。接著再輸出 $n - \left \lfloor{\frac{n}{gcd}}\right \rfloor$ 個 $0$ 就可以了。 ---- 解法 2: 枚舉所有數字當第一個選的數字,檢查他的倍數總和是否等於全部數字的總和。時間複雜度為 $O(n + \frac{n}{2} + \frac{n}{3} + \dots + \frac{n}{n})$ = $O(nlogn)$ ---- ### Code ``` #include <bits/stdc++.h> using namespace std; const int N = 1e5 + 5; int n; int g, sum, a[N]; int main() { ios::sync_with_stdio(false), cin.tie(0); cin >> n; for (int i = 1; i <= n; i++) cin >> a[i]; for (int i = 1; i <= n; i++) { if (a[i] > 0) { g = gcd(g, i); sum += a[i]; } } if (g == 0) { cout << n << '\n'; for (int i = 1; i <= n; i++) cout << 0 << " \n"[i == n]; return 0; } int zero = n - n / g; cout << zero + 1 << '\n'; cout << sum; for (int i = 1; i <= zero; i++) cout << " 0"; cout << '\n'; } ``` --- # pC 見習魔法師宜恩 ## Setter : owoovo ---- ## Subtask 1 $N,a_i$很小 發現到會減少時間一定是把$x+1,x,x,...,x,x+1$中間的$x$變成$x+1$(填洞) 而且這樣都是減少兩單位的時間 ---- 把可以補的洞切成一段一段 ![image](https://hackmd.io/_uploads/ryEkszT2A.png) ---- 把每一段照花費排序 從花費小的開始拿 ---- ## 真的嗎 真的 因為在其他洞上面的洞一定不會比較小 ---- 最多會有$O(cN)$個洞 複雜度:$O(cN\log{cN})$ 如果沒有好好找洞會被卡記憶體 ---- ## Subtask 2 只有一個大洞 一定會從下面填上來 ---- 要找到最大的$x$讓$\displaystyle\sum_{i=1}^N{\max(0, x-a_i)}\leq K$ 把$a_i$排序後二分搜或好好數數 ---- ## full 現在的問題在洞太多了沒有辦法一個一個找(存) 有沒有辦法一起找(存)? ---- 以$a_i$當左邊的洞在遇到一個$\geq a_i$的人之後就沒有了 單調stack! 以stack存現在還能當左邊的人 每遇到新的一座山就把一些洞的(寬度,高度)存起來 把他們照寬度排序然後好好加就做完了! 複雜度:$O(N\log{N})$ ---- ## Code ``` #include<bits/stdc++.h> #define ll long long #define F first #define S second using namespace std; ll n,k,a[1000010]; vector<pair<ll,ll>> use; int main(){ cin>>n>>k; for(int i=1;i<=n;i++)cin>>a[i]; ll ans=0; for(int i=1;i<n;i++){ ans+=abs(a[i]-a[i+1]); } stack<pair<ll,ll>> sk; for(int i=1;i<=n;i++){ while(!sk.empty()&&a[i]>=sk.top().F){ auto [val,pos]=sk.top(); sk.pop(); if(!sk.empty()){ auto [val2,pos2]=sk.top(); use.push_back({i-pos2-1,min(val2,a[i])-val}); } } sk.push(make_pair(a[i],i)); } sort(use.begin(),use.end()); for(auto [val,t]:use){ if(val*t<=k){ k-=val*t; ans-=t*2; }else{ ll T=k/val; k-=val*T; ans-=T*2; break; } } cout<<ans<<"\n"; } ``` --- # pD 電梯難題 ## Setter : pacybwoah ---- 為了方便,我們讓$s_0=e_0=0$ ---- ## Subtask 1 $k=1$ 國中數學 算$\displaystyle\sum_{i=1}^n(\mid s_{i}-e_{i-1} \mid+\mid e_i-s_i\mid)$ ---- ## Subtask 2 $n\leq 20$ 枚舉所有給兩座電梯的分法再分別把他們當作$k=1$ 複雜度:$O(n\times 2^n)$ ---- ## Subtask 3 $n\leq 5000$ 發現到在送完第$i$組人後一定有一座電梯在$e_i$ 考慮定義$dp$狀態 $dp_{i,j}$為載完前$i$組人,電梯在$e_i,e_j$需要的最小時間 ---- $$ dp_{i,j} = \begin{cases} dp_{i-1,j}+\mid s_{i}-e_{i-1} \mid+\mid e_i-s_i\mid & \text{if } j \neq i-1 \\ \min\{dp_{i-1,k}+\mid s_{i}-e_{k} \mid+\mid e_i-s_i\mid\} &\text{if } j=i-1 \end{cases} $$ 轉移總共只有$O(n^2)$項 ---- ## full 我們再來觀察一下dp式 狀態太多了! 換一種寫法試試看 發現到如果第$i,i-1$組人用不同電梯 它們的位置會是$e_i,e_{i-1}$ 我們設$dp_i$為第$i,i-1$組人用不同電梯時接完前$i$組最少的時間 ---- ## dp式 $dp_i=\min_{j<i}\{dp_j+\displaystyle \sum_{k=j+1}^{i} \mid e_{k}-s_{k} \mid$ $+\displaystyle\sum_{k=j+1}^{i-1}\mid s_{k}-e_{k-1} \mid+\mid s_i-e_{j-1}\mid\}$ ---- 大家都有$\mid e_i-s_i\mid$ 把$\displaystyle\sum_{i=1}^n\mid e_i-s_i\mid$拿出來,算完dp後再加回去 要區間的加$\mid e_i-s_{i-1}\mid$,讓$\displaystyle pre_i=\sum_{j=1}^i{\mid s_j-e_{j-1} \mid}$ 接著用前綴和算 ---- ## 現在的dp式 $dp_i=\min_{j<i}\{dp_j +pre_{i-1}-pre_j+\mid s_i-e_{j-1}\mid\}$<br /> 把$dp'_i$設為$dp_i-pre_i$ $dp'_i=\mid s_i-e_{i-1}\mid+\min_{j<i}\{dp'_j +\mid s_i-e_{j-1}\mid\}$ ---- 把絕對值拆開來 $$ dp'_i = \mid s_i-e_{i-1}\mid+\min_{j<i}\begin{cases} dp'_j-e_{j-1}+s_i & \text{if } e_{j-1} \leq s_i \\ dp'_j+e_{j-1}-s_i & \text{otherwise. } \end{cases} $$ 對$e_i$開兩顆線段樹 存$dp'_i-e_{i-1}$和$dp'_i+e_{i-1}$分別的最小值 (要記得離散化) 這樣就做完了! 複雜度:$O(n\log n)$ ---- ## 還有一件事 發現到轉移的詢問其實是一個前(後)綴 BIT! ---- ## Code ``` #include<iostream> #include<vector> #include<algorithm> #include<utility> #include<cmath> using namespace std; typedef long long ll; const ll inf = 1e18; struct BIT{ int n; vector<ll> bit; BIT(int _n){ n = _n; bit.resize(n + 1); fill(bit.begin(), bit.end(), inf); } void modify(int pos, ll num){ while(pos <= n){ bit[pos] = min(bit[pos], num); pos += pos & -pos; } } ll query(int pos){ ll ans = inf; while(pos > 0){ ans = min(ans, bit[pos]); pos -= pos & -pos; } return ans; } }; int main(){ ios::sync_with_stdio(false); cin.tie(0); int n, k; cin >> k >> n; vector<pair<ll, ll>> vec(n + 1), cp(n + 1); vector<ll> cc; for(int i = 1; i <= n; i++){ cin >> vec[i].first >> vec[i].second; cc.push_back(vec[i].first); cc.push_back(vec[i].second); } cc.push_back(0); sort(cc.begin(), cc.end()); cc.resize(unique(cc.begin(), cc.end()) - cc.begin()); int sz = cc.size(); for(int i = 1; i <= n; i++){ cp[i].first = lower_bound(cc.begin(), cc.end(), vec[i].first) - cc.begin() + 1; cp[i].second = lower_bound(cc.begin(), cc.end(), vec[i].second) - cc.begin() + 1; } vec[0] = make_pair(0, 0); ll ans = 1e18, last = 0; if(k == 1){ ans = 0; for(auto &[s, e]: vec){ ans += llabs(s - last); ans += llabs(e - s); last = e; } cout << ans << "\n"; return 0; } for(auto &[s, e]: vec) last += llabs(e - s); vector<ll> suf(n + 1), dp(n + 1); suf[n] = llabs(vec[n].first - vec[n - 1].second); for(int i = n - 1; i > 0; i--) suf[i] = suf[i + 1] + llabs(vec[i].first - vec[i - 1].second); dp[1] = vec[1].first; BIT cis(sz), trans(sz); cis.modify(1, dp[1]); trans.modify(sz, dp[1]); ll rest = 0; for(int i = 2; i <= n; i++){ dp[i] = min(cis.query(cp[i].first) + vec[i].first, trans.query(sz + 1 - cp[i].first) - vec[i].first) + rest; rest += llabs(vec[i].first - vec[i - 1].second); cis.modify(cp[i - 1].second, dp[i] - vec[i - 1].second - rest); trans.modify(sz + 1 - cp[i - 1].second, dp[i] + vec[i - 1].second - rest); } ans = dp[n]; for(int i = 1; i < n; i++) ans = min(ans, dp[i] + suf[i + 1]); cout << ans + last << "\n"; } ``` --- # pE 火車難題 ## Setter : PCC ---- ### 題目好長不想看? 簡短題敘:有一個邊權都是 1 的無向圖,邊上會有一些泡泡紙,你想要在走最短距離的情況下踩最多張泡泡紙。問從每個點到 $n$ 最多能踩幾張泡泡紙。 ---- ### 題目好長不想看? 但是不是所有邊都可以走,轉彎需要花錢。對於每個點來說,都會有一個指針指向第一條邊,想要往別條邊走需要花錢。每個點目前有 $c_i$ 元,可以花 $a_i$ 元把指針往後搬 $x_i$ 格,或花 $b_i$ 元把指針往後搬 $y_i$ 格 ---- ### Subtask 1:$a_i = b_i > c_i$ ---- 完全不能移指針!那有些邊就完全不能用到了。不是第 $1$ 條邊的邊都可以刪掉。 ---- 「所有到終點的最短路」其實就是把邊反轉之後的「所有從起點開始的最短路」。可以 BFS,過程中順便紀錄最多能踩幾張泡泡紙。複雜度 $O(n + m)$ ---- ### Subtask 2、3:$x_i = y_i = 1,b_i = a_i$ ---- 每次可以把指針往後移一格,最多可以往後移 $\left \lfloor{\frac{c_i}{a_i}}\right \rfloor$ 格。一樣把移不到的邊都拔掉,在剩下的圖上跑 BFS。 ---- 值得注意的是,每個點的指針是各自獨立的。接下來都會對每個點各自算出哪些邊可以用,哪些邊不能用,把不能用的邊刪掉後再跑 BFS。 ---- ### Subtask 4:$a_i = b_i,x_i = y_i$ ---- 只有一個指針。可以一直把指針往後移 $x_i$ 格,把指針可以移到的邊都標記起來。如果指針移到已經走過的邊,就代表陷入循環,可以跳出。 ---- 因為走完所有邊之前一定會陷入循環,所以每個點只會花 $O(d_i)$ 的時間確認。總複雜度為 $O(n + m)$ ---- ### Subtask 5、6:無其他限制 ---- 有兩個指針。如果我們對所有邊算出「把指針移到這裡的最小花費」,就知道哪些邊可以用了。 ---- 假設指針現在指向第 $p$ 條邊,那我可以花 $a_i$ 讓他指向第 $(p + x_i - 1) \text{ mod } d_i + 1$ 條邊,或是花 $b_i$ 讓他指向第 $(p + y_i - 1) \text{ mod } d_i + 1$ 條邊 有沒有覺得很像什麼東西? ---- 最短路!對每個車站來說,可以幫所有軌道建一個點,每個點建兩條邊出去,分別代表用 $a_i$ 跟用 $b_i$ 的情況。把一號軌道的距離設為 $0$ ,跑 Dijkstra 之後就可以把距離 $> c_i$ 的邊刪掉。最後在新圖上跑 BFS 就是答案了。複雜度 $O(m\log m + n)$ ---- ### Code ```cpp #include <bits/stdc++.h> using namespace std; #define ll long long #define pll pair<ll,ll> #define pii pair<int,int> #define fs first #define sc second #define tlll tuple<ll,ll,ll> const int mxn = 2e5+10; const ll inf = 2e18; vector<pii> all[mxn]; vector<pii> paths[mxn]; int a[mxn],b[mxn],x[mxn],y[mxn]; ll c[mxn]; int N; ll dist[mxn]; bool vis[mxn]; ll ans[mxn]; void calc(int id){ int d = all[id].size(); priority_queue<pll,vector<pll>,greater<pll>> pq; fill(dist,dist+d,inf); fill(vis,vis+d,0); dist[0] = 0; pq.push(pll(dist[0],0)); while(!pq.empty()){ auto [_,now] = pq.top();pq.pop(); if(vis[now])continue; vis[now] = true; int nxt = (now+x[id])%d; if(!vis[nxt]&&dist[nxt]>dist[now]+a[id]){ dist[nxt] = dist[now]+a[id]; pq.push(pll(dist[nxt],nxt)); } nxt = (now+y[id])%d; if(!vis[nxt]&&dist[nxt]>dist[now]+b[id]){ dist[nxt] = dist[now]+b[id]; pq.push(pll(dist[nxt],nxt)); } } for(int i = 0;i<d;i++){ auto [nxt,dis] = all[id][i]; if(dist[i]>c[id])continue; paths[nxt].push_back(pii(id,dis)); } return; } namespace BFS{ pll dp[mxn]; void GO(int s){ queue<int> q; for(int i = 1;i<=N;i++)dp[i] = pll(-1,-1); dp[s] = pll(0,0); q.push(s); while(!q.empty()){ auto now = q.front(); q.pop(); for(auto [nxt,w]:paths[now]){ if(dp[nxt].fs == -1){ dp[nxt] = pll(dp[now].fs+1,dp[now].sc+w); q.push(nxt); } else if(dp[nxt].fs == dp[now].fs+1)dp[nxt].sc = max(dp[nxt].sc,dp[now].sc+w); } } for(int i = 1;i<=N;i++)ans[i] = dp[i].sc; return; } } int main(){ ios::sync_with_stdio(0);cin.tie(0);cout.tie(0); cin>>N; for(int i = 1;i<=N;i++){ int d; cin>>d; while(d--){ pii p; cin>>p.fs>>p.sc; all[i].push_back(p); } } for(int i = 1;i<=N;i++)cin>>a[i]; for(int i = 1;i<=N;i++)cin>>x[i]; for(int i = 1;i<=N;i++)cin>>b[i]; for(int i = 1;i<=N;i++)cin>>y[i]; for(int i = 1;i<=N;i++)cin>>c[i]; for(int i = 1;i<=N;i++)calc(i); BFS::GO(N); for(int i = 1;i<=N;i++)cout<<ans[i]<<'\n'; return 0; } ```
{"title":"建中資訊校內賽題解","contributors":"[{\"id\":\"80534ff6-e4c7-48c5-88c7-d13ac225a4c2\",\"add\":3351,\"del\":283},{\"id\":\"52026c9b-045c-4e64-9cc6-78986ed19085\",\"add\":201,\"del\":11},{\"id\":\"7816456f-346c-479d-bcac-746977dfdcea\",\"add\":8621,\"del\":106}]","description":"A<B<C \\approx D<E"}
    1017 views
   owned this note