# APCS 2017年03月題解 ## 第一題 秘密差 ### 題目敘述 https://zerojudge.tw/ShowProblem?problemid=c290 ### 解套 N/A ### 題目分析 因為數字最長有1000位,所以用字串紀錄 直接讀入字串,開一個變數ans紀錄數字,並從頭到尾加/減即可 不過因為讀入的是字串,所以可以用「字元-'0'」來快速轉成數字 最後輸出ans的絕對值即可 時間複雜度$O(n)$ ### 解題流程 輸入$→$計算$→$輸出 ### 解題過程 #### 變數宣告 ```cpp=3 string x; int ans=0; ``` #### 輸入 ```cpp=6 cin>>x; ``` #### 計算 ```cpp=7 for(int i=0;i<x.size();i++)ans+=(i%2==0?x[i]-'0':'0'-x[i]); ``` #### 輸出 ```cpp=8 cout<<abs(ans); ``` 記得加絕對值 ### 完整程式碼 ```cpp=1 #include<bits/stdc++.h> using namespace std; string x; int ans=0; int main(){ cin>>x; for(int i=0;i<x.size();i++)ans+=(i%2==0?x[i]-'0':'0'-x[i]); cout<<abs(ans); } ``` ## 第二題 小群體 ### 題目敘述 https://zerojudge.tw/ShowProblem?problemid=c291 ### 解套 求一無向圖的連通塊數 ### 題目分析 用圖論的概念來看這題的話,就是把每個人看作節點,好友關係看做無向邊,就能把題目轉成求連通塊數量$~$只要跑dfs就解決了 如果沒有圖論的概念可以這樣想: 對於一群朋友來說,如果從A開始,不斷到A的朋友、A的朋友的朋友、...最後一定會回到A (其實就跟提示講的一樣) 因此如果我們用一個陣列been紀錄一個人有沒有處理過,並且從頭開始處理,一遇到沒處理過的就把朋友圈數(ans)加一,接著處理他、他朋友、他朋友的朋友...直到遇到處理過的就代表處理完了,如此每個人都跑過一遍就能得到答案了(遇到處理過的就直接跳過以節約時間) 時間複雜度$O(n)$ ### 解題流程 輸入$→$遍歷、dfs$→$輸出 ### 解題過程 #### 變數宣告 ```cpp=3 int n,friends[500000+10],ans=0; bool been[500000+10]={}; ``` n同題述,friends存每個人的朋友,其他同前述 #### dfs函式宣告 ```cpp=5 void dfs(int id){ if(been[friends[id]]==1)return; else{ been[friends[id]]=1; dfs(friends[id]); return; } } ``` 若其朋友到過就終止遞迴(第6行) 不然不斷往下遍歷 #### 輸入優化 ```cpp=14 ios::sync_with_stdio(0); cin.tie(0); ``` 不加也行 #### 輸入 ```cpp=16 cin>>n; for(int i=0;i<n;i++)cin>>friends[i]; ``` #### 遍歷、dfs ```cpp=18 for(int i=0;i<n;i++){ if(been[i]==0){ ans++; been[i]=1; dfs(i); } } ``` 遇到沒遇過的就把答案加一 #### 輸出 ```cpp=25 cout<<ans; ``` ### 完整程式碼 ```cpp=1 #include<bits/stdc++.h> using namespace std; int n,friends[500000+10],ans=0; bool been[500000+10]={}; void dfs(int id){ if(been[friends[id]]==1)return; else{ been[friends[id]]=1; dfs(friends[id]); return; } } int main(){ ios::sync_with_stdio(0); cin.tie(0); cin>>n; for(int i=0;i<n;i++)cin>>friends[i]; for(int i=0;i<n;i++){ if(been[i]==0){ ans++; been[i]=1; dfs(i); } } cout<<ans; } ``` ## 第三題 數字龍捲風 ### 題目敘述 https://zerojudge.tw/ShowProblem?problemid=c292 ### 解套 N/A ### 題目分析 正如題目提示所說 > 本題有多種處理方式,其中之一是觀察每次轉向與走的步數。例如起始方向是向左時,前幾步的走法是:左1、上 1、右 2、下 2、左 3、上 3、…… 一直到出界為止。 應該不難發現就方向而言,是「左上右下」不斷循環; 就行進距離而言,則是11223344... 因此我們用兩個變數xcur、ycur紀錄當前位置,cnt紀錄還剩幾個數字沒造訪過 接下來就是不斷地按提示的規則造訪、輸出 需要注意的是若cnt歸零(造訪完了)就要停止,並注意不能超出邊界(下面有較詳細的解釋) 時間複雜度$O(n^2)$ ### 解題流程 輸入$→$遍歷、輸出 ### 解題過程 #### 變數宣告 ```cpp=3 int n,direc,arr[50][50]; ``` direc存方向,存數字陣列 #### 輸入優化 ```cpp=5 ios::sync_with_stdio(0); cin.tie(0); ``` 不加也會過 #### 輸入 ```cpp=7 cin>>n>>direc; for(int i=0;i<n;i++){ for(int j=0;j<n;j++)cin>>arr[i][j]; } ``` #### 造訪用變數 ```cpp=11 int xcur=n/2,ycur=n/2,len=1,cnt=n*n; bool add=0; ``` xcur,ycur存位置,len存要走的長度,cnt存為造訪的位置數量,add決定要不要改變長度,後面解釋 #### 造訪(遍歷)、輸出 ```cpp=13 cout<<arr[xcur][ycur]; cnt--; while(cnt){ if(direc==0){ for(int i=1;i<=len;i++){ if(ycur-1>=0){ cout<<arr[xcur][ycur-1]; ycur--; cnt--; } else break; } } else if(direc==2){ for(int i=1;i<=len;i++){ if(ycur+1<n){ cout<<arr[xcur][ycur+1]; ycur++; cnt--; } else break; } } else if(direc==1){ for(int i=1;i<=len;i++){ if(xcur-1>=0){ cout<<arr[xcur-1][ycur]; xcur--; cnt--; } else break; } } else if(direc==3){ for(int i=1;i<=len;i++){ if(xcur+1<n){ cout<<arr[xcur+1][ycur]; xcur++; cnt--; } else break; } } if(add)len++; add=!add; direc=(direc+1)%4; } ``` 先處理初始位置(第13-14行) 接著依照方向不同有不同操作,以向左為例: 往左輸出並移動len個數字,除非已經到達邊界,那就直接break 其他方向依此類推 移動完後要處理移動距離與方向(第56-58行) 因為len每兩次要加1,所以令len在add為true時才加一(第56行), 而add則在true與false之間不斷互換(第57行)就能達到要的效果 而方向剛好是0變1,1變2,2變3,3變0,所以直接加一再對4取模就好 ### 完整程式碼 ```cpp=1 #include<bits/stdc++.h> using namespace std; int n,direc,arr[50][50]; int main(){ ios::sync_with_stdio(0); cin.tie(0); cin>>n>>direc; for(int i=0;i<n;i++){ for(int j=0;j<n;j++)cin>>arr[i][j]; } int xcur=n/2,ycur=n/2,len=1,cnt=n*n; bool add=0; cout<<arr[xcur][ycur]; cnt--; while(cnt){ if(direc==0){ for(int i=1;i<=len;i++){ if(ycur-1>=0){ cout<<arr[xcur][ycur-1]; ycur--; cnt--; } else break; } } else if(direc==2){ for(int i=1;i<=len;i++){ if(ycur+1<n){ cout<<arr[xcur][ycur+1]; ycur++; cnt--; } else break; } } else if(direc==1){ for(int i=1;i<=len;i++){ if(xcur-1>=0){ cout<<arr[xcur-1][ycur]; xcur--; cnt--; } else break; } } else if(direc==3){ for(int i=1;i<=len;i++){ if(xcur+1<n){ cout<<arr[xcur+1][ycur]; xcur++; cnt--; } else break; } } if(add)len++; add=!add; direc=(direc+1)%4; } } ``` ## 第四題 基地台 ### 題目敘述 https://zerojudge.tw/ShowProblem?problemid=c575 ### 解套 對答案二分搜 ### 題目分析 首先我們先看一下題目數字的範圍,幫助我們分析會過的複雜度;會發現$n,k\leq50000$,但座標範圍$\leq10^9$,代表直徑最長也可以到$10^9$ 因此很明顯地我們的演算法要嘛跟直徑無關(直接算出直徑),或是跟直徑的$log$有關 但說實話我不認為有辦法直接算出直徑,所以只能想辦法把直徑的部分壓到$log$ 最後的結論就是:我們要對直徑(答案)二分搜!~~by通靈~~ 理由是我們可以發現如果要檢驗一個直徑能不能只架$\leq k$個基地台,只需要$O(n)$的時間 因此只要對直徑(答案)做二分搜,再用$O(n)$的線性搜判斷一直徑可不可行,就能得到答案 線性搜實作上我們先把所有座標從小到大排序,檢驗直徑時從頭開始掃,把基地台的最左界設在起始座標,接下來的座標如果超出最右界就在新放一個基地台並重置右界,就能得到需要的基地台數量, 最後再跟$k$比較決定方案可不可行 二分搜就是一般的二分搜,如果中點符合線性搜條件就把右界往下縮,不然把左界往上縮 最終複雜度$O(nlogl)$(設l為座標長度) ### 解題流程 輸入$→$二分搜(用線性搜決定往上或往下縮)$→$輸出 ### 解題過程 #### 變數宣告 ```cpp=3 int n,k,p[500000+10]; ``` 同題述 #### 線性搜檢驗函式 ```cpp=4 bool check(int len){ int cnt=1,right=p[0]+len; for(int i=1;i<n;i++){ if(p[i]<=right){ continue; } else{ cnt++; right=p[i]+len; } } return cnt<=k; } ``` cnt紀錄需要的基地台數量,right紀錄當前能提供服務的最右界 如果新的座標還在服務範圍內就continue(第7-9行) 不然(第10行)就新設一個基地台(第11行)並更新右界(第12行) 最後回傳基地台數量合不合要求(第15行) #### 輸入優化 ```cpp=18 ios::sync_with_stdio(0); cin.tie(0); ``` 你懂的,不加也行 #### 輸入、排序 ```cpp=20 cin>>n>>k; for(int i=0;i<n;i++)cin>>p[i]; sort(p,p+n); ``` #### 二分搜 ```cpp=23 int l=1,r=1e9; while(l<=r){ if(l==r)break; else if(r==l+1){ if(!check(l))l=r; break; } else{ int m=(l+r)/2; if(check(m))r=m; else l=m+1; } } ``` 用check當檢驗標準 因為希望最後的基地台直徑越小越好,所以只剩兩個可能數字時(第26-29行),要用 ```cpp if(!check(l))l=r; ``` 因為只有在比較小的數字不行時我們才會用大的數字 ### 輸出 ```cpp=36 cout<<l; ``` ### 完整程式碼 ```cpp=1 #include<bits/stdc++.h> using namespace std; int n,k,p[500000+10]; bool check(int len){ int cnt=1,right=p[0]+len; for(int i=1;i<n;i++){ if(p[i]<=right){ continue; } else{ cnt++; right=p[i]+len; } } return cnt<=k; } int main(){ ios::sync_with_stdio(0); cin.tie(0); cin>>n>>k; for(int i=0;i<n;i++)cin>>p[i]; sort(p,p+n); int l=1,r=1e9; while(l<=r){ if(l==r)break; else if(r==l+1){ if(!check(l))l=r; break; } else{ int m=(l+r)/2; if(check(m))r=m; else l=m+1; } } cout<<l; } ```