Editorial for 路徑偵測 (APCS 2023-06 初級)


簡潔題意

從 \((0, 0)\) 出發,依序走過 \(n\) 個座標點,每一步只往東西南北其中一個方向直走;題目保證第一個點在 X 軸正向(一開始向東)。統計整條路徑上左轉、右轉、迴轉各幾次,依序輸出(\(1 \le n \le 100\);直行不算轉彎)。

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

  • 子題組 1(\(60\) 分):\(n = 2\)。
  • 子題組 2(\(40\) 分):\(n \le 100\)。

先拿下子題組 1(\(60\) 分):只有一個轉彎要判斷

\(n = 2\) 時路徑只有兩段:\((0,0) \to\) 第一個點(題目保證向東)、第一個點 \(\to\) 第二個點。轉彎只發生在第一個點,判斷第二段往哪走就結束了,連迴圈都不用——只用到語法書上冊第三單元為止的語法。

第二段的方向,拿第二個點跟第一個點比(注意:是跟第一個點比,不是跟原點比):

  • \(y\) 變大 → 轉向北。原本面朝東,北在左手邊 → 左轉
  • \(y\) 變小 → 轉向南 → 右轉
  • \(x\) 變小 → 掉頭向西 → 迴轉
  • \(x\) 變大 → 繼續向東 → 直行,什麼都不用數。
#include <iostream>
using namespace std;

int main() {
    int n;                      // 子題組 1 保證 n = 2
    int x1, y1, x2, y2;
    cin >> n;
    cin >> x1 >> y1 >> x2 >> y2;

    // 第一段保證向東,所以只要判斷第二段轉去了哪裡
    if (y2 > y1) {
        cout << 1 << " " << 0 << " " << 0 << endl;   // 轉向北=左轉
    } else if (y2 < y1) {
        cout << 0 << " " << 1 << " " << 0 << endl;   // 轉向南=右轉
    } else if (x2 < x1) {
        cout << 0 << " " << 0 << " " << 1 << endl;   // 掉頭向西=迴轉
    } else {
        cout << 0 << " " << 0 << " " << 0 << endl;   // 繼續向東=直行
    }
    return 0;
}

範例 2 的 \(n = 9\),這支程式讀不完,答案會是錯的——這是正常的:APCS 逐筆給分,這一版穩穩拿下子題組 1 的 \(60\) 分。

從 \(60\) 分到 \(100\) 分:把方向編成數字,轉彎就變成減法

\(n\) 變大之後,每個點都要做一樣的判斷:「上一段朝哪、這一段朝哪、所以轉了什麼」。方向只有四種,把它們編成 \(0\)、\(1\)、\(2\)、\(3\),而且故意按逆時針排:東 \(0\)、北 \(1\)、西 \(2\)、南 \(3\)。

這個編號的好處是:左轉一次,編號剛好 \(+1\)(東 \(\to\) 北 \(\to\) 西 \(\to\) 南 \(\to\) 東)。所以「這一段編號 \(-\) 上一段編號」就直接告訴你轉了幾個逆時針 \(90\) 度:

編號差 意義
\(0\) 直行(不計)
\(+1\) 左轉
\(+2\) 迴轉
\(+3\)(也就是 \(-1\)) 右轉

差是負的怎麼辦?繞了一圈回來而已——先 \(+4\) 再取餘數 % 4,就把 \(-1\) 折回 \(3\)、\(-2\) 折回 \(2\)。(先加再取餘是必要的:C++ 對負數取餘會給出負的結果,\(-1 \% 4\) 是 \(-1\) 不是 \(3\)。)

每一段的方向跟上一個點比座標就知道;整條路徑用 pxpy 記住上一個點、prevDir 記住上一段方向,一路滑過去。新增的語法只有第四單元的單層 for——不需要陣列。

#include <iostream>
using namespace std;

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

    int px, py;                 // 上一個點
    cin >> px >> py;            // 第一個點:題目保證從 (0,0) 走到這裡是向東
    int prevDir = 0;            // 上一段方向:0=東 1=北 2=西 3=南(逆時針編號)
    int L = 0, R = 0, U = 0;

    for (int i = 2; i <= n; i++) {
        int x, y;
        cin >> x >> y;

        int dir;                // 這一段的方向:跟上一個點比就知道
        if (x > px) {
            dir = 0;            // 東
        } else if (y > py) {
            dir = 1;            // 北
        } else if (x < px) {
            dir = 2;            // 西
        } else {
            dir = 3;            // 南
        }

        int turn = (dir - prevDir + 4) % 4;   // 逆時針轉了幾個 90 度
        if (turn == 1) L += 1;                // 左轉
        if (turn == 2) U += 1;                // 迴轉
        if (turn == 3) R += 1;                // 右轉(turn == 0 是直行,不計)

        px = x;
        py = y;
        prevDir = dir;
    }
    cout << L << " " << R << " " << U << endl;
    return 0;
}

測過再交:兩個範例裡一次「直行」都沒有

範例 2 蓋到了左轉、右轉、迴轉,但整條路徑每一步都在轉彎——「繼續直走」這個情況兩個範例都沒考。自己補測:

輸入 正確輸出 在測什麼
32 05 05 3 1 0 0 中途直行:第二段繼續向東,不能被數成任何一種轉彎
25 02 0 0 0 1 向西掉頭、但 \(x\) 還是正的:方向要跟上一個點比,不是跟原點比
43 03 43 16 1 2 0 1 左轉後迴轉再左轉:同一種轉彎出現兩次

三個都親眼看過正確,這題就穩了。

常犯錯誤
  1. 左轉右轉搞反:「面朝東時,北在左手邊」——想像自己站在路上,不要用看地圖的直覺。範例 1 當場現形(印 0 1 0,正確是 1 0 0)。
  2. 忘了第一段就朝東:從第一個點才開始記方向,第一個點上的轉彎整個沒算到。範例 1 當場現形(印 0 0 0)。
  3. 直行也被數進去:編號差 \(0\) 什麼都不是,別把它跟迴轉混在一起。兩個範例都矇得過(範例裡沒有直行)——自測表第一列就是為它設計的。
  4. 負數取餘忘了先 \(+4\)(dir - prevDir) % 4 算出 \(-1\)、\(-3\),右轉和一半的迴轉都對不上號。範例 1 剛好只有左轉、照樣過;範例 2 會現形(印 2 0 3,正確是 2 1 5)。
  5. 方向跟原點比:第一個點剛好在 X 軸上,\(y\) 的比較兩種寫法一樣,範例 1 矇得過;「往西走但 \(x\) 還是正的」就會分不出迴轉和直行——自測表第二列就是為它設計的。