Editorial for 機械鼠 (APCS 2023-10 初級)


簡潔題意

老鼠站在位置 \(x\),先選一個方向(左或右),然後沿著那個方向走,經過食物就可以停下來吃,吃完可以繼續往前、也可以就此結束。求最多能吃到幾個食物,以及最後吃的那個食物在哪個位置(\(3 \le n \le 20\) 且 \(n\) 為奇數,老鼠與食物的位置都在 \(-100\) 到 \(100\) 之間,食物位置不會與老鼠重疊)。

依正確通過的測試資料筆數給分,其中:

  • 第 1 子題組(\(60\) 分):\(n = 3\)。
  • 第 2 子題組(\(40\) 分):一般情況。

先看懂題目:選了方向,就把那一邊吃光

「吃完後可以選擇繼續往相同方向移動,或者結束今天的覓食」——沒有任何理由提早結束,再往前走只會多吃到食物,不會少吃。所以答案只有兩種可能:往右把右邊的食物吃光,或往左把左邊的食物吃光。

題目於是變成兩個問題:

  1. 位置比 \(x\) 大的食物有幾個?比 \(x\) 小的有幾個?選多的那一邊
  2. 最後停下來的位置,就是那個方向最遠的那個食物——往右是最大的,往左是最小的。

還有兩件題目已經幫你保證好的事:

  • 食物位置不會與老鼠重疊 ⇒ 每個食物不是在右邊就是在左邊,沒有第三種情況。
  • \(n\) 是奇數 ⇒ 左右兩邊的數量加起來是奇數,不可能一樣多 ⇒ 不必煩惱平手怎麼辦。

先拿下子題組 1(\(60\) 分):排一次序就看得出來

子題組 1 保證 \(n = 3\),只有三個食物。把它們讀成三個變數,用三次比較由小到大排好,叫做 \(a \le b \le c\)——接著只要看中間那個 \(b\) 站在哪一邊

  • \(b > x\):那 \(b\) 和 \(c\) 都在右邊 ⇒ 右邊至少 \(2\) 個、左邊最多 \(1\) 個 ⇒ 往右贏,最後停在最右邊的 \(c\)。
  • \(b < x\):那 \(a\) 和 \(b\) 都在左邊 ⇒ 往左贏,最後停在最左邊的 \(a\)。

剩下的只是數量是 \(2\) 還是 \(3\):

情況 輸出
\(b > x\) 而且 \(a > x\)(三個都在右邊) 3 和 \(c\)
\(b > x\) 但 \(a < x\) 2 和 \(c\)
\(b < x\) 但 \(c > x\) 2 和 \(a\)
\(b < x\) 而且 \(c < x\)(三個都在左邊) 3 和 \(a\)

排序用上冊 3.9 的三次比較加暫存變數,判斷用 if / else——沒有迴圈、也沒有陣列

#include <iostream>
using namespace std;

int main() {
    int x, n;
    cin >> x >> n;

    int a, b, c;
    cin >> a >> b >> c;

    // 三次比較,把 a、b、c 由小到大排好
    if (a > b) { int t = a; a = b; b = t; }
    if (a > c) { int t = a; a = c; c = t; }
    if (b > c) { int t = b; b = c; c = t; }

    if (b > x) {
        // b、c 都在右邊 → 往右贏,停在最右邊的 c
        if (a > x) {
            cout << 3 << ' ' << c << endl;
        } else {
            cout << 2 << ' ' << c << endl;
        }
    } else {
        // a、b 都在左邊 → 往左贏,停在最左邊的 a
        if (c < x) {
            cout << 3 << ' ' << a << endl;
        } else {
            cout << 2 << ' ' << a << endl;
        }
    }
    return 0;
}

\(n\) 在這個子題組固定是 \(3\),讀進來只是為了把輸入吃完,實際上沒有用到。到這裡用的東西全部落在語法書上冊的前三個單元——讀入輸出變數if

交出去之前先自己測兩種情況

範例 1 已經幫你測掉「\(2\) 個對 \(1\) 個、往左贏、位置取最小」,剩下兩種自己補:

輸入 預期輸出 在測什麼
0 3 / -1 -2 -3 3 -3 三個食物全在同一邊(另一邊一個都沒有)
-50 3 / -10 -20 -80 2 -10 贏的那一邊,最遠的食物是負數

範例 2 會答錯是正常的:它的 \(n = 9\),本來就不屬於子題組 1。這支程式只讀三個食物,對範例 2 會印出 2 13(正確答案是 5 100)。APCS 依正確通過的測試資料筆數給分,子題組 1 的 \(60\) 分照樣穩穩拿到。

