# TOI 2020/12 新手同好會程式解析 ## [第一題] [紅綠燈](https://toi-reg.csie.ntnu.edu.tw/question/202012-onsite/TrafficLight.pdf) ![](https://i.imgur.com/IGKAOKZ.png) --- ### 解題思路 先直觀思考前幾種狀況,用已知變數,列出他們對應的不等式。 根據題目敘述,以下設紅燈秒數為`r`,綠燈秒數為`g`,小明走路秒數為`s` 開始之前先提醒自己:不等號要特別小心。 題目假設***只要小明抵達路口時還是綠燈,他就一定可以(飛奔)通過路口***,數學上的意義是,只要是綠燈就算可以通過。 $g$ 秒那一瞬間,燈號由綠轉紅,不算是綠燈。$(g+r)+g$ 秒同理。 $g+r$ 秒那一瞬間,燈號由紅轉綠,算是綠燈。$(g+r)+(g+r)$ 秒同理。 :::spoiler <法一> 先判斷碰到綠燈的狀況 | Case | 說明 | 不等式 | | -------- | -------- | -------- | | 1 | 第一次紅燈結束前 (s未滿g秒) | $s<g$ | | 2 | 第一次紅燈結束後、第二次綠燈結束前 | $(g+r) \le s < (g+r)+g$ | | 3 | 第二次紅燈結束後、第三次綠燈結束前 | $(g+r)+(g+r) \le s < (g+r)+(g+r)+g$ | 因為在判斷碰到綠燈的可能,所以**只要是綠燈,都要記得加等號**。 接著,請問Case n-1的不等式長什麼樣子呢?沒錯,就是 $(g+r)\times n \le s < (g+r)\times n+g$ 這個一般式概括了所有情況。 Case 1把它改成$0\le s< g$,方便接下來的推導。 輸入`s`不會真的是0秒,不用擔心輸入$0$會出問題。 觀察所有Case共同的規律,如何化成此題的判斷條件,無論n是多少都適用? 我們把一般式同取除以$(g+r)$的餘數: $0 \le s\mod (g+r) < g$ 在程式實作時,兩個不等式拆開,再用and符號連接,此即判斷條件。 #### 程式碼1 ```cpp= #include <iostream> using namespace std; int main(){ int r, g, s; cin >> r >> g >> s; if (s % (r+g) >= 0 && s % (r+g) < g) cout << "yes" << endl; else cout << "no" << endl; return 0; } ``` 由於r, g, s均為正數,`s % (r+g)`的結果必大於等於0,因此前面那個條件,根本可以不用。上面的程式可以簡化為: #### 程式碼2 ```cpp= #include <iostream> using namespace std; int main(){ int r, g, s; cin >> r >> g >> s; if (s % (r+g) < g) cout << "yes" << endl; else cout << "no" << endl; return 0; } ``` ::: :::spoiler <法二> 先判斷碰到紅燈的狀況 | Case | 說明 | 不等式 | | -------- | -------- | -------- | | 1 | 第一次綠燈結束後、第一次紅燈結束前 | $g \le s < g+r$ | | 2 | 第二次綠燈結束後、第二次綠燈結束前 | $(g+r)+g \le s < (g+r)+(g+r)+g$ | | ... | ... | ... | | n | 第n次綠燈結束後、第n次綠燈結束前 | $(g+r)\times (n-1)+g \le s < (g+r)\times n$ | 同樣為了概括不同的n,我們把不等式同除以$(g+r)$取餘數。 這次不能直接轉換,$g \le s\mod (g+r) < 0$ 不是個合理的不等式。 我們拆開來想想看。左邊不等式是: $g \le s\mod (g+r)$ 很正常,沒什麼問題。 右邊的話,因為$s$ 差點被(綠+紅)整除,那餘數一定非常大,最大是$g+r-1$ 所以應該改成: $s\mod (g+r)\le g+r-1$ 但這個式子其實沒有必要。餘數運算有上限是必然的結果,這一題規範下限,意即使用左邊這個不等式即可。程式如下: #### 程式碼3 ```cpp= #include <iostream> using namespace std; int main(){ int r, g, s; cin >> r >> g >> s; if (s % (r+g) >= g) cout << "no" << endl; else cout << "yes" << endl; return 0; } ``` 程式碼3跟程式碼2比起來,只是換了不等號,yes/no換位置,執行結果一致。 ::: ## [第二題] [歌唱大賽](https://toi-reg.csie.ntnu.edu.tw/question/202012-onsite/Singer.pdf) ![](https://i.imgur.com/akcN3w4.png) --- ### 解題思路 * 為了儲存評審人數,需要有一個變數`n`。 * 為了儲存每位評審的給分,需要根據`n`值,宣告**陣列**`a[n]` * 使用迴圈結構(**for-loop**),一一定義陣列元素,儲存給分 * 使用迴圈結構,一一尋找**最大值**、**最小值**和總和 * 將總和扣掉最大值和最小值,即為所求 --- ### 如何尋找最大值/最小值 :::spoiler [子題一] 三數找極值 如果不會從多數找極值,就先從這題下手吧! 這個解法不能類推到第二個子題,但思考的過程,對於未來學習**排序演算法**是有幫助的。 三數找極值,需要用巢狀選擇結構(if-else)與不等式來比大小。 開始之前先想想,最多需要比幾次呢? * **Step 1. 用數學關係找出所有可能性** 三個數比大小,總共有幾種可能?這是個排列組合問題。 由小排到大或由大排到小,共有$3\times 2\times 1 = 6$ 種可能。 確認可能性的數量,當程式出現錯誤,你才找得出到底少了哪一種組合。 * **Step 2.用樹狀結構條列判斷步驟** 三個變數加上大於小於符號,馬上開始寫if-else,一定會亂成一團。 稍安勿躁,先在紙上或腦中,寫出你的判斷順序。 畫完圖之後會發現,最少2次、最多3次比較,才能確定abc的大小關係。 ```graphviz digraph hierarchy { nodesep=1.0 // increases the separation between nodes node [color=Red,fontname=Courier,shape=box] //All nodes will this shape and colour edge [color=Blue, style=dashed, fontcolor=darkgreen] //All the lines look like this "a<b?"->"b<c?" [label="a<b"] "a<b?"->"a<c?" [label="a>=b"] "b<c?"->"a<b<c" [label="b<c"] "b<c?"->"c<a?" [label="b>=c"] "c<a?"->"c<a<b"[label="c<a"] "c<a?"->"a<=c<b"[label="c>=a"] "a<c?"->"b<=a<c"[label="a<c"] "a<c?"->"c<b?"[label="a>=c"] "c<b?"->"c<b<=a"[label="c<b"] "c<b?"->"a<=b<=c"[label="c>=b"] } ``` * **Step 3.用if-else實作**(程式碼略) 實作為3層if-else巢狀迴圈。有些同學可能想,咦我可以用邏輯運算子呀! 像是`a<b && b<c`,那六種情況在同一層就解決了。這樣當然可以,但很容易出錯。上面的樹狀結構,最下面的節點確實有6種,但有的有等號有的沒有,很容易出錯,漏掉同分的情況,須要特別小心。 ::: :::spoiler [子題二] 多數找極值 多數找極值,顯然不能用前面的方法了。我們根本不知道有幾個數,就算知道了,各種可能也會比得沒完沒了。回到最初的問題,目的是找出最大值和最小值,其實根本沒有必要找出每一個數之間的大小關係。 開始之前,先假設一個情境:你眼前有一堆分數沒排序的考卷,你該如何尋找最高分的那一張呢?沒意外的話,你不可能記下所有數字,而是一個、一個數字仔細尋找,腦中只記一個數字:最高的那一個。 程式也是這樣的。運用迴圈結構,一個、一個數字去檢查,用一個變數`max`,記載目前為止的**最大值**。當考卷翻閱完畢,最高分就出來了。尋找最低分的考卷,也是一樣的道理。 至於最大值、最小值的初始值是多少呢?有兩種不同的想法: 1. 把兩個都設成第一眼看到的數字。 1. 分別設為確定比所有數字都小/大的數字。以考卷這題為例,滿分100分的話,就分別設成`-1`和`101`。 ::: --- ### 程式碼 依照解題邏輯一步一步做的話,程式碼長這個樣子: :::spoiler 程式碼1 ```cpp= #include <iostream> using namespace std; int main() { int n; cin >> n; int a[n]; for(int i=0; i<n; i++){ cin >> a[i]; } int min = a[0], max = a[0], sum = 0; for(int i=1; i<n; i++){ if (a[i] < min) min = a[i]; if (a[i] > max) max = a[i]; sum += a[i]; } cout << sum - min - max << endl; return 0; } ``` ::: <br /> 仔細看會發現,上面跟下面的for-loop,都是重複n。 把數字一個一個輸入,跟找總和、最大值、最小值,其實可以在同一個迴圈。 :::spoiler 程式碼2 ```cpp= #include <iostream> using namespace std; int main() { int n; cin >> n; int a[n]; int min = a[0], max = a[0], sum = 0; for(int i=0; i<n; i++){ cin >> a[i]; if (a[i] < min) min = a[i]; if (a[i] > max) max = a[i]; sum += a[i]; } cout << sum - min - max << endl; return 0; } ``` ::: <br /> 最後,有沒有人在想,就沒有辦法不能把所有數字排序一遍嗎?當然有,但排序法的程式比較難寫,今天先用懶人法:內建函數。 c++有一個函式庫`algorithm`,裡面有個函數`sort()`,可以將陣列由小排到大,有興趣的可以自己google看看。這題如果使用sort的話長這樣: :::spoiler 程式碼3 ```cpp= #include <iostream> #include <algorithm> using namespace std; int main() { int n; cin >> n; int a[n]; int sum = 0; for(int i=0; i<n; i++){ cin >> a[i]; sum += a[i]; } sort(a, a+n); cout << sum - a[0] - a[n-1] << endl; return 0; } ``` ::: ## [第三題] [傳送門](https://toi-reg.csie.ntnu.edu.tw/question/202012-onsite/Portal.pdf) ![](https://i.imgur.com/5TWYAel.png) :::spoiler 程式碼1 ```cpp= #include <iostream> using namespace std; int main() { int a, b, n; cin >> a >> b >> n; int door[n]; for(int i = 0; i < n; i++){ cin >> door[i]; } // find idx of a and b int ia, ib; for(int i = 0; i < n; i++){ if(door[i] == a) ia = i; if(door[i] == b) ib = i; } // swap idx of a and idx of b to ensure ia < ib int temp; if(ia>ib){ temp = ia; ia = ib; ib = temp; } // sum up int sum = 0; for(int i = 0; i < n; i++){ if(i == ia) i = ib+1; sum += door[i]; } cout << sum << "\n"; return 0; } ``` ::: :::spoiler :::