# APCS 2020年07月題解 ## 第一題 購物車 ### 題目敘述 https://zerojudge.tw/ShowProblem?problemid=f579 ### 解套 N/A ### 題目分析 直接按題目要求一次處理即可 對於每位客人,開兩個變數x與y紀錄商品a與b分別買了幾個,當讀到絕對值等於a或b時, 代表是對a或b做拿出或放入的動作,這時直接把x或y加上輸入值即可(正負號會被考慮到且拿出、放入剛好能抵銷),最後看兩者有沒有都大於0就能判斷是否都有買,若有則把答案加一 時間複雜度$O(n×每位客人操作次數)$ ### 解題流程 輸入$→$依序處理$→$輸出 ### 解題過程 #### 變數宣告 ```cpp=3 int a,b,n,inp,ans=0; ``` inp是輸入用的變數,ans存答案 #### 輸入 ```cpp=5 cin>>a>>b>>n; ``` #### 處理每位客人 ```cpp=6 while(n--){ int x=0,y=0; while(cin>>inp&&inp){ if(abs(inp)==a)x+=inp; if(abs(inp)==b)y+=inp; } if(x>0&&y>0)ans++; } ``` 先開x,y紀錄(第7行),接著不斷輸入inp直到其等於0為止(第8行) 若絕對值等於a或b就加到x或y(第9、10行),最後若x與y都大於0就更新答案 #### 輸出 ```cpp=14 cout<<ans; ``` ### 完整程式碼 ```cpp=1 #include<bits/stdc++.h> using namespace std; int a,b,n,inp,ans=0; int main(){ cin>>a>>b>>n; while(n--){ int x=0,y=0; while(cin>>inp&&inp){ if(abs(inp)==a)x+=inp; if(abs(inp)==b)y+=inp; } if(x>0&&y>0)ans++; } cout<<ans; } ``` ## 第二題 骰子 ### 題目敘述 https://zerojudge.tw/ShowProblem?problemid=f580 ### 解套 N/A ### 題目分析 首先根據題目敘述,一開始骰子的狀態如下 | 上 | 下 | 左 | 右 | 前 | 後 | | - | - | - | - | - | - | | 1 | 6 | 5 | 2 | 4 | 3 | 然後**向前旋轉**就是前→下、下→後、後→上、上→前 **向右旋轉**就是右→下、下→左、左→上、上→右 那為了方便我寫了一個dice[結構](https://oi-wiki.org/lang/struct/)紀錄骰子的上下左右前後,再開一個dice陣列紀錄各骰子狀況 最後按題目要求操作即可 時間複雜度$O(n)$ ### 解題流程 輸入$→$操作$→$輸出 ### 解題過程 #### 變數、結構宣告 ```cpp=3 int n,m,a,b; struct dice{ int up,down,left,right,ffront,bback; dice():up(1),down(6),left(5),right(2),ffront(4),bback(3){} }; dice arr[20+10]; ``` n,m,a,b使題述相同 因為front,back是C++內建函式名稱,所以改成ffront,bback避免撞名(在這題沒有影響) 第6行是dice的初始化 #### 輸入 ```cpp=10 cin>>n>>m; while(m--){ cin>>a>>b; ``` #### 操作—交換 ```cpp=13 if(a>0&&b>0)swap(arr[a],arr[b]); ``` 若a,b都大於零代表交換操作,直接swap就好 #### 操作—向前旋轉 ```cpp=14 else if(b==-1){ int tmp=arr[a].up; arr[a].up=arr[a].bback; arr[a].bback=arr[a].down; arr[a].down=arr[a].ffront; arr[a].ffront=tmp; } ``` 依照前述改變值 #### 操作—向右旋轉 ```cpp=21 else{ int tmp=arr[a].up; arr[a].up=arr[a].left; arr[a].left=arr[a].down; arr[a].down=arr[a].right; arr[a].right=tmp; } ``` 依照前述改變值 #### 輸出 ```cpp=29 for(int i=1;i<=n;i++)cout<<arr[i].up<<" "; ``` ### 完整程式碼 ```cpp=1 #include<bits/stdc++.h> using namespace std; int n,m,a,b; struct dice{ int up,down,left,right,ffront,bback; dice() : up(1),down(6),left(5),right(2),ffront(4),bback(3) {} }; dice arr[20+10]; int main(){ cin>>n>>m; while(m--){ cin>>a>>b; if(a>0&&b>0)swap(arr[a],arr[b]); else if(b==-1){ int tmp=arr[a].up; arr[a].up=arr[a].bback; arr[a].bback=arr[a].down; arr[a].down=arr[a].ffront; arr[a].ffront=tmp; } else{ int tmp=arr[a].up; arr[a].up=arr[a].left; arr[a].left=arr[a].down; arr[a].down=arr[a].right; arr[a].right=tmp; } } for(int i=1;i<=n;i++)cout<<arr[i].up<<" "; } ``` ## 第三題 圓環出口 ### 題目敘述 https://zerojudge.tw/ShowProblem?problemid=f581 ### 解套 N/A ### 題目分析 一開始想說直接硬做,也就是一個一個跑直到分數湊滿,但寫完之後估一下複雜度就發現不會過... ($O(nm)=4×10^9>10^8$) 於是就把腦筋動到也是搜尋但比較快的二分搜上去了~~~通靈~~ 首先會發現如果只單獨知道每一個房間的分數會很難搜尋, 但如果改成存前綴和就能對累積的分數進行二分搜 具體操作上開一個變數id紀錄當前的位置,而要求獲得$x$分其實就是要求累積分數達到「當前房間累積分數+$x$分」,當然還要處理溢出情況(這時候就減掉所有房間的分數從頭開始) 因此我們只要維護id,並每次透過二分搜前綴和找到下一個位置,操作$m$次就能得到答案了 時間複雜度$O(mlogn)$ ### 解題流程 輸入$→$二分搜$→$輸出 ### 解題過程 #### 變數宣告 ```cpp=3 int n,m,a; vector<int>v; ``` n,m同題述,v存房間分數前綴和,a只是輸入用 #### 輸入、v初始化 ```cpp=6 cin>>n>>m; v.assign(n,0); for(int i=0;i<n;i++){ cin>>a; v[i]=a+(i==0?0:v[i-1]); } ``` 第10行存前綴和 #### 尋找下一個房間 ```cpp=12 int id=0; for(int i=0;i<m;i++){ cin>>a; int tgt=a+(id==0?0:v[id-1]); if(tgt>v[n-1])tgt-=v[n-1]; id=(lower_bound(v.begin(),v.end(),tgt)-v.begin()+1)%n; } ``` 一開始在0號房間(第12行) 接著輸入目標分數(第14行),並將之從「加多少分」轉為「累積分數多少分」(第15行) 其中在轉為累積多少分的時候要加的是v[id-1],所以要注意id=0時不要加 那這樣會不會少加到呢?其實不會,因為id=0只有在一開始和因為溢出而回到最開始時兩種狀況,而這兩種狀況都還沒算0號房間的分數,所以不會有問題 加完之後如果超出總分的話就扣掉總分代表又繞了一圈(第16行) (題目保證要的分數不超過總分,所以不用擔心要多減) (這裡不能用tgt%=v[n-1],因為tgt==v[n-1]時不能減) 最後用lower_bound找出達到該累積分數最少需要的房間編號,並+1後%n跳到下一個房間(第17行) #### 輸出 ```cpp=20 cout<<id; ``` ### 完整程式碼 ```cpp=1 #include<bits/stdc++.h> using namespace std; int n,m,a; vector<int>v; int main(){ cin>>n>>m; v.assign(n,0); for(int i=0;i<n;i++){ cin>>a; v[i]=a+(i==0?0:v[i-1]); } int id=0; for(int i=0;i<m;i++){ cin>>a; int tgt=a+(id==0?0:v[id-1]); if(tgt>v[n-1])tgt-=v[n-1]; id=(lower_bound(v.begin(),v.end(),tgt)-v.begin()+1)%n; } cout<<id; } ``` ## 第四題 病毒演化 ### 題目敘述 https://zerojudge.tw/ShowProblem?problemid=f582 ### 解套 求所有字串與其父字串的漢明距離(相異字元數)總和 ### 題目分析 首先我們會發現每一個位置的距離會是互相獨立的,也就是說我們可以分開處理字串的每一個位置,不用擔心結果互相影響,藉此降低題目的難度 那麼對於每一個位置,要怎麼計算最小距離呢? 我們可以~~透過通靈~~發現因為是樹狀結構,感覺會跟dfs或bfs有關,不過因為有父子關係的節點比較有直接關聯,所以用dfs來算 具體的做法如下 我們定義$dp[i][j][k]$為第$i$個字串第$j$個位置是$k$的時候,由下往上到他累積的最小距離 (@,A,U,G,C轉成0,1,2,3,4),其中$dp[i][j][0]$同時也會代表最理想情況下的值 而我們對每一個j從根節點往下遍歷 首先對於葉節點來說,如果他是@,那他的五個值都會是0; 不然就是自己原本的字元與@會是0,其他無限大 而對於非葉節點來說,首先要先把子節點都遍歷一遍, 接著看他自己是甚麼字元 如果是@話那就枚舉A,U,G,C四種狀況,用子節點計算看哪種狀況值會最小,並把@的狀況設為四者的最小值 不然的話就把自己字元除外設成無限大,自己的字元用子節點計算看哪種狀況值會最小,並把@設為自身字元的值 (計算方式後面用程式碼解釋) 每個位置處理完後讓答案加上$dp[root][j][0]$,就能算出最後答案 (即使字元不是@,計算過程也會讓@是最小距離,所以直接加他就好) 時間複雜度$O(nm)$ ### 解題流程 輸入、初始化$→$遍歷位置、運算$→$輸出 ### 解題過程 #### 變數宣告 ```cpp=3 int n,m,a,b,root,ans=0,dp[1000+10][80+10][5]={}; string s[1000+10]; vector<int>v[1000+10]; unordered_map<char,int>mp={{'@',0},{'A',1},{'U',2},{'G',3},{'C',4}}; ``` n,m同題述,a,b是題目中的i,j,s存字串,v存圖,root存根,ans存答案,dp如前述,mp是為了方便轉換字元與數字 dp一開始預設都是0,有助後面操作 #### 操作函式 **葉節點** ```cpp=8 if(v[cur].size()==0){ if(s[cur][pos]=='@')return; else{ for(int i=0;i<=4;i++)dp[cur][pos][i]=INT_MAX; dp[cur][pos][0]=dp[cur][pos][mp[s[cur][pos]]]=0; return; } } ``` 沒有子節點代表是葉節點(第8行) 若本身是@就直接回傳(都是0) 不然把本身字元與最佳情況設成0,其他設無限大 **非葉節點** ```cpp=16 else{ for(int i:v[cur])func(i,pos); int c=mp[s[cur][pos]]; if(c!=0){ for(int i=0;i<=4;i++)dp[cur][pos][i]=INT_MAX; dp[cur][pos][c]=0; for(int i:v[cur])dp[cur][pos][c]+=min(dp[i][pos][c],dp[i][pos][0]+1); dp[cur][pos][0]=dp[cur][pos][c]; return; } else{ for(int i=1;i<=4;i++){ for(int j:v[cur])dp[cur][pos][i]+=min(dp[j][pos][i],dp[j][pos][0]+1); } dp[cur][pos][0]=min({dp[cur][pos][1],dp[cur][pos][2],dp[cur][pos][3],dp[cur][pos][4]}); return; } } ``` 先向下遞迴,把子節點處理完(第17行) 然後為了方便設c為字元轉成的數字 分兩種狀況討論 1. **字元是@** 分別處理A,T,C,G四種狀況(用子節點更新數值),然後把最佳狀況設為四者的最小值 2. **其他字元** 將自身除外設為無限大,用子節點更新自身的數值,然後把最佳狀況設為自身的數值 #### 輸入、存圖 ```cpp=36 cin>>n>>m; for(int i=0;i<n;i++){ cin>>a>>b; cin>>s[a]; if(a==b)root=a; else v[b].push_back(a); } ``` 第40行判斷是否為根節點 #### 依序處理每個位置 ```cpp=43 for(int i=0;i<m;i++){ func(root,i); ans+=dp[root][i][0]; } ``` 先處理每個位置(第44行) 再把答案加上跟節點的最好可能(第41行) ### 完整程式碼 ```cpp=1 #include<bits/stdc++.h> using namespace std; int n,m,a,b,root,ans=0,dp[1000+10][80+10][5]={}; string s[1000+10]; vector<int>v[1000+10]; unordered_map<char,int>mp={{'@',0},{'A',1},{'U',2},{'G',3},{'C',4}}; void func(int cur,int pos){ if(v[cur].size()==0){ if(s[cur][pos]=='@')return; else{ for(int i=0;i<=4;i++)dp[cur][pos][i]=INT_MAX; dp[cur][pos][0]=dp[cur][pos][mp[s[cur][pos]]]=0; return; } } else{ for(int i:v[cur])func(i,pos); int c=mp[s[cur][pos]]; if(c!=0){ for(int i=0;i<=4;i++)dp[cur][pos][i]=INT_MAX; dp[cur][pos][c]=0; for(int i:v[cur])dp[cur][pos][c]+=min(dp[i][pos][c],dp[i][pos][0]+1); dp[cur][pos][0]=dp[cur][pos][c]; return; } else{ for(int i=1;i<=4;i++){ for(int j:v[cur])dp[cur][pos][i]+=min(dp[j][pos][i],dp[j][pos][0]+1); } dp[cur][pos][0]=min({dp[cur][pos][1],dp[cur][pos][2],dp[cur][pos][3],dp[cur][pos][4]}); return; } } } int main(){ cin>>n>>m; for(int i=0;i<n;i++){ cin>>a>>b; cin>>s[a]; if(a==b)root=a; else v[b].push_back(a); } for(int i=0;i<m;i++){ func(root,i); ans+=dp[root][i][0]; } cout<<ans; } ```