第九屆高一生程式設計排名賽題解 === ### A. 獅子的野望 (yabo) 題目要求求出 $\lceil \frac{L}{K} \rceil$,然後在找出序列 $a$ 中差距最小的元素即可。 ```cpp #include <bits/stdc++.h> #define int long long using namespace std; const int N = 1e6+10; int n, k, l, a; signed main() { cin >> l >> k >> n; int mid=0, mv=1e18; for (int i=1; i<=n; i++) { cin >> a; if (mv > abs((l-1)/k+1-a)) { mv = abs((l-1)/k+1-a); mid = i; } } cout << mid << '\n'; } ``` ### B. 爆音爆音 (boing) #### Subtask 1 用 `next_permutation` 枚舉每一種排列方式即可,複雜度 $O(N!)$ #### Subtask 2 用依序加入元素的方式來製造排列 $p$,$sum$ 為目前總和也就是 $a_{p_1}+...+a_{p_k}$。每次加入 $i$ 時,若 $a_i \leq sum$ 加入後最終答案只會更差,因此只考慮 $i$ 使 $a_i > sum$;接著選擇符合的 $a_i$ 中最小的,答案不會更差。 因此,紀錄一個 $sum$ 代表目前已選擇元素的總和,每次遍歷所有元素。從尚未挑選過的元素且大於 $sum$ 中選擇最小的,並加入 $sum$。直到不存在任何一個元素大於 $sum$,剩下未被挑選的元素任意排列即可。時間 $O(N^2)$ 。 #### Subtask 3&4 "每次遍歷所有元素。從尚未挑選過的元素且大於 $sum$ 中選擇最小的" 可以以 `set` 在 $O(logN)$ 實現。時間 $O(NlogN)$。 ```cpp #include <bits/stdc++.h> #define int long long using namespace std; const int N = 1e6+10; int n; set<int> s; signed main() { cin >> n; for (int i=1; i<=n; i++) { int x; cin >> x; s.insert(x); } int sum = 0, cnt = 0; for (int i=1; i<=n; i++) { auto x = s.upper_bound(sum); if (x == s.end()) break; sum += *x; s.erase(x); cnt++; } cout << cnt << endl; } ``` ### C. 手指餅乾 (yubi) #### Subtask 1 可以以 $O(2^N)$ 枚舉每個餅乾要不要補上麵團。 #### Subtask 2 $a_i$ 只有兩種可能,$1$ 或 $2$。若 $a_i$ 為 $1$ 則補上麵團,則每個手指餅乾都會是 $2$。 #### Subtask 3&4 以 $O(\sqrt{N})$ 可以枚舉一個數字的所有因數。枚舉 $a_i$ 和 $a_i+1$ 的所有因數,對 $a_2, .., a_N$ 檢查補上麵團或不補上麵團否至少一個被該因數整除。 時間 $O(N \sqrt{N})$ 。 #### Subtask 5 可以從 Subtask 2 獲得啟發,只要把每個數字補成偶數即可。**用 Subtask 3&4 的作法,枚舉到的第一個因數就會是 $2$,也可通過此子題 OwO。** ```cpp #include <bits/stdc++.h> #define int long long using namespace std; const int N = 1e6+10; int n, a[N]; signed main() { cin >> n; for (int i=1; i<=n; i++) cin >> a[i]; for (int i=1; i<=n; i++) if (a[i]&1) a[i]++; for (int i=1; i<=n; i++) cout << a[i] << "\n "[i+1<=n]; } ``` ### D. 大型貼貼現場 (teetee) #### Subtask 1 若 $a_{i,j} \leq 0$ 就全部不要連,否則每個人都跟對面的人連,答案即為 $max(0,N \times a_{i,j})$ #### Subtask 2 也就是第一排的人跟第二排的哪個人連都沒有區別。因此第一排的人都優先考慮連自己正對面的人,若 $a_{i,j} \leq 0$ 則第 $i$ 人不參與配對即可。 #### Subtask 3 暴力枚舉第一排的人要跟第二排的哪個人配對,複雜度 $O(N!)$ 。 #### Subtask 4 考慮動態規劃,$dp[i][j]$ 代表第一排的 $[i,N]$ 和第二排 $[j,N]$ 參與配對。 轉移情況有三種: 1. 第一排第 $i$ 人不參與配對 2. 第二排第 $j$ 人不參與配對 3. 第一排第 $i$ 人和第二排第 $j$ 人配對,產生 $a_{i,j}$ 價值 因此轉移為 $dp[i][j] = max( dp[i+1][j], dp[i][j+1], dp[i+1][j+1]+a[i][j])$。 ```cpp #include <bits/stdc++.h> #define int long long using namespace std; const int N = 2001; int a[N][N], n, dp[N][N]; signed main() { cin >> n; for (int i=1; i<=n; i++) for (int j=1; j<=n; j++) cin >> a[i][j]; for (int i = n; i>0; i--) for (int j=n; j>0; j--) dp[i][j] = max(max(dp[i+1][j], dp[i][j+1]), dp[i+1][j+1]+a[i][j]); cout << dp[1][1] << endl; } ``` ### E. 裝水 (water) #### Subtask 1 首先必須觀察出,擴大水桶的步驟全部做完再把水倒入鍋釜,是比較好的。 因此先從 $1$ 到 $N$ 枚舉需要消耗多少魔力擴大多少次水桶,每次再算出需要消耗多少體力倒入鍋釜,取相加最小值,複雜度 $O(N^2)$。 #### Subtask 2 擴大 $i$ 次水桶後的水桶容量,即為 $1+(1+2+3+...+i)$,將 $1+2+3+...+i$ 用 $\frac{(1+i) \times i}{2}$ 公式計算可以優化到 $O(N)$。 #### Subtask 3 當擴大水桶後的容量至少為 $N$,只要消耗一體力倒入鍋釜。因此只要枚舉擴大次數 $i$ 直到 $1+(1+2+3+...+i) > N$ 即可,由上面的公式可以得知枚舉不超過 $\sqrt{2N}$ 次。複雜度 $O(\sqrt{N})$ #### Subtask 4 $f(i)$ 為擴大$i$ 次的總體力+魔力消耗,$f(i) \approx i + \frac{2N}{i(i+1)}$,$f(i+1) - f(i) = 1-\frac{4N}{i^3+3i^2+2i}$,因此當 $i \geq (4N)^\frac13$ 時 $f(i+1) - f(i)$ 會 $\geq 0$,因此答案出現在 $i \leq (4N)^\frac13$ 另一個不嚴謹的證明是,把 $i+1$ 當成 $i$ 做算幾,當 $\frac{i}{2} = \frac{2N}{i^2}$ 時有最小值。 ```cpp #include <bits/stdc++.h> #define int long long using namespace std; signed main() { int n; cin >> n; int ans = 9e18; for (int i=1; i<=10000000; i++) { int k = i*(i+1)/2+1; ans = min(ans, (n-1)/k+1+i); } cout << ans << endl; } ``` ### F.七彩繽紛銀河麵 (udon) #### subtask1 無論怎麼配都不會損失美味程度,所以把每個調味料配給跟他絕配的麵當中,美味程度最高的就好了。 複雜度$O(N)$。 #### subtask2 因為$N$很小,暴力去試每一種組合,再取美味程度最大的組合。 複雜度$O(N!)$。 #### subtask3, 4 我們可以把題目轉換成二分圖最大權匹配,直接套km。 對於地獄組合的負邊權,只要把他們都加上一個數$x$,使得他們的邊權都變成正的,然後輸出的時候記得再減掉$N*x$就可以了。 複雜度$O(N^3)$。 #### subtask5 跟subtask1一樣,先把每個調味料配給跟他絕配的麵當中,美味程度最高的配給他。 而對於剩下沒被配到的麵,可以分成兩種case : 1. 剩下的麵當中,都不和同一個調味料產生地獄組合。 這時候一定找的到一種配法,使得剩下的麵都不會配到會跟自己產生地獄組合 的調味料,所以直接輸出跟subtask1作法一樣的答案就好了。 2. 剩下的麵當中,都會和同一個調味料產生地獄組合。 一個最直觀的想法就是把損失最少美味程度的麵配給那個調味料。 但這不一定會是最大的美味程度,這時候把先前已經配對過的麵,拿來配那個調味料,如果拿來配對的麵不會跟調味料產生地獄組合,就可以使損失的美味程度變成0,美味程度總和可能會更大。 所以就枚舉把每個先前已經被配到的麵配給那個調味料,並且把配那個麵的調味料中,美味程度次高的麵配給他,然後再取max。 複雜度$O(N)$。 ```cpp #include<bits/stdc++.h> using namespace std; #define ll long long const ll mx = 5e5+5; ll n, match_id[mx], match_val[mx], match_val2[mx], hateid[mx], hatev[mx]; bool matched[mx]; signed main(){ ios::sync_with_stdio(0), cin.tie(0); cin >> n; // 輸入並對每種調味料找到與之形成絕配中美味程度最高與次高的 for(int i = 1, soft, v; i <= n; i++){ cin >> soft >> v; if(v > match_val[soft]){ match_id[soft] = i; match_val2[soft] = match_val[soft]; match_val[soft] = v; } else if (v > match_val2[soft]){ match_val2[soft] = v; } } // 看剩下的那些麵團與哪一種調味料形成地獄組合 for(int i = 1; i <= n; i++){ cin >> hateid[i] >> hatev[i]; } ll cur = 0; for(int i = 1; i <= n; i++){ matched[match_id[i]] = 1; cur += match_val[i]; } bool all_hate_same = 1; ll be_hated = -1, mn = 1e18; for(int i = 1; i <= n; i++){ if(!matched[i]){ if(hateid[i] == 0){ all_hate_same = 0; break; } if(be_hated == -1) be_hated = hateid[i], mn = hatev[i]; else{ if(hateid[i] != be_hated) all_hate_same = 0; else{ mn = min(hatev[i], mn); } } } } // 沒有調味料會形成地獄組合 or 不是與同一種調味料產生地獄組合 or 會產生地獄組合的調味料有人配對了 if(be_hated == -1 || match_val[be_hated] > 0 || !all_hate_same){ cout << cur << '\n'; return 0; } ll ans = cur - mn; // 枚舉配對完成的麵團中,誰要被換下來與會使所有麵團產生地獄組合的調味料配對。 for(int i = 1; i <= n; i++){ if(match_id[i] != 0){ int id = match_id[i]; ll loss = match_val[i] - match_val2[i]; if(hateid[id] == be_hated){ loss += min(hatev[id], mn); } ans = max(ans, cur - loss); } } cout << ans << '\n'; } ``` --- ### G.爆炸吧現充 (imhorny) #### subtask1 枚舉兩個人都使用砲台,兩個人都不使用砲台,其中一個人使用砲台的情況。 複雜度$O(1)$。 #### subtask2 使用最短路。 把每個座標$i$跟$i+1$及$i-1$連一個邊權為1的邊,每個砲台跟降落點$(x_i \pm d_i)$連一個邊權為0的邊。 做完最短路再去枚舉每個點兩人的最短路,並且取min。 複雜度$O(LlogL)$。 #### subtask3 因為$L$太大,所以沒辦法使用$O(LlogL)$,最多只能用$O(L)$的做法。 參考subtask2的建圖方式,會發現邊權只有0和1,所以其實可以用01bfs去做。 01bfs跟一般bfs不一樣之處在於一般bfs時保證讓queue當中的點,距離從前到後為非嚴格遞增。 但如果有邊權為0的邊,這時候從那條邊去更新其他點,並把它放進queue的最後方,可能會失去距離從前到後為非嚴格遞增這項性質。 這時候可以發現,最前面的點一定是在queue中距離最小者,而從邊權為0的邊所更新的點距離跟他一樣,是queue當中距離最小的,那只要把他塞進queue的頭就可以維護上述的性質了。 而c++有deque可以支援從頭或尾插入,所以把queue改成deque去做01bfs就可以了。 複雜度$O(L)$。 #### subtask4 也是最短路,但因為$L$太大,要改變建點跟建邊的方式。 我們可以將每個砲台所在的座標建成一個點,每個砲台的降落點座標也建成一個點。 把每個砲台跟他的降落點建一條邊權為0的邊,並把每個砲台跟他左右的兩個點建一條邊權為其距離的邊,之後去做最短路得出到每個點的距離。 再來去枚舉每兩個相鄰的點,算出如果在這兩個點的區間上見面所需的最少時間,然後再取min。 複雜度$O(NlogN)$。 ```cpp #include<bits/stdc++.h> #define int long long using namespace std; const int mxN = 5e5 + 5; int L, N, x[mxN], d[mxN], dis[mxN * 3][2]; vector<pair<int,int>> g[mxN * 3]; void dijkstra(int st) { int type = 0; if(st != 0) type = 1; priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> pq; dis[st][type] = 0; pq.push({0, st}); while(!pq.empty()) { auto [d, u] = pq.top(); pq.pop(); if(dis[u][type] < d) continue; for(auto [v, w] : g[u]) { if(dis[v][type] > d + w) { dis[v][type] = d + w; pq.push({dis[v][type], v}); } } } } signed main() { cin >> L >> N; vector<int> tmp; tmp.push_back(0); tmp.push_back(L); for(int i = 0; i < N; i++) { cin >> x[i] >> d[i]; tmp.push_back(x[i]); if(x[i] - d[i] > 0) tmp.push_back(x[i] - d[i]); if(x[i] + d[i] < L) tmp.push_back(x[i] + d[i]); } sort(tmp.begin(), tmp.end()); tmp.resize(unique(tmp.begin(), tmp.end()) - tmp.begin()); for(int i = 0; i < N; i++) { int id = lower_bound(tmp.begin(), tmp.end(), x[i]) - tmp.begin(); int tl = -1, tr = -1; if(x[i] - d[i] >= 0) tl = lower_bound(tmp.begin(), tmp.end(), x[i] - d[i]) - tmp.begin(); if(x[i] + d[i] <= L) tr = lower_bound(tmp.begin(), tmp.end(), x[i] + d[i]) - tmp.begin(); if(tl != -1) g[id].push_back({tl, 0}); if(tr != -1) g[id].push_back({tr, 0}); } for(int i = 1; i < tmp.size(); i++) { g[i].push_back({i-1, tmp[i] - tmp[i-1]}); g[i-1].push_back({i, tmp[i] - tmp[i-1]}); } memset(dis, 0x3f, sizeof dis); dijkstra(0); dijkstra(lower_bound(tmp.begin(), tmp.end(), L) - tmp.begin()); int ans = (L + 1) / 2; for(int i = 0; i < tmp.size(); i++) { if(i) { int td = tmp[i] - tmp[i-1]; if (td >= abs(dis[i][0] - dis[i-1][1])) ans = min(ans, max(dis[i][0], dis[i-1][1]) + ((td - abs(dis[i][0] - dis[i-1][1]) + 1) / 2)); if (td >= abs(dis[i][1] - dis[i-1][0])) ans = min(ans, max(dis[i][1], dis[i-1][0]) + ((td - abs(dis[i][1] - dis[i-1][0]) + 1) / 2)); } ans = min(ans, max(dis[i][0], dis[i][1])); g[i].clear(); } cout << ans << '\n'; } ``` --- ### H.兔田迷宮 (usadamaze) #### subtask1 $N$超小,而且他有給你測資,所以用手解。 #### suntask2,3 可以發現,如果在$i$及$i+1$之間安裝一個傳送器,就會使目前在$i$的朋友跟在$i+1$的朋友位置交換。 而題目的要求是使得交換後朋友們的順序是按照$e_i$由小排到大,這就很容易聯想到Bubble sort,這時候傳送器的數目就相當於時間複雜度。 Bubble sort的時間複雜度為$O(N^2)$,而題目的$N \leq 1000$,並且傳送器數量最多可以到$2*10^6$,所以用把朋友們依照$e_i$做Bubble sort,並在每次交換時都標記成在兩者之間安裝一個傳送器就可以了。 或者你覺得$N$最多只有1000太少了,用手解也是可以(X)。 ```cpp #include <bits/stdc++.h> #define int long long using namespace std; const int N = 1010; vector<int> ans; int a[N], ord[N], n; void mswap (int j) { swap(a[j], a[j+1]); ans.push_back(j); } signed main() { string useless; int cid; cin >> useless >> cid >> n; for (int i=1; i<=n; i++) { int x; cin >> x; ord[x] = i; a[i] = i; } for (int i=1; i<=n; i++) for (int j=1; j<=n-1; j++) if (ord[a[j]] > ord[a[j+1]]) mswap(j); cout << "#Case " << cid << endl; cout << ans.size() << endl; for (auto y: ans) cout << y << ' '; cout << endl; } ``` {%hackmd Bk4uKyJJY %}