從 \(60\) 分到 \(100\) 分:邊讀邊記

\(n\) 變大以後,不可能再一個食物配一個變數。但其實根本不需要把食物存下來——從頭到尾要用的只有四個數字:左邊幾個、右邊幾個、往左走會停在哪、往右走會停在哪。用一個 for 迴圈邊讀邊更新就好:

#include <iostream>
using namespace std;

int main() {
    int x, n;
    cin >> x >> n;

    int left_count = 0;   // 左邊有幾個食物
    int right_count = 0;  // 右邊有幾個食物
    int left_end = x;     // 往左走會停在哪(左邊最遠的食物)
    int right_end = x;    // 往右走會停在哪(右邊最遠的食物)

    for (int i = 1; i <= n; i++) {
        int food;
        cin >> food;
        if (food > x) {
            right_count++;
            if (food > right_end) {
                right_end = food;
            }
        } else {
            left_count++;
            if (food < left_end) {
                left_end = food;
            }
        }
    }

    if (left_count > right_count) {
        cout << left_count << ' ' << left_end << endl;
    } else {
        cout << right_count << ' ' << right_end << endl;
    }
    return 0;
}

四個地方值得看清楚:

  • 每個食物只會被算到一邊:不是 right_count++ 就是 left_count++,因為食物不會落在 \(x\) 上。
  • 兩個擂台if (food > right_end) right_end = food; 就是「比目前的紀錄還遠,就換它當紀錄」。
  • 擂台的初值直接用老鼠自己的位置 \(x\):右邊的食物一定比 \(x\) 大,所以只要右邊有食物,right_end 就一定會被換掉;左邊同理。而萬一那一邊一個食物都沒有,「往那邊走的終點」本來就是原地不動——這個初值自己就說得通。別設成 \(0\)——位置可以是負數,\(0\) 沒有「一定會被換掉」的保證。
  • 最後那個 else 就代表「右邊比較多」:\(n\) 是奇數,兩邊不可能一樣多,所以不必另外寫一個相等的分支。

測過再交:範例沒蓋到的四種情況

兩個範例都是「左右兩邊都有食物、而且贏的那一邊最遠的食物是正數」。沒被蓋到的自己補:

輸入 預期輸出 在測什麼
0 5 / -1 -2 -3 -4 -5 5 -5 全部食物都在同一邊,另一邊一個都沒有
-50 3 / -10 -20 -80 2 -10 贏的那一邊,最遠的食物是負數
0 5 / 5 5 -7 -8 -9 3 -9 食物位置有重複(題目只保證不與老鼠重疊)
0 19 / 1 2 3 4 5 6 7 8 9 10 -1 -2 -3 -4 -5 -6 -7 -8 -9 10 10 \(n\) 到上限、兩邊只差一個

第一列特別重要:一整邊都沒有食物是最容易被忘掉的情況,而兩個範例都不是這樣。

這四種都親眼看過正確,這題就穩了。

常犯錯誤
  1. 忘了「全部食物都在同一邊」:如果你的寫法是「先把食物排好,再找出第一個超過 \(x\) 的食物」,那當所有食物都在左邊時,這個「第一個」根本不存在——迴圈跑完什麼都沒印出來。兩個範例的右邊都有食物,所以都測不出來;用自測表第一列 0 5 / -1 -2 -3 -4 -5 一試就現形(畫面上一個字都沒有)。
  2. 擂台初值設成 \(0\):位置可以是負數,\(0\) 不見得會被換掉。範例 1 就會印出 2 0(正確答案是 2 1)——左邊的食物是 \(1\) 和 \(5\),都比 \(0\) 大,left_end 從頭到尾停在 \(0\)。
  3. 比的是「哪一邊比較遠」而不是「哪一邊比較多」:題目要的是吃最多,不是走最遠。兩個範例剛好兩種算法答案一樣、都矇得過去;換成 0 3 / 1 2 -100 就現形——正確答案是 2 2(右邊兩個),比距離的版本會印 1 -100
  4. 忘了取最遠,印成最後讀到的那個:範例 1 會印 2 5,範例 2 會印 5 25
  5. 數量和位置配錯邊:往左贏卻印右邊的位置。範例 1 會印 2 13
  6. 子題組 1 的版本寫成「只要有一個食物在右邊就往右」:右邊只有一個、左邊有兩個的時候就錯了。範例 1 會印 1 13