---
# System prepended metadata

title: TOI 2020/12 新手同好會程式解析

---

# 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

:::

