# APCS 2021.11.07 ###### tags: `APCS` > [name=peienwu] > [name=thanksone] > [time=Sun, Nov 7, 2021 8:06 PM] --- ## 原文網址:https://peienwu.com/apcs2111/ --- ## P1 修補圍籬 [題目連結](https://zerojudge.tw/ShowProblem?problemid=g595) ### 題解 如果在兩端就直接取旁邊的高度,否則取跟左右邊高度的最小值。 ### 時間複雜度 $O(n)$ ### AC程式碼 ```cpp= #include <bits/stdc++.h> #define int long long #define N 105 #define IOS ios::sync_with_stdio(0),cin.tie(0) using namespace std; int n,A[N]; signed main(){ IOS; cin>>n; for(int i=0;i<n;i++)cin>>A[i]; int ans = 0; if(A[0] == 0)ans += A[1]; if(A[n-1] == 0)ans += A[n-2]; for(int i=1;i<n-1;i++){ if(A[i] == 0)ans += min(A[i-1],A[i+1]); } cout<<ans<<endl; } ``` >[name=peienwu] ## P2 動線安排(魔王題) [題目連結](https://zerojudge.tw/ShowProblem?problemid=g596) ### 題解 把線分成橫的跟直的就可以好好處理交叉了! ### 時間複雜度 $O(h(n + m))$ ### AC程式碼 ```cpp= #include <bits/stdc++.h> using namespace std; int n, m, cnt = 0, ans = 0; array<array<int, 104>, 104> R, C, I; void add(int r, int c){ bool ok; I[r][c] = 1; cnt++; if(C[r][c] || R[r][c]) cnt--; C[r][c] = R[r][c] = 0; ok = 0; //直下情況 for(int i = r + 1; i < n; i++){ if(I[i][c]) ok = 1; } if(ok){ for(int i = r + 1; i < n; i++){ if(I[i][c] || R[i][c]) break; R[i][c]++; cnt += C[i][c] == 0; } } ok = 0; //直上情況 for(int i = r - 1; i >= 0; i--){ if(I[i][c]) ok = 1; } if(ok){ for(int i = r - 1; i >= 0; i--){ if(I[i][c] || R[i][c]) break; R[i][c]++; cnt += C[i][c] == 0; } } ok = 0; //橫右情況 for(int i = c + 1; i < m; i++){ if(I[r][i]) ok = 1; } if(ok){ for(int i = c + 1; i < m; i++){ if(I[r][i] || C[r][i]) break; C[r][i]++; cnt += R[r][i] == 0; } } ok = 0; //橫左情況 for(int i = c - 1; i >= 0; i--){ if(I[r][i]) ok = 1; } if(ok){ for(int i = c - 1; i >= 0; i--){ if(I[r][i] || C[r][i]) break; C[r][i]++; cnt += R[r][i] == 0; } } } void pull(int r, int c){ I[r][c] = 0; cnt--; for(int i = r + 1; i < n; i++){ //直下情況 if(!R[i][c]) break; R[i][c]--; cnt -= C[i][c] == 0; } for(int i = r - 1; i >= 0; i--){ //直上情況 if(!R[i][c]) break; R[i][c]--; cnt -= C[i][c] == 0; } for(int i = c + 1; i < m; i++){ //橫右情況 if(!C[r][i]) break; C[r][i]--; cnt -= R[r][i] == 0; } for(int i = c - 1; i >= 0; i--){ //橫左情況 if(!C[r][i]) break; C[r][i]--; cnt -= R[r][i] == 0; } } signed main(){ int h, r, c, t; cin >> n >> m >> h; while(h--){ cin >> r >> c >> t; if(t){ pull(r, c); }else{ add(r, c); } ans = max(ans, cnt); } cout << ans << "\n" << cnt; return 0; } ``` > [name=thanksone] ## P3 生產線 [題目連結](https://zerojudge.tw/ShowProblem?problemid=g597) ### 差分作法 #### 題解 用差分的想法加值,再用前綴還原,最後再排序。最後利用Greedy的想法,將每一項最小的工作量乘上最大的時間,總和即為答案。 :::info **差分** 差分是前綴和的逆運算,也就是說,把兩項的差算出來就是差分。定義如下: $$b_i = \begin{cases}a_i-a_{i-1}, &\text{if }i\gt 1 \\a_1, & \text{if } i = 1\end{cases}$$ 差分的使用時機是區間加值,一個區間內的數字都加上一個定值,這時候就可以使用到差分的技巧。使用方式如下,當我要在區間 $[l,r]$ 的每一個數字都加上一個值$v$,以下步驟: 1. 定義一個新的陣列 $b_i$ 表示每一項差分 2. 設 $b_l = b_l+v$,$b_{r+1} = b_{r+1}-v$ 3. 將差分的每一項加上前一項(做前綴和 $b_i = b_i+b_{i-1}$),即為原數列 第二步驟可以重複好幾次做,這樣複雜度從原本的$O(n)$就變成了$O(1)$了! ::: #### 時間複雜度 差分:$O(m)$ 、排序 $O(n\log n)$ 總時間複雜度:$O(n\log n + m)$ #### AC程式碼 ```cpp= #include <bits/stdc++.h> #define int long long #define N 200005 #define IOS ios::sync_with_stdio(0),cin.tie(0) using namespace std; int n,m,A[N],B[N]; signed main(){ IOS; memset(A,0,sizeof(A)); cin>>n>>m; while(m--){ int x,y,w;cin>>x>>y>>w; A[x] += w; A[y+1] -= w; } for(int i=1;i<=n;i++)cin>>B[i]; for(int i=1;i<=n;i++)A[i] = A[i] + A[i-1]; sort(A+1,A+n+1); sort(B+1,B+n+1); int ans = 0; for(int i=1;i<=n;i++){ ans += A[i] * B[n-i+1]; } cout<<ans<<endl; } ``` > [name=peienwu] ### 線段樹作法 ![](https://i.imgur.com/hePctby.png) 很奇怪,最近兩次的APCS第三題都有人想要砸資結,特別是線段樹,可能有些人特別偏愛線段樹吧! #### 題解 線段樹最原本的應該是區間詢問、單點修改,如果要區間修改的話就會用到[懶標](https://peienwu.com/seg1),所以實作上相對上比較複雜一點。這一題用線段樹的目的是區間加值,加值完過後的排序以及Greedy跟差分的作法是一樣的,用線段樹真的是多此兩舉(實作較複雜、較耗時)! 當然,這一題比較特別只有最後一起做單點查詢,因此不用懶標,最侯直接計算一路去經過的答案也行!下面的程式碼就是把完全包含區間的節點加值,不用使用到懶標,最後一次查詢。 #### 時間複雜度 區間加值 $O(m\log n)$,n個點的詢問 $O(n\log n)$,排序 $O(n\log n)$ 總時間複雜度:$O((n+m)\log n)$ #### AC程式碼 ```cpp= #include <bits/stdc++.h> #define int long long #define N 200005 #define IOS ios::sync_with_stdio(0),cin.tie(0) using namespace std; int n,m,t,A[N],B[N],ans = 0; struct node{ int val = 0,sz; }seg[4*N]; void build(int id,int l,int r){ seg[id].sz = r - l; if(r - l <= 1)return; int mid = (l + r) / 2; build(id*2,l,mid); build(id*2+1,mid,r); } void modify(int id,int l,int r,int ql,int qr,int val){ if(r <= l || r <= ql || l >= qr)return; if(ql <= l && qr >= r){ seg[id].val += val; return; } int mid = (l + r) / 2; modify(id*2,l,mid,ql,qr,val); modify(id*2+1,mid,r,ql,qr,val); } void query(int id,int l,int r,int val){ if(r <= l)return; ans += seg[id].val; if(r - l == 1)return; int mid = (l + r) / 2; if(val < mid)return query(id*2,l,mid,val); else return query(id*2+1,mid,r,val); } signed main(){ IOS; cin>>n>>m; build(1,1,n+1); while(m--){ int x,y,w;cin>>x>>y>>w; y++; modify(1,1,n+1,x,y,w); } for(int i=1;i<=n;i++){ ans = 0;query(1,1,n+1,i); A[i] = ans; } for(int i=1;i<=n;i++)cin>>B[i]; sort(A+1,A + n + 1); sort(B+1,B + n + 1); int ans = 0; for(int i=1;i<=n;i++)ans += A[i] * B[n-i+1]; cout<<ans<<endl; } ``` ## P4 真假子圖 [題目連結](https://zerojudge.tw/ShowProblem?problemid=g598) ### 二分搜尋+DFS作法 #### 題解 一開始看到這題,應該很難通靈出二分搜這個作法(我覺得光把題目看懂就有點難度了)。這題有一個條件要特別注意: > 保證若調查員的 k 個 pair 的結果和組長存留的 m 個 pair 不會產生矛盾, 則保證調查員的資料一定和原本 A, B 分組吻合 > 這一題每一個觀察員並可看成不是獨立的(假如一個觀察員不產生矛盾,則他回傳的那一些邊都會被沿用),所以題目 $p$ 筆詢問可以聯集一起處理。 將情報員當成點,合作關係當成邊,那麼合法的圖就會有兩個點集,點集中的點互不相鄰,也就是二分圖。 二分搜第一個使得圖變得不二分的人,把它消失,**最多重複3次**就做完了。 :::info **為什麼可以二分搜?** 二分搜是用來找一串01字串的分界點,並且必須具有單調性才能二分搜。這一題之所以會有單調性是因為,當我查詢觀察員 $P_i$ 的回傳資料是否正確時,會將前面 $P_1$ 到 $P_{i-1}$ 觀察員回傳的所有邊納入考慮。 假設有一個觀察員 $P_j (1\le j < i)$ 回傳的資料是錯誤的,這些邊會導致整張圖變成非二分圖,對於 $j$ 後面的所有點來說,都是非二分圖。這樣就有了以 $j$ 為分界點的單調性,即可二分搜。 ::: 二分圖判斷可以用 $dfs$ 做,$dfs$ 的時候把每個點塗上顏色,如果相鄰的點跟自己顏色一樣就表示這不是一張二分圖。 #### 時間複雜度 $O((n + m + pk)\log p)$ #### AC程式碼 ```cpp= #include <bits/stdc++.h> #define pb push_back #define mid (l + r) / 2 using namespace std; struct e{ int u, v; }; int n; array<bool, 10004> WA; //不可行的觀察員編號 array<int, 20004> vis; //DFS是否走訪、二分圖顏色 array<vector<e>, 10004> E; //每一個觀察員的回傳邊 array<vector<int>, 20004> G; //存進行DFS的圖 bool dfs(int u, int t){ //用DFS塗色、判斷二分圖 if(vis[u]) return 1; bool ans = 1; vis[u] = t; for(int v : G[u]){ if(vis[v] == t) return 0; ans &= dfs(v, 3 - t); } return ans; } bool check(int p){ //檢查第p個觀察員回傳是否正確 bool ans = 1; for(int i = 0; i < n; i++){ G[i].clear(); vis[i] = 0; } for(int i = 0; i <= p; i++){ for(auto [u, v] : E[i]){ //將觀察員的邊推入G G[u].pb(v); G[v].pb(u); } } for(int i = 0; i < n; i++){ ans &= dfs(i, 1); //將每一個連通塊 } return ans; } void BS(int l, int r){ //二分搜觀察員 if(check(r)) return; //當邊的連集不會讓圖有問題,則回傳 while(l != r){ if(check(mid)) l = mid + 1; else r = mid; } WA[l] = 1; E[l].clear(); //剔除一錯誤觀察員 } signed main(){ int m, p, k, a, b; cin >> n >> m; while(m--){ cin >> a >> b; E[0].pb({a, b}); } cin >> p >> k; for(int i = 1; i <= p; i++){ for(int j = 0; j < k; j++){ cin >> a >> b; E[i].pb({a, b}); } } for(int i = 0; i < 3; i++){ //至多三個觀察員 BS(0, p); } for(int i = 1; i <= p; i++){ if(WA[i]) cout << i << "\n"; } return 0; } ``` > [name=thanksone] ### DSU作法 > Idea From [name=Kennyfs] #### 題解 這一題的題目限制有說最多3個錯誤的情報員,因此我們可以用上面二分搜的方式做三次找到答案。如果題目**不限制錯誤調查員的數量**,也就是用二分搜時間會超時,但是用DSU可以在線性時間內完成! DSU的目的在處理集合問題,根據下面這個關鍵條件: > 保證若調查員的 k 個 pair 的結果和組長存留的 m 個 pair 不會產生矛盾, 則保證調查員的資料一定和原本 A, B 分組吻合 我們只要對每一筆詢問看會不會與組長手中的pair矛盾即可。如果每一次都做DFS,會發現時間複雜度是 $O(pn)$,必然超時。 DSU的想法是,我們將組長手中的圖中上每一個連通塊都分別塗上兩種顏色(必為二分圖,因此將兩邊各塗上不同顏色)。接著,把每個顏色當作初始的並查集中的集合,將每一筆觀察員回傳的邊的兩端指向的集合合併起來,過程中如果發生邊的兩端同屬一個集合,表示這是一個錯誤的觀察員。做完每一個觀察員之後,把所有變更過的還原成初始狀態(組長手中的圖)即可。 :::info **舉例** > 8 5 > 0 2 1 3 1 2 4 6 5 6 > 1 2 > 1 4 0 6 整個過程就是下面這張GIF: ![](https://i.imgur.com/gLpSD6p.gif) 步驟: 1. 利用DFS為組長手中的圖上色,每一個連通塊兩色(以編號1,2,3...) 2. 將每一個顏色當作並查集元素 3. 觀察員輸入的邊兩端(u,v)非同色,表示不發生矛盾,則將u所在集合與v所在集合的對方(連通塊兩色的另一色)合併 4. 重複 3. 共k次,如果發生(u,v)為同一色,則觀察員錯誤。 ::: #### 時間複雜度 $O(n + pk\alpha)$ #### AC程式碼 ```cpp= #include <bits/stdc++.h> #define IOS ios::sync_with_stdio(0),cin.tie(0) #define int long long #define N 20005 #define M 10005 using namespace std; int n,m,p,k; int color[N],boss[N],num[N]; bool WA[M],f; int other(int s){return (s%2)?s+1:s-1;} //other為同一連通塊另外一種顏色 vector<int> edge[N],change; void init(){ //初始化 memset(color,0,sizeof(color)); memset(WA,0,sizeof(WA)); } void dfs(int id,int col){ //對所有點上色 color[id] = col; for(auto i:edge[id]){ if(!color[i])dfs(i,other(col)); } } int find_boss(int id){ //尋找祖先、及路徑壓縮 if(boss[id] == id)return id; change.push_back(id); return boss[id] = find_boss(boss[id]); } signed main(){ IOS; init(); cin>>n>>m; while(m--){ int x,y;cin>>x>>y; edge[x].push_back(y); edge[y].push_back(x); } int now = 1; for(int i=0;i<n;i++){ //對所有點上色 if(!color[i]){ dfs(i,now);now += 2; } } for(int i=1;i<=now;i++){boss[i] = i;num[i] = 1;} cin>>p>>k; for(int i=1;i<=p;i++){ change.clear(); //儲存待更改的點集 f = 0; for(int j=0;j<k;j++){ int x,y;cin>>x>>y; if(f)continue; x = color[x],y = color[y]; //尋找邊兩端點的顏色所處的集合 int bx = find_boss(x),by = find_boss(y); int ox = find_boss(other(y)),oy = find_boss(other(x)); if(bx == by){WA[i] = 1;f = 1;continue;} //位於同一集合,此觀察員是錯的 //以下是啟發式合併(小的集合並到大的集合) if(num[bx] < num[ox]){ boss[bx] = ox;num[ox] += num[bx]; change.push_back(bx); } else{ boss[ox] = bx;num[bx] += num[ox]; change.push_back(ox); } if(num[by] < num[oy]){ boss[by] = oy;num[oy] += num[by]; change.push_back(by); } else{ boss[oy] = by;num[by] += num[oy]; change.push_back(oy); } } for(auto i : change){boss[i] = i;num[i] = 1;} //觀察員的邊結束,看完後復原 } for(int i=1;i<=p;i++)if(WA[i])cout<<i<<endl; //輸出最後錯誤觀察員答案 } ``` >[name=peienwu]