# 成大邀請賽 初賽 題解 ## A. 小業教授的研究計畫 ###### tags: constructive algorithm ###### author: ColtenOuO, 難度定位: Easy 結論:只有唯一的 $1$ 種填法 如果要你輸出那組解呢? ::: spoiler 參考程式碼 (ColtenOuO) ```cpp= #include <bits/stdc++.h> #define int long long using namespace std; signed main(void) { int a,b,c; cin >> a >> b >> c; cout << 1 << "\n"; return 0; } ``` ::: ## B. 分組任務 ###### tags: binary search, graphs, data structure ###### author: sam571128, 難度定位: Medium ### 子任務 1: $1 \le n \le 18$ 可以直接用遞迴或者位元枚舉的方式 將每張牌分配到第一組或第二組 在枚舉的過程中,更新最大答案即可 複雜度:$\mathcal{O}(n^22^n)$ ### 接下來的想法 觀察到其實我們可以將兩兩牌之間的關係畫成一張無向圖 而這個問題就轉化為「將點分成兩組,取兩組內最小的邊」 由於我們想要選的答案是最小的最大答案 可以很直覺的想到,其實我們可以「對答案二分搜」! 不過要怎麼檢查答案呢? 最容易想到的假解可能是:「檢查 $x$ 的時候,將 $\le x$ 的邊加入,如果連通塊數量是 $2$,就是合法的」 會發現到如果這樣做,有可能發現大的邊也會跟著被用到! 因此,其實正確的作法是 「把 $>x$ 的邊的兩點分別分到不同的組,如果可以達成這樣做沒有矛盾,$x$ 就是合法的」 ### 子任務 2: $1 \le n \le 1000$ 用上面的想法,暴力的將兩點分到不同組(作法可能滿多的) ### 滿分解 發現到其實在檢查的時候,如果我們把 $> x$ 的邊都加入 其實要檢查的是「這張圖是不是二分圖」! 所以可以用 DFS 或者 DSU 等方式做完 時間複雜度:$O((n+m) \log C)$, $O(m \alpha(n) \log C)$ ::: spoiler 參考程式碼 (sam571128) ```cpp= #include <bits/stdc++.h> #define fastio ios_base::sync_with_stdio(0); cin.tie(0); using namespace std; const int N = 1e6+5; vector<pair<int,int>> adj[N]; int dsu[N]; int find(int u){ return dsu[u] == u ? u : dsu[u] = find(dsu[u]); } void unite(int u, int v){ u = find(u), v = find(v); if(u == v) return; dsu[u] = v; } int n; bool check(int x){ iota(dsu,dsu+N,0); for(int i = 0; i < n; i++){ for(auto [j,w] : adj[i]){ if(w > x){ if(find(i) == find(j) || find(i+n) == find(j+n)) return false; unite(i,j+n); unite(i+n,j); } } } return true; } signed main(){ fastio int m; cin >> n >> m; for(int i = 0; i < m; i++){ int u,v,w; cin >> u >> v >> w; adj[u].push_back({v,w}); adj[v].push_back({u,w}); } int l = 0, r = 1e9+7; while(l < r){ int mid = l+r>>1; if(check(mid)) r = mid; else l = mid+1; } cout << r << "\n"; } ``` ::: ### 真的需要二分搜嗎? 其實會發現,二分搜在做的事情其實也就只是判斷 $> x$ 的邊的圖是不是二分圖 那我們把邊從大到小排序好,依序從大到小加入圖 那這題的答案其實就是「第一個使得圖變成不是二分圖的邊權」 當然,如果這張圖本身就是二分圖,答案就是 $0$ ::: spoiler 參考程式碼 (sam571128) ```cpp= #include <bits/stdc++.h> #define fastio ios_base::sync_with_stdio(0); cin.tie(0); using namespace std; const int N = 1e6+5; vector<pair<int,pair<int,int>>> edges; int dsu[N]; int find(int u){ return dsu[u] == u ? u : dsu[u] = find(dsu[u]); } void unite(int u, int v){ u = find(u), v = find(v); if(u == v) return; dsu[u] = v; } int n; signed main(){ fastio iota(dsu,dsu+N,0); int m; cin >> n >> m; for(int i = 0; i < m; i++){ int u,v,w; cin >> u >> v >> w; edges.push_back({w,{u,v}}); } sort(edges.begin(),edges.end(),greater<>()); for(auto [w, p] : edges){ auto [u,v] = p; if(find(u) == find(v)){ cout << w << "\n"; return 0; } unite(u,v+n); unite(v,u+n); } cout << 0 << "\n"; } ``` ::: ## C. 大富翁 ###### tags: greedy, dp, STL ###### author: sam571128, 難度定位: Medium 靈感來源:[Codeforces 1526C2 - Potions (Hard Version)](https://codeforces.com/problemset/problem/1526/C2) ### 子任務 2: $1 \le n \le 5, 1 \le a_i,b_i \le 10$ 基本上就是給人唬爛用的,可以用遞迴或者迴圈直接暴力跑過所有情形 最後再去檢查答案即可 複雜度:$\mathcal{O}(10^5)$ ### 子任務 3, 4: $1 \le n \le 100$, $1 \le n \le 10^4$ $dp$,會發現其實這個問題與背包很相似 有一個很直覺的 $dp$ 是這樣的 $$令 \ dp[i][j] \ 為前 \ i \ 輪遊戲中,最後的錢的數量為 \ j \ 所需花的最少輪$$ 不過由於 $a_i, b_i$ 的範圍很大,沒有辦法直接設這樣的狀態 有寫過 [這題](https://atcoder.jp/contests/dp/tasks/dp_e) 的人,可能知道處理方式 既然將總共的錢的數量存起來空間存不下,那我們將狀態設計交換! $$令 \ dp[i][j] \ 為前 \ i \ 輪遊戲中,在 \ j \ 輪花了錢,最少可以有的錢$$ 那轉移式其實很好想,就會是底下這樣 $$dp[i][j] = \max \begin{cases} \max(0,dp[i-1][j-1] + a[i] - b[i]) &, j \ne 0 \\ dp[i-1][j] + a[i] \end{cases}$$ 不過由於有 $k$ 輪必須要將錢花完,可以額外判斷如果 $i \in {t_1, t_2, \cdots, t_k}$,那當 $dp[i][j] > 0$ 的時候,就將答案設成 $\infty$ 等等 複雜度:$\mathcal{O}(n^2)$ ::: spoiler 參考程式碼 (sam57128) ```cpp= #include <bits/stdc++.h> #define fastio ios_base::sync_with_stdio(0); cin.tie(0); #define int long long using namespace std; const int N = 1e3+5; int dp[N][N]; //The minimum money one can have after spending j times int arr[N], cost[N], val[N]; signed main(){ fastio int n,k; cin >> n >> k; for(int i = 1; i <= n; i++){ cin >> arr[i]; val[i] = 1e18; } for(int i = 1; i <= n; i++){ cin >> cost[i]; } while(k--){ int t; cin >> t; val[t] = 0; } for(int i = 1; i <= n; i++){ for(int j = 1; j <= n; j++){ dp[i][j] = 1e18; } } for(int i = 1; i <= n; i++){ for(int j = 0; j <= i; j++){ dp[i][j] = dp[i-1][j] + arr[i]; if(j != 0){ dp[i][j] = min(dp[i][j], max(0LL, dp[i-1][j-1] + arr[i] - cost[i])); } if(dp[i][j] > val[i]){ dp[i][j] = 1e18; } } } for(int i = 0; i <= n; i++){ if(dp[n][i] <= 1e15){ cout << i << "\n"; return 0; } } cout << -1 << "\n"; } ``` ::: ### 滿分解 在這個情況下,單純的 $dp$ 已經沒辦法通過這題了 可以注意到,其實對於每一次要將錢歸零的 round,中間的情況都是獨立的 因此,其實對於題目的 $k$,可以想成是我們要解底下這個問題 「每一輪會得到 $a_i$ 元,而每輪最多可以花 $b_i$ 元,過程中,錢的數量必須 $\ge 0$,然後在結束時,錢必須是 $0$。最少需要在多少輪花費錢」 那對於這個問題,其實我們可以用貪心的方式去思考,對於每一天,能讓錢的數量越少,就花多少。當我們遇到錢要低於 $0$ 的時候,我們就將花最少錢的操作「反悔」掉!直到我們的錢回復到 $\ge 0$ 的情況。 因此,我們會需要一個可以儲存花過錢的情況的資料結構,並能夠得到最小值和刪除最小值。 而這個就是 Min Heap (`priority_queue`) 可以做到的事情,也可以使用 set, BIT, 線段樹等資結 時間複雜度:$\mathcal{O} (n \log n)$ ::: spoiler 參考程式碼 (sam571128) ```cpp= #include <bits/stdc++.h> #define int long long #define fastio ios_base::sync_with_stdio(0); cin.tie(0); using namespace std; const int N = 1e6+5; int arr[N], arr2[N], has[N]; signed main(){ fastio int n,k; cin >> n >> k; int sum = 0; priority_queue<int> pq; for(int i = 1; i <= n; i++){ cin >> arr[i]; } for(int i = 1; i <= n; i++){ cin >> arr2[i]; } for(int i = 1; i <= k; i++){ int x; cin >> x; has[x] = 1; } int ans = 0; for(int i = 1; i <= n; i++){ sum += arr[i]; if(arr2[i]!=0){ sum -= arr2[i]; pq.push(-arr2[i]); } while(sum < 0){ auto x = pq.top(); pq.pop(); if(sum + (-x) > 0){ pq.push(x-sum); sum = 0; break; }else{ sum += (-x); } } if(has[i]){ if(sum > 0){ cout << -1 << "\n"; return 0; } ans += pq.size(); while(!pq.empty()) pq.pop(); } } cout << ans << "\n"; } ``` ::: ## D. 身分調查 ###### tags: graphs, data structure, dp ###### author: sam571128, official solution: victor.gao, 難度定位: Hard 靈感來源: [Codeforces 1594D - The Number of Imposters](https://codeforces.com/problemset/problem/1594/D) ### 最重要的想法 首先,如果 $a$ 說 $b$ 是誠實的,那代表什麼? - 如果 $a$ 是誠實派的,那 $b$ 也是誠實派的 - 如果 $a$ 是說謊派的,那 $b$ 也是說謊派的 反之,如果 $a$ 說 $b$ 是說謊的,那代表什麼? - 如果 $a$ 是誠實派的,那 $b$ 是說謊派的 - 如果 $a$ 是說謊派的,那 $b$ 是誠實派的 也就是說,其實我們可以將敘述改寫成 - 如果 $a$ 說 $b$ 誠實,$a,b$ 相同陣營 - 如果 $a$ 說 $b$ 說謊,$a,b$ 不同陣營 ### 子任務 1: $K = 2$ 既然敘述只有兩個,就照上面的邏輯好好的判 case 即可 ### 子任務 2: $K \le 100$ 暴力做 可以去枚舉要刪掉的區間,將沒有刪掉的其他的邊加入圖,每次用 DFS 塗色,去判斷有沒有辦法走到 $x$。如果可以,就可以知道 $x$ 的陣營了。接著就將最小的區間更新即可 枚舉的時間會是 $\mathcal{O}(K^2)$,但每一次都要跑過整張圖(只要跑有被說過的點就好,不用跑過 $N$ 個),所以是 $\mathcal{O}(K)$ 複雜度: $O(K^3)$ ### 子任務 3: $K \le 5000$ 跟上一題一樣,只是現在範圍變大了,沒辦法再每次都跑過整張圖 因此,我們會需要動態加邊,動態判斷答案的資料結構 而這個資料結構就是「並查集」 如果熟悉用並查集判斷二分圖的話,應該可以知道作法 對於每個點 $u$,建兩個 $u_0, u_1$,分別表示 $u$ 是誠實派和 $u$ 是說謊派的點 而對於兩種不同的操作,分別建的邊會是 1. 建 $u_0 - v_0$, $u_1 - v_1$(相同陣營) 2. 建 $u_0 - v_1$, $u_1 - v_0$(相反陣營) 判斷的時候,判斷 $1$ 與 $x_0$ 還是 $x_1$ 在同一個連通塊即可 複雜度:$\mathcal{O}(K^2 \alpha(n))$ ### 子任務 4: 保證有一個敘述是 $(x,y)=(1,X)$ 給人唬爛用的 ### 滿分解(一)二分搜 + 動態圖連通 By sam571128 會發現我們想要找可以刪掉的最大區間,因此,考慮二分搜! 假設我們現在要搜尋的長度為 $l$,那其實現在問題變成「能不能刪掉連續的 $l$ 條邊,讓 $1$ 和 $x_0, x_1$ 是連通的」 這個動作其實和 sliding Window 在做的事情很像,因此其實我們可以考慮看看用 sliding window 來處理! 一開始先將 $1 \sim l$ 的邊加入,當我們要往右滑的時候,就將最左邊的邊加入,最右邊的邊刪除 因此,問題就轉變成了很經典的「[Dynamic Connectivity](https://codeforces.com/edu/course/2/lesson/7/3/practice/contest/289392/problem/C)」的問題 所以,這裡有兩種作法 1. 使用 Link-Cut Tree 或 Euler Tour Tree 等作法,每次滑窗需要花 $O(\log^2 n)$ 的時間 2. 這裡不用在線,所以其實可以考慮 時間線段樹 + rollback DSU 的作法! 如果是第一種做法,那其實就照做就好,所以這裡不特別提 這裡來講講第二種作法,仔細想一下 sliding window 在做的事情 會發現其實每條邊都會剛好存在於一個時間區間 因此可以用類似的方式在時間線段樹加邊 接著就可以維護好每一個 window 的答案了 接著就從左界最小的開始做即可! 複雜度:$\mathcal{O}(k \log n \log^2 k)$,常數太大可能會被卡 ::: spoiler 參考程式碼(時間線段樹 sam571128) ```cpp= #pragma GCC optimize("Ofast,unroll-loops,O3") #pragma GCC optimize("avx,avx2,sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,fma,tune=native") #include <bits/stdc++.h> #define pii pair<int,int> #define fastio ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0); using namespace std; const int N = 2e5+5; int dsu[N], sz[N], ans[N]; vector<pair<int,int>> st_dsu, st_sz; vector<pair<int,int>> tr[4*N]; inline void rollback(){ auto [a,b] = st_dsu.back(); st_dsu.pop_back(); auto [c,d] = st_sz.back(); st_sz.pop_back(); dsu[a] = b; sz[c] = d; } inline int find(int u){ return dsu[u]==u ? u : find(dsu[u]); } inline void unite(int u, int v){ u = find(u), v = find(v); if(u==v){ st_dsu.emplace_back(pii(0,dsu[u])); st_sz.emplace_back(pii(0,sz[v])); return; } if(sz[u] > sz[v]) swap(u,v); st_dsu.emplace_back(pii(u,dsu[u])); st_sz.emplace_back(pii(v,sz[v])); dsu[u] = v; sz[v] += sz[u]; } inline void add(int ml, int mr, pair<int,int> val, int idx, int l, int r){ if(ml <= l && r <= mr){ tr[idx].emplace_back(val); return; } int mid = l+r>>1; if(ml <= mid) add(ml,mr,val,idx<<1,l,mid); if(mr > mid) add(ml,mr,val,idx<<1|1,mid+1,r); } int n,m,x,flag=0; inline void dfs(int idx, int l, int r){ int cnt = 0; for(auto [u,v] : tr[idx]){ if(find(u)!=find(v)){ unite(u,v); cnt++; } } if(l == r){ if(find(1)==find(x)) ans[l] = 1; else if(find(1)==find(x+n)) ans[l] = 2; } else{ int mid = l+r>>1; dfs(idx<<1,l,mid); dfs(idx<<1|1,mid+1,r); } while(cnt--) rollback(); tr[idx].clear(); } pair<pair<int,int>,int> queries[N]; int ll, rr, tmp; inline bool check(int t){ if(t == -1){ return true; } if(x == 1){ return true; } if(t == m){ return false; } for(int i = 1; i <= m; i++){ auto [p,s] = queries[i-1]; auto [a,b] = p; if(i <= m-t){ add(i,2*(m-t)-i,{a,b},1,1,2*m-2*t); } if(i > t){ add(m-t+m-i+1,2*m-2*t,{a,b},1,1,2*m-2*t); } } for(int i = 0; i <= 2*m-2*t; i++){ ans[i] = 0; } dfs(1,1,2*m-2*t); int mn = 1e9; for(int i = 1; i <= m-t; i++){ if(ans[i] == 1){ mn = min(mn, i+1); }else if(ans[i] == 2){ mn = min(mn, i+1); } } for(int i = m; i >= t+1; i--){ if(ans[m-i+1+m-t] == 1){ mn = min(mn, i-t); }else if(ans[m-i+1+m-t] == 2){ mn = min(mn, i-t); } } if(mn != 1e9){ ll = mn, rr = mn+t-1; return true; } return false; } signed main(){ fastio iota(dsu,dsu+N,0); fill(sz,sz+N,1); cin >> n >> m >> x; for(int i = 0; i < m; i++){ int a,b,s; cin >> a >> b >> s; queries[i] = {{a,b},s}; if (s==1){ unite(a,b); unite(a+n,b+n); } else { unite(a+n,b); unite(a,b+n); } } if (find(1)==find(x)) tmp=0; else if (find(1)==find(x+n)) tmp=1; else { cout<<-1<<'\n'; return 0; } iota(dsu,dsu+N,0); fill(sz,sz+N,1); int l = 0, r = m; while(l < r){ int mid = l+(r-l+1)/2; if(check(mid)) l = mid; else r = mid - 1; } if(l == 0) cout << l << " " << 0 << " " << 0 << " " << tmp+1 << "\n"; else cout << l << " " << ll << " " << rr << " " << tmp+1 << " " << "\n"; } ``` ::: ### 滿分解(二)分治優化 By victor.gao 題目要求刪除最大一段中間區間,這問題等價把再陣列複製一倍,留下一段最小的區間 所以問題就變成在這兩倍長的陣列中選取一段最小區間,使得能確定 $x$ 的身分 把題目問題再簡化一點,假設現在有 $a$ 已經確認身分並指認 $b$,那顯然能確定 $b$ 的身分,反過來如果 $a$ 指認 $b$ 且 $b$ 已經確定身分,那也能確定 $a$ 的身分。那可以把 $a$ 指認 $b$ 視為一條無向邊,那整個問題就變成**用一段最小的區間的邊使 $1$ 與 $x$ 連通**。 那觀察一下 假如現在右界 $r_1$ 能成功的最大左界在 $l_1$,那現在有 $r_2 > r_1$,在這情況下加入的邊變多了,那一定可以刪除一些(可能沒有)左界不必要的邊來達成條件,所以在這時候 $l_2 \geq l_1$。 所以我們發現在 $r$ 遞增時,對應到的 $l$ 也會非嚴格遞增,這時候我們把每個 $r$ 對到的左界當做一個 dp 值和轉移來源,可以經過上面敘述的發現他有轉移點單調遞增的情況,而判斷連通性可以用 dsu。 我們考慮分治優化 這時候會遇到一個問題,在分治優化中能使複雜度保持在 $\mathcal{O}(n\ log\ n)$ 是因為每次對詢問的點 $mid$ 都暴力只跑一段他目前可能的轉移點,這樣最多遞迴 $log\ n$ 層,每層暴力跑總和都是 $O(n)$,但現在是要確認連通性,假設現在可轉移來源是 $ql \sim qr$ 現在在查詢的區間是 $l \sim r$ 且查詢的點是 $mid$,不可能每次都把 $qr \sim mid$ 的邊都放入 dsu 再去查詢 $ql \sim qr$,這樣複雜度會不再是 $\mathcal{O}(n\ log\ n)$,那假如我在往左遞迴下去前先把 $dp[mid]+1 \sim min(l-1,qr)$ 先放入 dsu ,往右遞迴前先把 $max(qr+1,l) \sim mid$ 放入 dsu,因為可以知道在後面的遞迴中這段都不會被動到過,離開的時候再 undo,這樣做在查詢一樣能保持分治優化的 $\mathcal{O}(n\ log\ n)$ ,而我們在遞迴前多加入的邊可以發現每層的次數都不超過 $(qr-dp[mid])+(mid-l+1)$ 而這複雜度與分治優化一樣也是 $\mathcal{O}(n\ log\ n)$,總複雜度就會是分治優化加可回溯 dsu 的複雜度 $\mathcal{O}(2k \log (2k) \log n)$ 複雜度:$\mathcal{O}(k \log k \log n)$ :::spoiler 題外話 這東西超快好像才跑 300~400 ms,但我們認為初賽不必要把解法卡那麼緊, 所以也讓三個 log 的解過了,不過時間卡的比較緊一點官解跑 2.3 s 然後這想法出現在某次 JOISC 裡 (這樣應該不算暴雷),然後是電方塊想出來並教我的 \\LCBorz/ ::: ::: spoiler 參考程式碼 (分治優化 victor.gao) ```cpp= //#pragma GCC optimize("Ofast,unroll-loops,O3") //#pragma GCC optimize("avx,avx2,sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,fma,tune=native") #include<bits/stdc++.h> //#include<bits/extc++.h> //#pragma pack(1) #define fast ios::sync_with_stdio(0); cin.tie(0); #define pii pair<int,int> #define x first #define y second #define N 100015 using namespace std; //using namespace __gnu_pbds; mt19937 rng(chrono::steady_clock::now().time_since_epoch().count()); //typedef tree<int, null_type,less_equal<int>, rb_tree_tag,tree_order_statistics_node_update> order_multiset; //typedef tree<int, null_type,less<int>, rb_tree_tag,tree_order_statistics_node_update> order_set; struct dsu_data{ int a,sz1,b,sz2; dsu_data(int _a,int _sz1,int _b,int _sz2):a(_a),sz1(_sz1),b(_b),sz2(_sz2){} }; struct dsu{ int boss[2*N],sz[2*N]; stack<dsu_data>st; void init(int n){ for (int i=0;i<=n+2;i++){ boss[i]=i; sz[i]=1; } while (!st.empty()) st.pop(); } int find(int x){ if (boss[x]==x) return x; return find(boss[x]); } void Merge(int a,int b){ int u=find(a),v=find(b); st.push(dsu_data(u,sz[u],v,sz[v])); if (u==v) return; if (sz[u]<sz[v]) swap(u,v); boss[v]=u; sz[u]+=sz[v]; } void rollback(){ if (st.empty()) return; dsu_data np=st.top(); st.pop(); if (np.a==np.b) return; boss[np.a]=np.a; sz[np.a]=np.sz1; boss[np.b]=np.b; sz[np.b]=np.sz2; } void undo(int t){ while (st.size()>t) rollback(); } }dsu; int n,m,x,is[2*N],dp[2*N],ans=-1,pos; pii edge[2*N]; // qr+1 ~ l-1 is inside inline bool check(){ return dsu.find(x)==dsu.find(1); } void Merge(int l,int r,int ql,int qr){ if (l>r||ql>qr) return; int mid=(l+r)>>1,init_sz=dsu.st.size(); for (int i=mid;i>=l&&i>qr;i--){ dsu.Merge(edge[i].x,edge[i].y); } for (int i=min(qr,mid);i>=ql;i--){ dsu.Merge(edge[i].x,edge[i].y); if (check()){ dp[mid]=i; break; } } dsu.undo(init_sz); for (int i=min(l-1,qr);i>dp[mid];i--) dsu.Merge(edge[i].x,edge[i].y); Merge(l,mid-1,ql,dp[mid]); dsu.undo(init_sz); for (int i=mid;i>=max(l,qr+1);i--) dsu.Merge(edge[i].x,edge[i].y); Merge(mid+1,r,max(dp[mid],ql),qr); dsu.undo(init_sz); } void change_ans(int l,int r){ if (l>r||(l>m&&r>m)||!l) return; if (r>m){ swap(l,r); l-=m; l++; r--; } else { if (l-1>=m-r) r=l-1,l=1; else l=r+1,r=m; } if (ans<r-l+1){ ans=r-l+1; pos=l; } else if (ans==r-l+1) pos=min(pos,l); } int main(){ fast cin>>n>>m>>x; for (int i=1;i<=m;i++){ cin>>edge[i].x>>edge[i].y>>is[i]; edge[i+m]=edge[i]; is[i+m]=is[i]; } if (x==1){ cout<<m<<' '<<1<<' '<<m<<" "<<1<<'\n'; return 0; } dsu.init(2*n); int col=-1; for (int i=1;i<=m;i++){ if (is[i]==1){ dsu.Merge(edge[i].x,edge[i].y); dsu.Merge(edge[i].x+n,edge[i].y+n); } else { dsu.Merge(edge[i].x,edge[i].y+n); dsu.Merge(edge[i].y,edge[i].x+n); } } if (dsu.find(1)==dsu.find(x)) col=1; else if (dsu.find(1)==dsu.find(x+n)) col=2; if (col==-1) cout<<-1<<'\n'; else { dsu.init(n); Merge(1,2*m,1,2*m); for (int i=1;i<=2*m;i++){ change_ans(dp[i],i); } if (ans==0) cout<<0<<" "<<0<<" "<<0<<" "<<col<<'\n'; else cout<<ans<<' '<<pos<<" "<<pos+ans-1<<" "<<col<<'\n'; } return 0; } ``` ::: ### 滿分解(三)Link Cut Tree by victor.gao ::: info 其實我根本不會 Link Cut Tree 我是抄中國人寫好的東西 如果有問題請不要怪我 ::: 藉由上面的想法,我們需要找一段最小區間使得 $1$ ,$x$ 連通 假設現在為每個邊設邊權為他的 id,每次加 $i$ 第條邊後 這時刻的答案就會是 $i -$ $path(1,x)$ 上的邊權最小值 那現在加一條邊 $u,v$ - $u,v$ 不連通,就直接連起來 - $u,v$ 已經連通,把 $path(u,v)$ 上邊權最小的邊拔掉,然後加入 $u,v$ 大致作法是把每條邊都變成一個點去做 Link Cut Tree 同時維護好最小值的邊 [詳細作法可自己看 oi-wiki 因為我根本不懂 Link Cut Tree 的運作](https://oi-wiki.org/ds/lct/#%E7%BB%B4%E6%8A%A4%E8%BE%B9%E6%9D%83) 複雜度應該是總共會有 $2k+n$ 個點加邊 $2k$ 次 $\mathcal{O}(2k \log (2k+n))$ 總複雜度 $\mathcal{O}(k \log (k+n))$ 實際跑起來分治優化 + dsu 兩個 log 更快 在洛谷那題我兩個 log 排全部 rk4 ![](https://i.imgur.com/HJBfAAW.png) :::spoiler 參考程式碼 (Link Cut Tree 是抄洛谷上某個寫題解的人的) ```cpp= #include<bits/stdc++.h> using namespace std; #define ls(x) Tree[x].son[0] #define rs(x) Tree[x].son[1] #define fa(x) Tree[x].fa typedef long long LL; const int maxn = 600010; struct node{ int son[2], Min, id, fa, lazy; }Tree[maxn]; int n, m, w[maxn], num, Min; bool vis[maxn]; struct Node{ int u, v, w; }a[maxn]; inline bool IsRoot(int x) { return (ls(fa(x)) == x || rs(fa(x)) == x) ? false : true; } inline void PushUp(int x){ Tree[x].Min = w[x]; Tree[x].id = x; if ( ls(x) && Tree[ls(x)].Min < Tree[x].Min ) { Tree[x].Min = Tree[ls(x)].Min; Tree[x].id = Tree[ls(x)].id; } if ( rs(x) && Tree[rs(x)].Min < Tree[x].Min ) { Tree[x].Min = Tree[rs(x)].Min; Tree[x].id = Tree[rs(x)].id; } } inline void Update(int x) { Tree[x].lazy ^= 1; swap(ls(x), rs(x)); } inline void PushDown(int x){ if ( !Tree[x].lazy ) return ; if ( ls(x) ) Update(ls(x)); if ( rs(x) ) Update(rs(x)); Tree[x].lazy = 0; } inline void Rotate(int x){ int y = fa(x), z = fa(y), k = rs(y) == x, w = Tree[x].son[!k]; if ( !IsRoot(y) ) Tree[z].son[rs(z) == y] = x; fa(x) = z; fa(y) = x; if ( w ) fa(w) = y; Tree[x].son[!k] = y; Tree[y].son[k] = w; PushUp(y); } inline void Splay(int x){ stack<int> Stack; int y = x, z; Stack.push(y); while ( !IsRoot(y) ) Stack.push(y = fa(y)); while ( !Stack.empty() ) { PushDown(Stack.top()); Stack.pop(); } while ( !IsRoot(x) ) { y = fa(x); z = fa(y); if ( !IsRoot(y) ) Rotate((ls(y) == x) ^ (ls(z) == y) ? x : y); Rotate(x); } PushUp(x); } inline void Access(int root) { for ( int x = 0; root; x = root, root = fa(root) ) { Splay(root); rs(root) = x; PushUp(root); } } inline void MakeRoot(int x) { Access(x); Splay(x); Update(x); } inline int FindRoot(int x) { Access(x); Splay(x); while ( ls(x) ) x = ls(x); Splay(x); return x; } inline void Link(int u, int v) { MakeRoot(u); if ( FindRoot(v) != u ) fa(u) = v; } inline void Cut(int u, int v) { MakeRoot(u); if ( FindRoot(v) != u || fa(v) != u || ls(v) ) return ; fa(v) = rs(u) = 0; } inline void Split(int u, int v) { MakeRoot(u); Access(v); Splay(v); } inline bool Check(int u, int v) { MakeRoot(u); return FindRoot(v) == u; } struct DSU{ int f[maxn]; void init(int n){for(int i=1;i<=n;i++)f[i]=i;} int find(int x){return f[x]==x?x:f[x]=find(f[x]);} void merge(int x,int y){x=find(x),y=find(y);f[x]=y;} }dsu; int start=1e9,ans=-1,pos,tmp=1; void change_ans(int l,int r){ if (l>r||(l>m&&r>m)||!l) return; if (r>m){ swap(l,r); l-=m; l++; r--; } else { if (l-1>=m-r) r=l-1,l=1; else l=r+1,r=m; } if (ans<r-l+1){ ans=r-l+1; start=l; } else if (ans==r-l+1) start=min(start,l); } int main(){ cin>>n>>m>>pos; dsu.init(2*n); for (int i=1;i<=m;i++){ cin>>a[i].u>>a[i].v>>a[i].w; if (a[i].w==1){ dsu.merge(a[i].u,a[i].v); dsu.merge(a[i].u+n,a[i].v+n); } else { dsu.merge(a[i].u,a[i].v+n); dsu.merge(a[i].u+n,a[i].v); } a[i].w=i; a[i+m]=a[i]; a[i+m].w=i+m; } if (dsu.find(1)==dsu.find(pos)) tmp=1; else tmp=2; for (int i=1;i<=n;i++) w[i] = 0x3f3f3f3f; for (int i=n+1;i<=n+2*m;i++) w[i] = a[i - n].w; if (pos==1){ cout<<m<<" "<<1<<" "<<m<<" "<<1<<'\n'; return 0; } for (int i=1;i<=2*m;i++){ if (a[i].u == a[i].v) continue; if (!Check(a[i].u,a[i].v)) { Link(a[i].u,i+n); Link(i+n,a[i].v); ++ num; vis[i] = true; } else { Split(a[i].u,a[i].v); int x=Tree[a[i].v].id; Splay(x); fa(ls(x))=fa(rs(x))=0; vis[x-n]=false; Link(a[i].u,i+n); Link(i+n,a[i].v); vis[i]=true; } if (Check(1,pos)){ Split(1,pos); int val=Tree[pos].Min; change_ans(val,i); } } if (ans==-1) cout<<ans<<'\n'; else if (ans==0) cout<<ans<<' '<<0<<' '<<0<<" "<<tmp<<'\n'; else cout<<ans<<" "<<start<<" "<<start+ans-1<<" "<<tmp<<'\n'; return 0; } ``` ::: ## E. 罰款金額 ###### tags: math, data structure, offline algorithm ###### author: ColtenOuO, official solution: sam571128, 難度定位: Hard ### 子任務 2: $Q \le 10$ 對於每個詢問,直接暴力跑過整個區間 $[l,r]$ 即可 複雜度: $\mathcal{O}(nQ)$ ### 子任務 3: $k = 1$ 由於 $a_i \ge 1$,因此,其實所有人只有兩種情況 1. $a_i = 1 = k$,沒有裝強也沒有裝弱 2. $a_i > 1 = k$,裝強 那我們要怎麼知道所有裝強的人的答案總和呢? 很簡單,既然 $a_i > k$,那麼 $|a_i - k| = a_i - k$! 因此,只要找到這些 $a_i$ 的總和減去數量乘以 $k$,就是答案了 可以簡單的使用兩個前綴和預處理完 複雜度:$O(n + Q)$ ### 子任務 4: $n \le 1000, k \le 10$ 我們可以預處理好 $k=1 \sim 10$ 的時候,每個區間 $[l,r]$ 的答案 接下來每一次詢問的時候,直接去查表即可! 複雜度:$\mathcal{O}(n^2k + Q)$ ### 滿分解(一)離線 By ColtenOuO 注意到這題其實可以離線做 我們考慮將陣列中的每個數字和詢問由小到大去處理 每次遇到一個詢問,必須要去找一個區間內 $< k$ 和 $> k$ 的數字總和與數量 因此,要做到這一點,我們需要一個可以處理「單點修改」、「區間詢問」的資料結構 而這件事情,可以使用 BIT 或者線段樹來處理 細節:$a_i = k$ 的沒有裝弱也沒有裝強,必須要好好處理這部份 可能有點幫助的小觀察:對於兩個數字 $x,y$,$|x-y| + |x+y| = 2\max(x,y)$ 複雜度:$\mathcal{O}(Q\log n)$ ::: spoiler 參考程式碼 (sam571128) ```cpp= #include <bits/stdc++.h> #define fastio ios_base::sync_with_stdio(0); cin.tie(0); #define int long long using namespace std; const int N = 2e5+5; vector<pair<pair<int,int>,int>> ask[N]; vector<int> pos[N]; pair<int,int> bit[N]; int arr[N], cnt[N], ans[N]; pair<int,int> operator + (pair<int,int> a, pair<int,int> b){ return {a.first + b.first, a.second + b.second}; } void operator += (pair<int,int> &a, pair<int,int> b){ a = a + b; } void update(int pos, pair<int,int> val){ while(pos < N){ bit[pos] += val; pos += pos&-pos; } } pair<int,int> query(int pos){ pair<int,int> res = {0,0}; while(pos){ res += bit[pos]; pos -= pos&-pos; } return res; } signed main(){ fastio int n,q; cin >> n >> q; for(int i = 1; i <= n; i++){ cin >> arr[i]; cnt[arr[i]]++; pos[arr[i]].push_back(i); update(i,{arr[i],1}); } for(int i = 1; i <= q; i++){ int l,r,k; cin >> l >> r >> k; ask[k].push_back({{l,r},i}); } for(int i = 1; i < N; i++){ for(auto [p,id] : ask[i]){ auto [l,r] = p; auto [a,b] = query(r); auto [c,d] = query(l-1); int num = (r-l+1) - (b-d) - (upper_bound(pos[i].begin(),pos[i].end(),r) - lower_bound(pos[i].begin(),pos[i].end(),l)); int val = a-c; ans[id] = val*2 + (num)*i*2; } for(auto x : pos[i]){ update(x,{-i,-1}); } } for(int i = 1; i <= q; i++){ cout << ans[i] << "\n"; } } ``` ::: ### 滿分解(二)莫隊+值域分塊 By Penguin07 對於所有的詢問,我們用莫隊的方式將他們排好 接著,我們將數字對值域進行分塊,每一塊儲存的是那塊的總和和數字數量 我們依序去處理每一個區間的答案 當我們要跑到 $k$ 的時候,暴力處理 $k$ 所在的塊,然後剩下的用塊的答案去處理 複雜度:$\mathcal{O}(Q\sqrt{n})$ ::: spoiler 參考程式碼 (Penguin07) ```cpp= #include <bits/stdc++.h> using namespace std; const int N = (int) 2e5 + 5; const int B = 550; struct Block { int L, R; long long all = 0; int all_cnt = 0; long long sum[B] = {}; int cnt[B] = {}; void add(int x) { all += x; all_cnt += 1; sum[x - L] += x; cnt[x - L] += 1; } void remove(int x) { all -= x; all_cnt -= 1; sum[x - L] -= x; cnt[x - L] -= 1; } pair<long long, int> get(int ql, int qr) { ql = max(ql, L); qr = min(qr, R); if(ql == L && qr == R) { return {all, all_cnt}; } long long ans = 0; int ans_cnt = 0; for(int i = ql; i < qr; i++) { ans += sum[i - L]; ans_cnt += cnt[i - L]; } return {ans, ans_cnt}; } }; int main() { ios::sync_with_stdio(false); cin.tie(0); int n, q; cin >> n >> q; vector<long long> a(n); for(int i = 0; i < n; i++) { cin >> a[i]; } vector<long long> pref(n + 1); for(int i = 0; i < n; i++) { pref[i + 1] = pref[i] + a[i]; } vector<array<int, 4>> queries(q); for(int i = 0; i < q; i++) { int l, r, k; cin >> l >> r >> k; --l; queries[i] = {l, r, k, i}; } sort(queries.begin(), queries.end(), [](const auto& a, const auto& b) { if(a[0] / B == b[0] / B) { return a[0] / B & 1 ? a[1] > b[1] : a[1] < b[1]; } return a[0] < b[0]; }); const int BLOCK_CNT = (N + B - 1) / B; vector<Block> block(BLOCK_CNT); for(int i = 0; i < BLOCK_CNT; i++) { block[i].L = i * B; block[i].R = (i + 1) * B; } auto Add = [&](int x) { block[x / B].add(x); }; auto Remove = [&](int x) { block[x / B].remove(x); }; auto Get = [&](int l, int r) { long long sum = 0, cnt = 0; for(int i = 0; i < BLOCK_CNT; i++) { auto [x, y] = block[i].get(l, r + 1); sum += x; cnt += y; } return pair<long long, long long>{sum, cnt}; }; vector<long long> ans(q); int L = 0, R = 0; for(auto e : queries) { int ql = e[0], qr = e[1], k = e[2], id = e[3]; while(L > ql) { L -= 1; Add(a[L]); } while(R < qr) { Add(a[R]); R += 1; } while(L < ql) { Remove(a[L]); L += 1; } while(R > qr) { R -= 1; Remove(a[R]); } auto lhs = Get(0, k - 1); auto rhs = Get(k + 1, N - 1); ans[id] = pref[R] - pref[L] - (R - L - lhs.second - rhs.second) * k + (lhs.second + rhs.second) * k; ans[id] += abs(lhs.first - lhs.second * k) + abs(rhs.first - rhs.second * k); } for(int i = 0; i < q; i++) { cout << ans[i] << "\n"; } return 0; } ``` ::: ## F. 動物園 ###### tags: math, number theory ###### author: sam571128, 難度定位: Easy ### 子任務 2: $x \le 10$ 暴力 or 手算即可 ### 子任務 3: $x \le 10^3$ 從子任務 2 可以猜測其實答案並不會比 $x$ 大多少 因此,其實可以暴力跑過前 $2000$ 個數字等等的 不過,其實會發現當數字到了一定大小之後,數字的範圍會超出 long long! 在這個情況下,有兩種解決的方法 1. 使用可以做大數運算的語言,如 python 或 Java 的 BigInteger 2. 要知道一點點數論的概念 $$ \begin{align*} (a + b) \bmod n &= ((a \bmod n) + (b \bmod n)) \bmod n \\ (a - b) \bmod n &= ((a \bmod n) - (b \bmod n)) \bmod n\\ (a \cdot b ) \bmod n &= (a \bmod n)(b \bmod n) \bmod n \end{align*} $$ 在 $\mathcal{O}(x^2)$ 的時間建出這些數字的答案 接著直接輸出即可 ### 主要想法 觀察到這題其實數字雖然是有規律的,但其實好像根本一點關係也沒有欸 或者沒想法,應該會發現其實 $NO$ 根本不可能發生! 一定都有一組解 其實如果有仔細想一下,會想到其實 $$「兩個數字相減被 \ x \ 整除」\iff 「兩個數字除以 \ x \ 的餘數相同」$$ 然後,根據鴿籠原理,其實如果我們跑過 $x+1$ 個數字,由於除以 $x$ 的餘數只有 $x$ 種,因此一定會有其中一個出現 $2$ 次以上! 其實題目給的 $1,12,123$ 根本不重要,就算換成一些奇怪的數字都一樣找的到答案! ### 滿分解 對於每一次輸入的 $x$,暴力的跑過前 $x+1$ 個數字 開一個陣列或 map 存上一個出現過餘數是 $i$ 的數字 每一次要找下一個數字的時候,假設現在是 $k$,下一個數字是 $n$,那串接起來的數字會是 $$10^{\log_{10}n} k + n$$ 由於這個數字到後來可能會很大,要記得每一次都要 $\text{mod } x$ 複雜度:$\mathcal{O}(\sum x)$ ::: spoiler 參考程式碼 (sam571128) ```cpp= #include <bits/stdc++.h> #define fastio ios_base::sync_with_stdio(0); cin.tie(0); #define int long long using namespace std; int getLog(int x){ int cnt = 0; while(x){ cnt++; x/=10; } return cnt; } string getNum(int n){ string s; for(int i = 1; i <= n; i++){ s += to_string(i); } return s; } void solve(){ int x; cin >> x; int has[x] = {}; int now = 0; for(int i = 1; i <= x+1; i++){ now = ((int)round(pow(10,getLog(i)))*now+i)%x; if(has[now]){ cout << "YES\n"; cout << getNum(has[now]) << " " << getNum(i) << "\n"; break; } has[now] = i; } } signed main(){ fastio int t; cin >> t; while(t--) solve(); } ``` ::: ### 賽中看到的另解 由於我們沒有說輸出的兩隻動物編號必須是相異的 輸出兩個相同的編號也會過 QQ 這不是預期的就是了 ## G. 鬼鍵 ###### tags: implementation ###### author: visitorCKW, 難度定位: Easy 模擬題,照著題目敘述模擬即可 ::: spoiler 參考程式碼 (sam571128) ```cpp= #include <bits/stdc++.h> #define fastio ios_base::sync_with_stdio(0); cin.tie(0); #define int long long using namespace std; signed main(){ fastio int n,m,k,q; cin >> n >> m >> k >> q; map<int, pair<int,int>> mp; map<pair<int,int>,int> has; for(int i = 1; i <= k; i++){ int a,x,y; cin >> a >> x >> y; mp[a] = {x,y}; has[{x,y}] = a; } while(q--){ int x,y,z; cin >> x >> y >> z; set<int> st,st2; st.insert(mp[x].first); st.insert(mp[y].first); st.insert(mp[z].first); st2.insert(mp[x].second); st2.insert(mp[y].second); st2.insert(mp[z].second);; if(st.size() != 2 || st2.size() != 2){ cout << "Not ghost key condition!\n"; continue; } int r1 = *st.begin(), r2 = *st.rbegin(); int c1 = *st2.begin(), c2 = *st2.rbegin(); int cnt = 0; for(auto r : {r1,r2}){ for(auto c : {c1,c2}){ cnt += (has[{r,c}]==x || has[{r,c}] == y || has[{r,c}] == z); } } if(cnt==3){; for(auto r : {r1,r2}){ for(auto c : {c1,c2}){ if(has[{r,c}] && has[{r,c}]!=x && has[{r,c}] != y && has[{r,c}] != z){ cout << "Find ghosy key: " << " " << has[{r,c}] << "\n"; goto nxt; } } } } cout << "Not ghost key condition!\n"; nxt:; } } ``` ::: ## H. 小業教授的冷泡茶 ###### tags: dp, two pointers ###### author: ColtenOuO, 難度定位: Medium ### 子任務 2: 所有區塊的茶葉重量一樣 可以計算出一個機器人最多可以採收到多少區塊 假設這個數字是 $x$,那麼答案就會是 $\min(na_i, x \cdot m \cdot a_i)$ 複雜度:$O(n)$ ### 子任務 3: $m = 1$ 只有一個機器人的話,那我們可以用「雙指針」來計算出最大的解! 固定 $l$,我們可以知道從他開始,往右最遠可以延伸到哪裡 接著就取總和最大的區間即可 複雜度:$O(n)$ ### 子任務 4: $n = 1000$ 考慮 $dp$,我們可以列出以下的狀態 $$令 \ dp[i][j] \ 為前 \ i \ 個區塊,總共有\ j \ 個機器人所能採收到的最大總和是多少$$ 那麼,我們可以很輕易的列出這樣的轉移式 $$dp[i][j] = \max\begin{cases} dp[i-1][j] \\ dp[l][j-1] + \text{sum}(l+1,i) &, \text{if }\text{sum}(l+1,i) \le k \end{cases}$$ 可以在 $O(n^2m)$ 的時間做完(時間卡的有點緊) 如果沒滾動的話,滿分解也只會拿到這筆 ### 滿分解 將子任務 3 和 4 的作法結合! 對於每一個位置 $i$,我們往前維護他最前面從哪裡開始,機器人都可以拿完區間 $[l,i]$ 的茶葉,這個可以在 $O(n)$ 的時間做完,設這個值為 $left[i]$ 接著,我們一樣考慮使用 $dp$,想法與子任務 4 差不多,不過轉移必須要變快! 發現到上一題的轉移式,第二個條件其實在我們知道最左邊可以延伸到哪之後,其實可以改成 $$dp[i][j] = \max\begin{cases} dp[i-1][j] \\ dp[x][j-1] + \text{sum}(left[i],i) &, \text{if }\text{sum}(l+1,i) \le k, \ x \le left[i]-1 \end{cases}$$ 因此,只要對 $dp$ 維護好前綴 $\max$,轉移就可以在 $\mathcal{O}(1)$ 的時間完成!(由於 $nm$ 很大,要滾動) 複雜度: $\mathcal{O}(nm)$ ::: spoiler 參考程式碼 (ColtenOuO) ```cpp= #include <bits/stdc++.h> #define int long long using namespace std; int dp[2][200005],a[200005],tmp[200005],c[200005]; signed main(void) { int n,m,k2; cin >> n >> m >> k2; for(int i=1;i<=n;i++) cin >> a[i] , c[i] = c[i-1] + a[i]; int l = 1 , r = 1 , total = 0; while( r <= n ) { total += a[r]; while( total > k2 ) total -= a[l] , l++; if( total == 0 ) tmp[r] = -1; else tmp[r] = l; r++; } for(int i=1;i<=m;i++) { for(int k=1;k<=n;k++) { if( tmp[k] != -1 ) dp[i%2][k] = max({dp[(i+1)%2][k],dp[i%2][k-1],dp[(i+1)%2][tmp[k]-1]+c[k]-c[tmp[k]-1]}); else dp[i%2][k] = max(dp[i%2][k-1],dp[(i+1)%2][k]); } for(int k=1;k<=n;k++) dp[(i+1)%2][k] = 0; } cout << dp[m%2][n] << "\n"; } ``` ::: ### Bonus 本題有使用 Aliens Trick 的 $\mathcal{O}(n \log C)$ 的作法 詳細作法可以去參考 [APCS 2021/09 D.美食博覽會](https://zerojudge.tw/ShowProblem?problemid=g278) 的相關題解