--- image: https://i.imgur.com/jmQC6SP.png --- # 20200704 APCS 題目整理與詳解 ## 整理者 CSY 教徒們 \CSY 教我/ ## 觀念題上半場 ### 題型分類 - 運行追蹤:9 題 - 程式填空:5 題 - 程式除錯:3 題 - 效能分析:1 題 - 基礎知識:2 題 ### 重要考點 - 邏輯運算子:4 題 - 全域變數:2 題 - 後序運算:2 題 - 搜尋:1 題 - merge:1 題 - GCD:1 題 ### 水題 1. x1 = true `(!x1 || !x2 ) && !x3 == true` A. x2 True , x3 True B. x2 True , x3 False C. x2 False , x3 True D. x2 False , x3 False 答案:D ### 難題 1. 使用輾轉相除法得到介於 $10^6$ ~ $2 \times 10^6$ 兩數之 GCD(最大公因數),至多需要的次數接近下列何者? A. 50 B. 500 C. 5000 D. 50000 答案:A 2. 將兩個各項係數皆不爲 $0$ 且依照降冪排列的多項式相加,並去掉其係數爲 $0$ 者。 本題是一題 debug 題,題目大致如下,由於筆者記憶力不好,因此變數名稱與原題稍有出入: :::spoiler 程式碼 ```cpp typedef struct term { int power, val; } poly; int add(poly a1[],poly a2[],poly a3[],int n1,int n2){ int id1 = id2 = id3 = 0; while(id1 < n1 && id2 < n2){ if(a1[id1].power == a2[id2].power){ int sum = a1[id1].val + a2[id2].val; if(sum == 0) continue; a3[id3].power = a1[id1].power; a3[id3].val = sum; id1 = id1 + 1; id2 = id2 + 1; } else if(a1[id1].power > a2[id2].power){ a3[id3].power = a1[id1].power; a3[id3].val = a1[id1].val; id1 = id1 + 1; } else{ a3[id3].power = a2[id2].power; a3[id3].val = a2[id2].val; id2 = id2 + 1; } id3 = id3 + 1; } while(id1 < n1){ a3[id3].power = a1[id1].power; a3[id3].val = a1[id1].val; id1 = id1 + 1; } while(id2 < n2){ a3[id3].power = a2[id2].power; a3[id3].val = a2[id2].val; id2 = id2 + 1; } return id3; } ``` ::: 其實這有點像 merge_sort 中 merge 的過程,在盯了很久後會發現,問題出在這個 continue,在 continue 前並沒有將 idx1 與 idx2 加 1,因此若 $sum = 0$ 時,程式將會進入無窮迴圈,選擇答案中會使 $sum = 0$ 的選項即可。 3. 欲在一個已排序好,大小為 n 的陣列 a[] 搜尋一個值 v 的位置。以下的code ...(a)... 和 ...(b)... 應填入什麼? A. $i \leq j, (i + j) / 3$ B. $i \leq j, (i + 2 \times j) / 3$ C. $i < j, (i + j) / 2$ D. $i < j, (i + 2 \times j) / 3$ :::spoiler 程式碼 ~~~cpp= int find_value(int a[], int n, int v) { int i = 0, j = n - 1, k; while (...(a)...) { k = ...(b)...; if (a[k] == v) { return k; } else if (a[k] < v) { i = k + 1; } else { j = k - 1; } } return -1 } ~~~ ::: 答案:B 這題在考的是二分搜的細節觀念。程式碼中的 $i$ 和 $j$ 分別代表目前解的下界和上界,並且是左閉右閉 $[i, j]$ 的區間。因此如果選了 C 或 D,當 $v == a[n - 1]$ 的時候會找不到。在選項 A 中,有些位置會到不了(例如 $n-1$ 之類的),因此答案為 B。 ### 題組 :::spoiler 程式碼 ```cpp= int n = 10000; for(int i=0,j=0;i<n;i=i+1,j=j+3){ a[i] = j % 20; } int cnt = 0; i = 0; while(i<n){ if(a[i] == 11){ cnt = cnt + 1; i = i + /*填空*/; } else{ i = i + 1; } } printf("%d\n",cnt); ``` ::: 1. 問 $a[20]$ 2. 陣列中 11 的個數 3. 如果要讓數到 11 的次數為 2500 在發現 $a[i]==11$ 時,要將 $i$ 增加多少 ## 觀念題下半場 ### 題型分類 - 運行追蹤:11 題 - 程式填空:1 題 - 程式除錯:5 題 - 效能分析:1 題 - 基礎知識:2 題 ### 重要考點 - bubble sort(逆序數對):3 題 - 極醜 code 閱讀:3 題 - 字串加密與處理:3 題 - queue:2 題 - 整理式子後轉爲等差級數:3 題 ### 難題 1. 宣告多個變數 iTotal, iAlpha, iBeta, iGamma 於全域變數,並在多個函式中宣告同名區域變數,並進行相當亂的呼叫,整體 code 長度極長且可讀性極差,甚至需要滾動滑鼠滾輪才能找到各變數的值。 ## 題組 1. 將數字由大排到小 code 很醜 :::spoiler 程式碼 ```cpp= int B[10] = {/*1~10 random shuffle*/} int h(int x){ return x; } int g(int x, int y){ return h(y) - h(x); } void f(int n){ int tmp, i, j; for(i = 0; i < n - 1; i++){ for(j = 0; j < n - i - 1; j++){ if(g(B[i], B[j]) > 0){ tmp = B[i]; B[i] = B[j]; B[j] = tmp; } } } } ``` ::: 2. 將數字依 sum of ....... sum of digit 由小排到大 :::spoiler 程式碼 ```cpp= int B[10] = {/*每一種 h(x) 會有兩個*/} int h(int x){ int sum = 0; do{ sum = sum + x % 10; x = x / 10; } while(x / 10); if(sum / 10) return h(sum); else return sum; } int g(int x, int y){ return h(x) - h(y); } void f(int n){ int tmp, i, j; for(i = 0; i < n - 1; i++){ for(j = 0; j < n - i - 1; j++){ if(g(B[i], B[j]) > 0){ tmp = B[i]; B[i] = B[j]; B[j] = tmp; } } } } ``` ::: ## 實作題 題目來源:黃致皓、吳庭安 ### p1 #### 題敘 給定二數字 $X, Y$,及多個購物清單,問至少買入各一 $X, Y$ 物之訂單有多少筆?買入一物的定義是,該物品加入購物籃的次數大於被拿出購物籃的次數。 購物清單格式: 每筆以 $0$ 結尾,結尾前的各數字若為正整數 $k$,代表將商品 $k$ 放入購物籃,若為 $-k$ 則代表將 $k$ 拿出。 #### 範例測資 :::spoiler Sample 1 Sample Input 1 ``` 1 8 5 1 8 0 5 6 0 2 7 0 8 1 0 33 22 0 ``` Output 1 ``` 2 ``` ::: #### 範圍 $1 \leq m \leq 100, 1 \leq t \leq 100$ #### 解法 以一個陣列記錄每種物品當前個數,最後判斷兩個物品個數是否都大於 0。 #### Solution Code :::spoiler Solution ```cpp //By Koios1143 #include<bits/stdc++.h> #define LL long long #define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0); #define pii pair<int,int> using namespace std; int a,b,t,tmp,used[105]; int main(){ IOS cin>>a>>b>>t; int ans=0; for(int i=0 ; i<t ; i++){ memset(used,0,sizeof(used)); while(cin>>tmp && tmp){ if(tmp>0) used[tmp]++; else used[-tmp]--; } if(used[a]>0 && used[b]>0) ans++; } cout<<ans<<"\n"; return 0; } ``` ::: :::spoiler Solution (python3) ```python res = 0 ls = list(map(int , input().strip().split(" "))) r = int(input().strip()) for i in range(r): s = list(map(int , (input().strip().split(" "))[:-1])) for j in s: if j < 0: s.remove(abs(j)) if ls[0] in s and ls[1] in s: res += 1 print(res) ``` ::: ### p2 #### 題敘 給定 $n$ 顆六面骰子及 $m$ 筆操作,骰子一開始面向之位置固定(4 在前,1 在上),有三種操作: - 交換任兩顆骰子 - 其中一顆骰子往前滾一次 - 其中一顆骰子往右滾一次 每筆操作格式如下: $a$ $b$ 若 $b$ 為正,代表將 $a$ $b$ 交換位置。 若 $b = -1$,將編號 $a$ 之骰子向前滾一面。 若 $b = -2$,將編號 $a$ 之骰子向右滾一面。 最後依序輸出每個骰子上方的點數。 一開始骰子: | |3| | | |-|-|-|-| |5|1|2|6| | |4| | | :::spoiler 示意圖 ![](https://i.imgur.com/UiuaXb5.png) ::: #### 範例測資 :::spoiler Sample 1 1 2 1 -2 1 -1 ::: :::spoiler Sample 2 3 3 2 -1 3 -2 3 1 ::: :::spoiler Samples 1&2 示意圖 ![](https://i.imgur.com/dcMVopz.png) ::: #### 範圍 $1 \leq n \leq 100, 1 \leq m \leq 100$ #### 配分 |分數|限制| |:--:|:--:| |20 %| n = 1 , 操作只有翻滾| |80 %| 無特別限制| #### 解法(Solution 1) 直接紀錄每個點數向前與向右轉後的點數為何,每次直接轉移過去即可 #### 解法(Solution 2) 由於對面相加爲 7,因此我們只需儲存其中上、前、右方之數字即可。向右翻轉時,原本的左方(7 - 右方數字)變爲新的上方,而原來的上方變爲新的右方,向前翻轉以此類推。 #### Solution Code :::spoiler Solution 2 ```cpp #include<bits/stdc++.h> using namespace std; struct dice{ int Top; int Front; int Right; }; dice arr[105]; int n,m; signed main(){ cin>>n>>m; for(int i=1;i<=n;i++){ arr[i] = {1,4,2}; } for(int i=1,a,b;i<=m;i++){ cin>>a>>b; if(b==-1){ arr[a].Front = 7 - arr[a].Front; swap(arr[a].Front,arr[a].Top); } else if(b==-2){ arr[a].Right = 7 - arr[a].Right; swap(arr[a].Top,arr[a].Right); } else{ swap(arr[a],arr[b]); } } for(int i=1;i<=n;i++){ cout<<arr[i].Top<<" \n"[i==n]; } } ``` ::: :::spoiler Solution 3 ```cpp //Suifeng0214 //APCS 20200704 pB #include <bits/stdc++.h> #define int long long using namespace std; const int MAX = 110; int arr[MAX][10]; //arr[第幾顆骰子][第幾面] signed main(){ int n, m; cin >> n >> m; for (int i = 1; i <= n; i++){ for (int j = 1; j <= 6; j++){ arr[i][j] = j; } } for (int i = 0; i < m; i++){ int a, b; cin >> a >> b; if (b == -1){ int tmp = arr[a][4]; arr[a][4] = arr[a][1]; arr[a][1] = arr[a][3]; arr[a][3] = arr[a][6]; arr[a][6] = tmp; }else if (b == -2){ int tmp = arr[a][6]; arr[a][6] = arr[a][2]; arr[a][2] = arr[a][1]; arr[a][1] = arr[a][5]; arr[a][5] = tmp; }else{ swap(arr[a], arr[b]); } } for (int i = 1; i <= n; i++){ cout << arr[i][1]; //輸出朝上的位置>< if (i!=n)cout << " "; } } ``` ::: ### p3 #### 題敘 環狀迷宮有 $n$ 個房間,依序編號爲 $0$ ~ $n-1$。 玩家初始位置在房間 $0$,且只能依編號 $0,1 ...n-1,0,1$ 的環狀順序走。 第 $i$ 個房間有點數 $P_i$,每次離開房間 $i$,就可獲得 $P_i$ 點數。 逃脫迷宮方法:依序蒐集 $m$ 把鑰匙,第 $i$ 把鑰匙可用點數 $Q_i$ 兌換,保證所有 $Q_i$ 都 $\leq$ $P_i$ 之總和。 每次兌換完,手中的點數就會全部被清空,為了盡快逃出,玩家只要點數夠,就會馬上兌換鑰匙。 最後玩家蒐集完鑰匙後,會停在哪個房間? :::spoiler Sample > Sample Input > ``` > 7 3 > 2 1 5 3 4 5 4 > 8 9 12 > ``` > Sample Output > ``` > 3 > ``` 解釋: 走過 $0,1,2$ 號房間後得到 $8$ 點點數($\ge8$),於 $3$ 號房間得到第一把鑰匙,點數歸零。 走過 $3,4,5$ 號房間後得到 $12$ 點點數($\ge9$),於 $6$ 號房間得到第二把鑰匙,點數歸零。 走過 $6,0,1,2$ 號房間後得到 $12$ 點點數($\ge12$),於 $3$ 號房間得到第三把鑰匙,點數歸零。 最後停在 $3$ 號房間,輸出 $3$。 ::: :::spoiler 示意圖 ![](https://i.imgur.com/m5aUMat.png) ::: #### 範圍 $1 \leq n \leq 2 \times 10^5, 1 \leq m \leq 2 \times 10^4$ #### 配分 |分數|限制| |:--:|:--:| |20%|$1\leq n,m \leq 100$| |80%|無特殊限制| #### 解法 先假設本題是在一個正常序列上,要怎麼去做呢? 題目要求的是找到最小的 x 使得 $a[i] + a[i+1] ..... +\: a[x-1] \ge$ 鑰匙點數 我們可以令前綴和 $s[i] = a[0] + a[1]+ ... +\: a[i]$,接著,問題就轉換成,對於當前的位置 $p$,找到一個最小的位置 $x$,使得 $s[x-1]-s[p-1]\ge$ 鑰匙點數。這個地方由於 $s$ 序列具有單調性,我們可以用二分搜去處理。 那麼在環狀序列上,該如何做呢? 可以發現,若將原序列複製一次,也就是令$a[i+n] = a[i]$,則對於一個 $p$ 找到的最小位置 $x$, $x$ % $n$ 就會是得到這一把鑰匙後,在環上的位置! 且由於題目保證鑰匙點數小於所有房間點數和,$x$ 最大就只會是 $p+n$,因此將原序列複製一次後就相當足夠了。若本題沒有上述限制,其實也只要將需要的鑰匙點數預先 mod 所有房間點數和即可。 複雜度:$O(n + m\: log\:n)$ #### Solution Code :::spoiler Solution ~~~cpp //by 8e7 #include <iostream> #include <algorithm> #define ll long long using namespace std; int main() { int n, m; cin >> n >> m; ll room[n], key[m], pref[2 * n]; for (int i = 0;i < n;i++) cin >> room[i], pref[i] = pref[i + n] = room[i]; for (int i = 0;i < m;i++) cin >> key[i]; for (int i = 1;i < 2 * n;i++) { pref[i] += pref[i - 1]; } int ind = 0; for (int i = 0;i < m;i++) { ll left = ind ? pref[ind - 1] : 0; ind = (lower_bound(pref, pref + 2 * n, left + key[i]) - pref) + 1; ind %= n; } cout << ind << endl; } ~~~ ::: ### p4 #### 題敘 有 $n$ 條 RNA 病毒,各自擁有一個 RNA 序列,且長度皆為 $m$。 每條的輸入格式為下: ( 自身編號 , 親代編號 , RNA 序列 ) 若自身編號等於親代編號則是根源。 其中自身編號為 $1$ ~ $n$ RNA 序列為由 A、U、C、G、@ 組成的字串。@ 為未知的字元,可任意為 A、U、C、G 其中一種。 求所有的親代和子代最小的差距總和。 - 差距定義為兩相鄰節點之基因序列不同位置數量。 :::spoiler Sample 1 Sample Input 1 ``` 2 3 1 1 AAC 2 1 A@@ ``` Sample Output 1 ``` 0 ``` ::: :::spoiler Sample 2 Sample Input 2 ``` 6 1 1 1 @ 2 1 @ 3 1 C 4 1 C 5 2 A 6 2 A ``` Sample Output 2 ``` 1 ``` ::: :::spoiler Sample 2 示意圖 ```graphviz digraph hierarchy { nodesep=1.0 node [color=blue,fontname=Courier,shape=box] edge [color=Blue, style=dashed] "1, 1, @"->{"2, 1, @" "3, 1, C" "4, 1, C"} "2, 1, @"->{"5, 2, A", "6, 2, A"} } ``` ::: #### 範圍 $1\leq n \leq 1000, 1\leq m \leq 80$ #### 配分 |分數|限制| |:-:|:-:| |20 %|無任何 @ 字元,且對於每個非根源節點 $i$,其親代 $j$ 之編號皆小於它| |40 %|對於每個非根源節點 $i$,其親代 $j$ 之編號皆小於它| |40 %| 無特別限制| #### 解法 首先觀察到,每個位置的字元互不相干,因此我們只需要解決長度 = 1 的問題 m 次就好了。 再來,對於一個點的各個子節點所形成的子樹,他們的答案互不相關。也就是說,假設 $A$ 有兩個子樹 $B$、$C$,$B$ 選擇的鹼基是什麼完全不會影響到 $C$ 的最佳選擇,$B$、$C$ 的最佳選擇皆只與其子樹與父節點 $A$ 有關,因此我們考慮使用樹形動態規劃技巧 ( 樹 DP ) 來解決問題。 那要儲存什麼值呢?先以遞迴方式取得 $B$ 子樹的最佳答案,接著直接從 $A$ 進行轉移?這樣做的話會發現,你無法判斷 $A$ 的鹼基與 $B$ 的鹼基是否相同,不知道是否要將答案加一。因此,只儲存單一最佳答案並不能解決此一問題,考慮增加表格維度,如下: 設 $dp[i][j]$ 爲以 $i$ 爲根,且 $i$ 的鹼基編號爲 $j$ 的子樹的最小修改次數,則可對於所有 $i$ 的子節點 $x$ ,枚舉 $x$ 的鹼基編號 $k$ 進行轉移。若 $j \neq k$,則將變異數加一。 轉移式如下: $dp[i][j] = \Sigma\: \min\limits_{0<k<s} (dp[x][k] + (j\neq k))$ 。 複雜度:$O(s^2 nm)$ $s$ 爲鹼基種類數 可進一步儲存每個點以任意鹼基的最小值 $mn[x]$,如此在轉移時只需使用 $dp[i][j] = min(mn[x]+1,dp[x][j])$ 即可,複雜度降爲 $O(snm)$。(感謝吳邦一教授提醒) #### Solution Code :::spoiler Solution1 O(s^2nm) ```cpp //by emanlaicepsa #include<bits/stdc++.h> #define ll long long #define pii pair<int,int> #define fi first #define se second #define IOS ios::sync_with_stdio(0),cin.tie(0); #define pb push_back #define all(n) (n).begin(),(n).end() using namespace std; vector<int> E[1005]; string s[1005]; string ord = "AUCG"; ll dp[1005][4],n,m; void dfs(int x,int c){ for(auto &i:E[x]) dfs(i,c); for(int i=0;i<4;i++){ if(s[x][c] != '@' && s[x][c] != ord[i]){ dp[x][i] = 1e9; continue; } for(auto &j:E[x]){ ll mn = 1e9; for(int k=0;k<4;k++){ mn = min(mn,dp[j][k] + (k!=i)); } dp[x][i] += mn; } } } signed main(){ IOS; cin>>n>>m; for(int i=1,a,b;i<=n;i++){ cin>>a>>b; cin>>s[a]; if(a!=b)E[b].pb(a); } ll ans = 0; for(int i=0;i<m;i++){ memset(dp,0,sizeof(dp)); dfs(1,i); ans += *min_element(dp[1],dp[1]+4); } cout<<ans<<'\n'; } ``` ::: :::spoiler Solution2 O(snm) ```cpp= //by emanlaicepsa #include<bits/stdc++.h> #define ll long long #define pii pair<int,int> #define fi first #define se second #define IOS ios::sync_with_stdio(0),cin.tie(0); #define pb push_back #define all(n) (n).begin(),(n).end() using namespace std; vector<int> E[1005]; string s[1005]; string ord = "AUCG"; ll dp[1005][4],n,m; ll dp2[1005]; void dfs(int x,int c){ for(auto &i:E[x]) dfs(i,c); for(int i=0;i<4;i++){ if(s[x][c] != '@' && s[x][c] != ord[i]){ dp[x][i] = 1e9; continue; } for(auto &j:E[x]){ dp[x][i] += min(dp2[j]+1,dp[j][i]); } } dp2[x] = *min_element(dp[x],dp[x]+4); } signed main(){ IOS; cin>>n>>m; for(int i=1,a,b;i<=n;i++){ cin>>a>>b; cin>>s[a]; if(a!=b)E[b].pb(a); } ll ans = 0; for(int i=0;i<m;i++){ memset(dp,0,sizeof(dp)); memset(dp2,0,sizeof(dp2)); dfs(1,i); ans += *min_element(dp[1],dp[1]+4); } cout<<ans<<'\n'; } ``` :::