# APCS 2021.11.07 --- ## 1. [修補圍籬](https://zerojudge.tw/ShowProblem?problemid=g595) ---- ### 想法: 直接遍歷一次陣列,當遇到 $0$ 時就對左右兩個位置取 $min$ 可以把 $h[0]$ 跟 $h[n+1]$ 設成一個很大的數,實做起來比較方便 ::: spoiler code : ```cpp= #include<iostream> using namespace std; int h[105],n,ans; int main(){ cin>>n; h[0]=h[n+1]=100000; // 將 h[0] 跟 h[n+1] 設為一個很大的數 for(int i=1;i<=n;i++) cin>>h[i]; for(int i=1;i<=n;i++) if(h[i]==0) ans+=min(h[i-1],h[i+1]); // 如果 h[i] 是 0 就對左右取 min(最小值) cout<<ans<<endl; return 0; } ``` ::: --- ## 2. [動線安排](https://zerojudge.tw/ShowProblem?problemid=g596) 這題大概是這次 APCS 第二難(或是第一)的題目惹 ### 想法: 我們存每根木樁往上、下、左、右是否有連出線,每次操作完就從每根木樁往外走, 同時在圖上標記,最後再看有幾個格子有標記就好,詳情請見 code :::spoiler code : ```cpp= #include<iostream> using namespace std; bool is[105][105]; // 每個位置是否有木樁 bool ex[105][105][4]; // 每個位置的木樁往上、左、右、下是否有連線 /* 0 -> 上 * 1 -> 左 * 2 -> 右 * 3 -> 下 * 為什麼要醬設呢,之後就知道惹~ * */ int m,n,h; int dx[4]={-1,0,0,1},dy[4]={0,-1,1,0}; // 4個方向每次移動的座標改變量 bool mk[105][105]; // 有沒有被標記 bool op(int x,int y,int d,int t){ // 從 (x,y) 這個格子往d方向,做各種操作 // 並且這次的操作是 t // t=0 連上,並回傳是否有遇到木樁 // t=1 拔掉 // t=2 標記 while(x>=0&&x<m&&y>=0&&y<n){ if(is[x][y]){ if(t==1||t==0){ ex[x][y][3-d]=(t^1); // 我們發現相反方向的代號加起來剛好是3耶!!! // 於是乎就可以順便修改這個木樁的狀態啦!!! } return 1; } if(t==2) mk[x][y]=1; x+=dx[d]; y+=dy[d]; } return 0; } int cnt(){ // 計算當前圖上的答案 for(int i=0;i<m;i++)for(int j=0;j<n;j++) mk[i][j]=0; // 把標記清空 for(int i=0;i<m;i++)for(int j=0;j<n;j++)if(is[i][j]){ // 找出有木樁的格子 for(int d=0;d<4;d++)if(ex[i][j][d]){ is[i][j]=0; // 避免直接回傳 op(i,j,d,2); is[i][j]=1; // 回復原狀 } } int ans=0; for(int i=0;i<m;i++)for(int j=0;j<n;j++){ if(is[i][j]||mk[i][j]) ans++; // 有木樁或有標記都會算進答案 } return ans; } int main(){ int maxn=0; cin>>m>>n>>h; while(h--){ int r,c,t; cin>>r>>c>>t; if(t==0){ // 加入木樁 for(int d=0;d<4;d++)// 4個方向 if(op(r,c,d,t)){ ex[r][c][d]=1; } is[r][c]=1; } else{ // 拔掉木樁 is[r][c]=0; for(int d=0;d<4;d++)// 4個方向 if(ex[r][c][d]){ // 如果原本有連出線,就要修改他連到的那根木樁的狀態 ex[r][c][d]=0; op(r,c,d,t); } } maxn=max(maxn,cnt()); } cout<<maxn<<endl<<cnt()<<endl; return 0; } ``` ::: ::: spoiler 乾淨的 code : ```cpp= #include<iostream> using namespace std; bool is[105][105]; bool ex[105][105][4]; int m,n,h; int dx[4]={-1,0,0,1},dy[4]={0,-1,1,0}; bool mk[105][105]; bool op(int x,int y,int d,int t){ while(x>=0&&x<m&&y>=0&&y<n){ if(is[x][y]){ if(t==1||t==0) ex[x][y][3-d]=(t^1); return 1; } if(t==2) mk[x][y]=1; x+=dx[d]; y+=dy[d]; } return 0; } int cnt(){ for(int i=0;i<m;i++)for(int j=0;j<n;j++) mk[i][j]=0; for(int i=0;i<m;i++)for(int j=0;j<n;j++)if(is[i][j]){ for(int d=0;d<4;d++)if(ex[i][j][d]){ is[i][j]=0; op(i,j,d,2); is[i][j]=1; } } int ans=0; for(int i=0;i<m;i++)for(int j=0;j<n;j++){ if(is[i][j]||mk[i][j]) ans++; } return ans; } int main(){ int maxn=0; cin>>m>>n>>h; while(h--){ int r,c,t; cin>>r>>c>>t; if(t==0){ for(int d=0;d<4;d++)if(op(r,c,d,t)) ex[r][c][d]=1; is[r][c]=1; } else{ is[r][c]=0; for(int d=0;d<4;d++)if(ex[r][c][d]){ ex[r][c][d]=0; op(r,c,d,t); } } maxn=max(maxn,cnt()); } cout<<maxn<<endl<<cnt()<<endl; return 0; } ``` ::: --- ## 3. [生產線](https://zerojudge.tw/ShowProblem?problemid=g597) 這題會用到一個小技巧,叫做差分 ---- ### 什麼是差分? 對於一個正整數序列 $A$ ,我們用另一個陣列 $B$ 來記錄 $A$ 中每一項的差 具體來說 $B[i]=A[i]-A[i-1]$ 因此當我們想要在 $A$ 中的 $l$ ~ $r$ 加上某個數 $x$ 時 我們可以直接在 $B[l]$ 加上 $x$ , 並在 $B[r+1]$ 扣除 $x$ 這麼一來,我們只要從頭跑一次差分陣列 $B$ 就可以知道每一項是多少了 ---- ### 想法: 先利用差分處理完每台機器需要處理多少資料後 再將 "要處理比較多資料的機器" 跟 "產出速率較快的位置配對" 就可以得出答案惹 ~ 要小心 overflow ::: spoiler code ```cpp= #include<iostream> #include<algorithm> // sort #include<vector> // vector #define ll long long using namespace std; int n,m; ll dif[200005]; // 差分陣列 int main(){ vector<ll> pos,mac; // pos 是存每個位置的產出速率 // mac 是存每台機器要處理的資料量 cin>>n>>m; while(m--){ int l,r,w; cin>>l>>r>>w; dif[l]+=w; dif[r+1]-=w; } for(int i=1;i<=n;i++){ int t; cin>>t; pos.push_back(t); } ll now=0; // 用來得出差分陣列所派表的每一項是多少 for(int i=1;i<=n;i++){ now+=dif[i]; // 加上差分陣列(變化量)就可以得到這項的值 mac.push_back(now); } // 排序 pos 跟 mac sort(pos.begin(),pos.end()); sort(mac.begin(),mac.end()); ll ans=0; for(int i=0;i<n;i++){ ans+=pos[i]*mac[n-i-1]; // 大配小 } cout<<ans<<endl; return 0; } ``` ::: --- ## 4. [真假子圖](https://zerojudge.tw/ShowProblem?problemid=g598) 這題好像有蠻多解法的,但我只會 dsu... ### 什麼是 dsu ? 中文是併查集,顧名思義,就是一種支援合併與查詢的資料結構 詳情請自行 google ### 想法: 因為題目保證只有三個人會與組長的資料矛盾,且剩下的人彼此都是不矛盾的 因此我們可以在發現矛盾時,將資料恢復成只有組長的資料的狀態 #### 要怎麼知道有沒有矛盾呢? 我們可以幫每個人建立一個假想敵, 編號為 $i$ 的人的假想敵編號為 $i+n$ 當我們要建立一個 ${x,y}$ 之間的合作關係時 如果 $x$ 跟 $y$ 是同組的,或者 $x+n$ 跟 $y+n$ 是同組的 那就產生矛盾了,因為合作關係的雙方必須是不同組的 同樣的,對於這組合作關係 我們把 $x$ 和 $y+n$ 合併成同一組 同樣的 $y$ 和 $x+n$ 合併為同一組 這樣就可以快樂 AC 了~ ::: spoiler code : ```cpp= #include<iostream> #include<vector> using namespace std; const int N=4e4+5; // 因為還有假想敵所以陣列要開兩倍 vector<int> ini; // 組長手中的資料 int n,m; // dsu 的部分 int boss[N],siz[N]; int find(int x){ if(boss[x]==x) return x; return boss[x]=find(boss[x]); } void merge(int a,int b){ if(siz[a]<siz[b]) swap(a,b); siz[a]+=siz[b]; boss[b]=a; } // 回復成組長的資料的部分 void repair(){ for(int i=0;i<2*n;i++){ siz[i]=1; boss[i]=i; } for(int i=0;i<m;i++){ int a=ini[i*2],b=ini[i*2+1]; int fa=find(a),fb=find(b); int FA=find(a+n),FB=find(b+n); merge(fa,FB); merge(FA,fb); } } signed main(){ cin>>n>>m; for(int i=0;i<2*n;i++){ // 初始化 siz[i]=1; boss[i]=i; } for(int i=0;i<m;i++){ int a,b; cin>>a>>b; ini.push_back(a); ini.push_back(b); int fa=find(a),fb=find(b); int FA=find(a+n),FB=find(b+n); // 假想敵 merge(fa,FB); merge(FA,fb); } vector<int> ans; int p,k; cin>>p>>k; for(int i=0;i<p;i++){ bool ok=1; // 是否矛盾 for(int j=0;j<k;j++){ int a,b; cin>>a>>b; if(!ok) continue; // 如果已經矛盾了就不用往下做了 int fa=find(a),fb=find(b); int FA=find(a+n),FB=find(b+n); // 假想敵 if(fa==fb||FA==FB){ // 產生矛盾! ok=0; continue; } merge(fa,FB); merge(FA,fb); } if(!ok){ ans.push_back(i+1); repair(); } } for(int i:ans) cout<<i<<endl; } ``` :::