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 都會成立。

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

常犯錯誤
  1. 先扣血才跳:把兩個 if 寫在 pos += hp; 前面。位置 \(0\) 是任何數的倍數,程式一開始就先被扣了一輪——範例 1 印 -1、範例 2 印 1
  2. 兩個判斷串成 if / else if:同時是兩個數的倍數時只扣到一邊。範例 2 的最後一步剛好踩在 \(18\)(同時是 \(3\) 和 \(2\) 的倍數),這樣寫會印 20
  3. 忘了生命值會變,每一步都跳 \(k\) 格:寫成 pos += k;。範例 1 矇得過去(生命值全程都是 \(1\)),範例 2 會印 28
  4. 終止條件寫成 while (hp != 0):生命值可能一次被扣掉好幾點、直接從正數跨過 \(0\) 變成負數,條件就永遠成立——範例 2 會卡在無窮迴圈。要用 hp > 0
  5. 輸入順序讀錯:讀成 cin >> k >> x1 >> x2 >> y1 >> y2;。範例 2 的數字剛好換過來一樣、矇得過去,範例 1 會印 1。輸入是「一行一組」,第二行是 \(x_1\) 和 \(y_1\)、第三行才是 \(x_2\) 和 \(y_2\)。
  6. 子題組 1 的版本漏掉 \(2k\) 那個分支:只判斷 \(k\),其他一律印 \(3k\)。範例 1 也矇得過去(\(k = 1\) 本來就要走三步),\(k = 2\) 就會印 6(正確答案是 4)——自測表第二列就是為了堵它。