Editorial for 機械鼠 (APCS 2023-10 初級)
簡潔題意
老鼠站在位置 \(x\),先選一個方向(左或右),然後沿著那個方向走,經過食物就可以停下來吃,吃完可以繼續往前、也可以就此結束。求最多能吃到幾個食物,以及最後吃的那個食物在哪個位置(\(3 \le n \le 20\) 且 \(n\) 為奇數,老鼠與食物的位置都在 \(-100\) 到 \(100\) 之間,食物位置不會與老鼠重疊)。
依正確通過的測試資料筆數給分,其中:
- 第 1 子題組(\(60\) 分):\(n = 3\)。
- 第 2 子題組(\(40\) 分):一般情況。
先看懂題目:選了方向,就把那一邊吃光
「吃完後可以選擇繼續往相同方向移動,或者結束今天的覓食」——沒有任何理由提早結束,再往前走只會多吃到食物,不會少吃。所以答案只有兩種可能:往右把右邊的食物吃光,或往左把左邊的食物吃光。
題目於是變成兩個問題:
- 位置比 \(x\) 大的食物有幾個?比 \(x\) 小的有幾個?選多的那一邊。
- 最後停下來的位置,就是那個方向最遠的那個食物——往右是最大的,往左是最小的。
還有兩件題目已經幫你保證好的事:
- 食物位置不會與老鼠重疊 ⇒ 每個食物不是在右邊就是在左邊,沒有第三種情況。
- \(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\) 到上限、兩邊只差一個 |
第一列特別重要:一整邊都沒有食物是最容易被忘掉的情況,而兩個範例都不是這樣。
這四種都親眼看過正確,這題就穩了。
常犯錯誤
- 忘了「全部食物都在同一邊」:如果你的寫法是「先把食物排好,再找出第一個超過 \(x\) 的食物」,那當所有食物都在左邊時,這個「第一個」根本不存在——迴圈跑完什麼都沒印出來。兩個範例的右邊都有食物,所以都測不出來;用自測表第一列
0 5/-1 -2 -3 -4 -5一試就現形(畫面上一個字都沒有)。 - 擂台初值設成 \(0\):位置可以是負數,\(0\) 不見得會被換掉。範例 1 就會印出
2 0(正確答案是2 1)——左邊的食物是 \(1\) 和 \(5\),都比 \(0\) 大,left_end從頭到尾停在 \(0\)。 - 比的是「哪一邊比較遠」而不是「哪一邊比較多」:題目要的是吃最多,不是走最遠。兩個範例剛好兩種算法答案一樣、都矇得過去;換成
0 3/1 2 -100就現形——正確答案是2 2(右邊兩個),比距離的版本會印1 -100。 - 忘了取最遠,印成最後讀到的那個:範例 1 會印
2 5,範例 2 會印5 25。 - 數量和位置配錯邊:往左贏卻印右邊的位置。範例 1 會印
2 13。 - 子題組 1 的版本寫成「只要有一個食物在右邊就往右」:右邊只有一個、左邊有兩個的時候就錯了。範例 1 會印
1 13。