Editorial for 小心陷阱 (APCS 2025-06 初級)
簡潔題意
從位置 \(0\) 出發,一開始有 \(k\) 點生命值。每一步往右跳「目前生命值」那麼多格;跳完之後,落點如果是 \(x_1\) 的倍數就扣 \(y_1\) 點、是 \(x_2\) 的倍數就扣 \(y_2\) 點(同時是兩者的倍數就兩邊都扣)。生命值變成 \(0\) 或負數時遊戲結束,輸出結束時的位置(\(1 \le k \le 20\)、\(1 \le x_1, x_2 \le 10\)、\(1 \le y_1, y_2 \le 20\))。
依正確通過的測試資料筆數給分,其中:
- 第 1 子題組(\(40\) 分):\(x_1 = 3\)、\(x_2 = 4\)、\(y_1 = y_2 = k\)。
- 第 2 子題組(\(60\) 分):無額外限制。
先拿下子題組 1(\(40\) 分):連迴圈都不用
子題組 1 保證 \(y_1 = y_2 = k\),而一開始的生命值剛好也是 \(k\)。這代表只要踩到一次倍數,生命值就會從 \(k\) 直接掉到 \(0\) 或更低,遊戲當場結束。
所以這個子題組真正要問的只有一件事:第一次踩到 \(3\) 或 \(4\) 的倍數,是在哪一格?
踩到之前生命值都還是 \(k\),每一步都跳 \(k\) 格,走過的位置依序是 \(k\)、\(2k\)、\(3k\)、……。而 \(3k\) 一定是 \(3\) 的倍數,所以最晚走到第三步就一定結束,答案只有三種可能:
| 情況 | 答案 |
|---|---|
| \(k\) 本身是 \(3\) 或 \(4\) 的倍數 | \(k\) |
| 否則,\(2k\) 是 \(3\) 或 \(4\) 的倍數 | \(2k\) |
| 否則 | \(3k\) |
三種情況接成一條 if / else if / else,「是 \(3\) 的倍數或是 \(4\) 的倍數」用 || 串起來,判斷倍數則是看餘數是不是 \(0\):
#include <iostream>
using namespace std;
int main() {
int k, x1, y1, x2, y2;
cin >> k;
cin >> x1 >> y1;
cin >> x2 >> y2;
if (k % 3 == 0 || k % 4 == 0) {
cout << k << endl;
} else if (2 * k % 3 == 0 || 2 * k % 4 == 0) {
cout << 2 * k << endl;
} else {
cout << 3 * k << endl;
}
return 0;
}
\(x_1\)、\(y_1\)、\(x_2\)、\(y_2\) 在這個子題組都是固定的,讀進來只是為了把輸入吃完,實際上一個都沒用到。
到這裡用到的東西全部落在語法書上冊的前三個單元——讀入輸出、變數、餘數、if——連迴圈都還沒用到。
交出去之前先自己測三種情況
三種可能各測一次,才知道三個分支都寫對了:
| 輸入 | 預期輸出 | 在測什麼 |
|---|---|---|
12 / 3 12 / 4 12 |
12 |
第一步就踩到(而且 \(12\) 同時是 \(3\) 和 \(4\) 的倍數) |
2 / 3 2 / 4 2 |
4 |
走兩步才踩到(\(2k = 4\) 是 \(4\) 的倍數) |
1 / 3 1 / 4 1(範例 1) |
3 |
走三步才踩到 |
範例 2 會答錯是正常的:它的 \(y_1 = 2\)、\(y_2 = 3\) 都不等於 \(k = 7\),本來就不屬於子題組 1。這支程式對範例 2 會印出 21(正確答案是 18)。APCS 依正確通過的測試資料筆數給分,子題組 1 的 \(40\) 分照樣穩穩拿到。
從 \(40\) 分到 \(100\) 分:照著規則走一遍
一般情況下 \(y_1\)、\(y_2\) 跟 \(k\) 沒有關係,踩到一次倍數不一定會死——生命值可能只是變小,然後用變小後的生命值繼續往前跳。範例 2 就是這樣:生命值 \(7 \to 4 \to -1\),每次跳的距離也跟著改變。
這種「規則講得很清楚、只是要一步一步照著做」的題目,最省事的解法就是把規則原封不動翻成程式。用一個 while 迴圈,只要生命值還是正的就繼續:
#include <iostream>
using namespace std;
int main() {
int k, x1, y1, x2, y2;
cin >> k;
cin >> x1 >> y1;
cin >> x2 >> y2;
int pos = 0; // 目前位置
int hp = k; // 目前生命值
while (hp > 0) {
pos += hp; // 往右跳「目前」的生命值那麼多格
if (pos % x1 == 0) hp -= y1; // 落點是 x1 的倍數
if (pos % x2 == 0) hp -= y2; // 落點是 x2 的倍數(兩個都是就都扣)
}
cout << pos << endl;
return 0;
}
三個地方對應三條規則,每一個都有人寫錯:
pos += hp;— 跳的格數是跳之前的生命值。先跳,跳完才檢查落點、才扣血,順序反過來就全錯。- 兩個各自獨立的
if— 不是if/else if。同時是兩個數的倍數時,\(y_1\) 和 \(y_2\) 都要扣。 while (hp > 0)— 生命值 \(\le 0\) 就結束,剛好扣到 \(0\) 也算結束。
這支程式能拿滿分,長度卻和子題組 1 那條三選一的 if 鏈差不多——差別在於「要走幾步」不用你事先想清楚了,交給迴圈自己走就好。
這個迴圈會不會停不下來?
不會。生命值每被扣一次至少少 \(1\) 點,而它最多只有 \(20\) 點,被扣二十次就一定結束。剩下的疑慮是「會不會一直跳都踩不到倍數」——把題目限制內全部 \(80\) 萬組輸入都跑過一遍,位置最遠只走到 \(910\),跳最多的那組也只跳了 \(92\) 步。
順帶一提,\(910\) 離 int 的上限很遠,位置用 int 存綽綽有餘。
測過再交:範例沒蓋到的四種情況
兩個範例已經幫你測掉不少:範例 1 有「走三步才踩到」和「生命值剛好扣到 \(0\)」,範例 2 有「同時是兩個數的倍數、兩邊都扣」和「生命值中途變小、跳的距離跟著變」。
沒被蓋到的自己補。想單獨測某件事,就從「要讓哪一行被觸發」倒推輸入:
| 輸入 | 預期輸出 | 在測什麼 |
|---|---|---|
6 / 6 3 / 6 3 |
6 |
\(x_1\) 與 \(x_2\) 相同:同一格仍然要扣兩次 |
20 / 1 1 / 1 1 |
110 |
\(x_1 = x_2 = 1\),每一格都是倍數、每一步都扣血 |
18 / 7 1 / 10 1 |
910 |
整個題目裡走最遠的一組(跳了 \(85\) 步) |
1 / 1 20 / 10 20 |
1 |
一次就把生命值扣成大負數 |
第一列特別重要:\(x_1\) 和 \(x_2\) 沒有規定要不同,兩個一樣時兩個 if 都會成立。
這四種都親眼看過正確,這題就穩了。
常犯錯誤
- 先扣血才跳:把兩個
if寫在pos += hp;前面。位置 \(0\) 是任何數的倍數,程式一開始就先被扣了一輪——範例 1 印-1、範例 2 印1。 - 兩個判斷串成
if/else if:同時是兩個數的倍數時只扣到一邊。範例 2 的最後一步剛好踩在 \(18\)(同時是 \(3\) 和 \(2\) 的倍數),這樣寫會印20。 - 忘了生命值會變,每一步都跳 \(k\) 格:寫成
pos += k;。範例 1 矇得過去(生命值全程都是 \(1\)),範例 2 會印28。 - 終止條件寫成
while (hp != 0):生命值可能一次被扣掉好幾點、直接從正數跨過 \(0\) 變成負數,條件就永遠成立——範例 2 會卡在無窮迴圈。要用hp > 0。 - 輸入順序讀錯:讀成
cin >> k >> x1 >> x2 >> y1 >> y2;。範例 2 的數字剛好換過來一樣、矇得過去,範例 1 會印1。輸入是「一行一組」,第二行是 \(x_1\) 和 \(y_1\)、第三行才是 \(x_2\) 和 \(y_2\)。 - 子題組 1 的版本漏掉 \(2k\) 那個分支:只判斷 \(k\),其他一律印 \(3k\)。範例 1 也矇得過去(\(k = 1\) 本來就要走三步),\(k = 2\) 就會印
6(正確答案是4)——自測表第二列就是為了堵它。