轉動迷宮 題解|APCS 2026 年 11 月中級 第 3 題


簡潔題意

\(H \times W\) 的地圖上,每一格是牆壁 #、路 .、起點 S 或終點 E(起點、終點也是路),地圖外圍當成牆壁。彈珠放在起點,以原地圖來看重力一開始朝下;每一步彈珠都沿重力方向一格一格滾,直到前方是牆壁或地圖外才停。放好後的第一次滾動是第 \(0\) 步;接著依序做操作字串的每一個字元:A 讓重力換成「下 → 右 → 上 → 左」循環的下一個方向,C 換回上一個,做完第 \(i\) 個操作後的滾動是第 \(i\) 步(彈珠沒動也算一步)。滾動途中進入終點那一格就算到達,輸出第一次到達是第幾步;所有操作做完都沒到達,輸出 \(-1\)(\(1 \le H, W \le 50\),操作字串長度 \(1\)~\(1000\))。

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

  • 子題組 1(\(60\) 分):操作字串只包含 A。
  • 子題組 2(\(40\) 分):無額外限制。

先拿下子題組 1(60 分):只有 A,第 \(i\) 步的方向就是 i % 4

地圖用 string 陣列一列一列讀進來,照 \(H\) 的上限開 string grid[MAX_H]。程式裡列、欄都從 \(0\) 開始編號,題目說的 \((r, c)\) 就是 grid[r - 1][c - 1]。先掃一遍找到 S,彈珠從那裡出發。

方向打成表,照題目的循環排:\(0\) 下、\(1\) 右、\(2\) 上、\(3\) 左。常見的方向表是上、右、下、左;這題照題目的順序排,換到下一個方向才剛好是編號加一。子題組 1 只有 A,重力每一步都換到下一個:第 \(0\) 步朝下、第 \(1\) 步朝右、第 \(2\) 步朝上、第 \(3\) 步朝左、第 \(4\) 步又朝下……所以第 \(i\) 步的方向就是 i % 4,操作字元一個都不用看。

每一步的滾動就是「沿一個方向一直走」:先算前方那一格,出界或是牆就停,否則踏上去。出界要先判,確定在地圖裡才能讀 grid[nr][nc]。題目要的是進入終點,不必停在上面,所以每踏上一格就檢查是不是 E——範例 2 的第 \(2\) 步就是滾過終點、停在 \((1, 4)\)。

操作有 \(n\) 個,滾動卻有 \(n + 1\) 次:放好彈珠的第 \(0\) 步也要滾、也要檢查。所以 step 從 \(0\) 跑到 \(n\);第一次踏進終點就印出 step,直接 return 0 結束——寫 break 只會跳出滾動那一層迴圈。全部跑完都沒到,印 \(-1\)。

#include <bits/stdc++.h>
using namespace std;

// 方向照題目的循環編號:0 下、1 右、2 上、3 左
const int dr[4] = {1, 0, -1, 0};      // 列的變化
const int dc[4] = {0, 1, 0, -1};      // 欄的變化

const int MAX_H = 50;
string grid[MAX_H];                   // 地圖:一列一條字串

int main() {
    int h, w;
    cin >> h >> w;
    for (int i = 0; i < h; i++) cin >> grid[i];
    string ops;
    cin >> ops;
    int n = ops.size();

    int r = 0, c = 0;                            // 彈珠的位置:從起點出發
    for (int i = 0; i < h; i++)
        for (int j = 0; j < w; j++)
            if (grid[i][j] == 'S') {
                r = i;
                c = j;
            }

    for (int step = 0; step <= n; step++) {      // 第 0 步到第 n 步,一共滾 n + 1 次
        int d = step % 4;                        // 子題組 1 只有 A:第 step 步朝第 step % 4 個方向
        while (true) {                           // 沿重力方向一格一格滾
            int nr = r + dr[d], nc = c + dc[d];  // 前方那一格
            if (nr < 0 || nr >= h || nc < 0 || nc >= w) break;   // 前方是地圖外:停
            if (grid[nr][nc] == '#') break;                      // 前方是牆:停
            r = nr;
            c = nc;
            if (grid[r][c] == 'E') {             // 每踏上一格就檢查
                cout << step << '\n';
                return 0;
            }
        }
    }
    cout << -1 << '\n';
    return 0;
}

這份程式交上去,範例 1 會 WA:第 \(3\) 個操作是 C,它照樣當成 A 往左滾,印 -1。這是正常的,逐筆給分之下子題組 1 的 \(60\) 分穩穩到手。

從 60 分到 100 分:A 是加一,C 是加三

有了 C,第 \(i\) 步的方向就不能再用 i % 4 算,改用變數 d 記住現在的方向,一開始是 \(0\)(下)。第 step 步滾之前,先做第 step 個操作,也就是 ops[step - 1](字串從 \(0\) 開始編號;第 \(0\) 步沒有操作):

  • A 換到下一個方向:d = (d + 1) % 4,左(\(3\))的下一個回到下(\(0\))。
  • C 換回上一個方向:d = (d + 3) % 4。四個方向繞成一圈,往回走一格就是往前走三格。

圖 1 把四個方向照原地圖的方位擺成一圈。迷宮順時針轉,以原地圖來看重力是往反方向轉,所以圖上 A 是逆時針繞。

圖 1:四個重力方向照題目的循環編號 0 下、1 右、2 上、3 左,照原地圖的方位擺成一圈。A 沿外圈走到下一個(d + 1),C 沿內圈走回上一個(d + 3);d = 0 時做 C,(0 + 3) % 4 = 3 是左,(0 − 1) % 4 卻是 −1,方向表沒有這一格

為什麼不寫 d = (d - 1) % 4?C++ 的 % 算出來的餘數,正負號跟被除數一樣(語法書 2.6):重力朝下時 d 是 \(0\),(0 - 1) % 4 得到 \(-1\),不是 \(3\)。方向表沒有第 \(-1\) 格,dr[-1] 會讀到陣列外面,彈珠怎麼動就說不準了。加 \(3\) 就不會碰到負數,這也是左轉寫成 (d + 3) % 4 的原因。三個範例剛好都沒有「重力朝下時做 C」,這個錯範例抓不到。

#include <bits/stdc++.h>
using namespace std;

// 方向照題目的循環編號:0 下、1 右、2 上、3 左
const int dr[4] = {1, 0, -1, 0};      // 列的變化
const int dc[4] = {0, 1, 0, -1};      // 欄的變化

const int MAX_H = 50;
string grid[MAX_H];                   // 地圖:一列一條字串

int main() {
    int h, w;
    cin >> h >> w;
    for (int i = 0; i < h; i++) cin >> grid[i];
    string ops;
    cin >> ops;
    int n = ops.size();

    int r = 0, c = 0;                            // 彈珠的位置:從起點出發
    for (int i = 0; i < h; i++)
        for (int j = 0; j < w; j++)
            if (grid[i][j] == 'S') {
                r = i;
                c = j;
            }

    int d = 0;                                   // 重力一開始朝下
    for (int step = 0; step <= n; step++) {      // 第 0 步到第 n 步,一共滾 n + 1 次
        if (step > 0) {                          // 第 step 步要先做第 step 個操作
            if (ops[step - 1] == 'A') d = (d + 1) % 4;   // A:下一個方向
            else d = (d + 3) % 4;                        // C:上一個方向(往回一格=往前三格)
        }
        while (true) {                           // 沿重力方向一格一格滾
            int nr = r + dr[d], nc = c + dc[d];  // 前方那一格
            if (nr < 0 || nr >= h || nc < 0 || nc >= w) break;   // 前方是地圖外:停
            if (grid[nr][nc] == '#') break;                      // 前方是牆:停
            r = nr;
            c = nc;
            if (grid[r][c] == 'E') {             // 每踏上一格就檢查
                cout << step << '\n';
                return 0;
            }
        }
    }
    cout << -1 << '\n';
    return 0;
}

跟子題版比,換掉的只有一件事:方向不再用 step % 4 算,改成用 d 記著、每一步先照操作轉。滾動和檢查一個字都沒改。地圖最大 \(50 \times 50\),每一步最多滾 \(49\) 格,第 \(0\) 步到第 \(1000\) 步加起來不到 \(5 \times 10^4\) 格,一下就跑完。

測過再交:範例裡的重力從來沒朝左

三個範例分別考了停在終點上(範例 1)、滾過終點(範例 2)、到不了終點(範例 3),但重力只朝過下、右、上——從來沒朝左,所以也沒有「重力朝下時做 C」、沒有「左的下一個回到下」。答案沒有 \(0\);「轉了沒動也算一步」「起點也是路」「要輸出第一次到達」這三件事寫錯了,三個範例的答案也都不會變。地圖越小越好算,自己造:

輸入 正確輸出 這一筆在測什麼
1 2 / ES / C 1 重力朝下時做 C:換成朝左,往左滾一格就進終點
2 1 / S / E / A 0 起點正下方就是終點:第 \(0\) 步就滾進去、停在上面
2 3 / .S. / E#. / AAAA 4 第 \(0\)、\(2\) 步彈珠不動,但都要算;第 \(3\) 步往左滾過起點;第 \(4\) 步轉滿一圈、重力又朝下才進終點
1 3 / S.E / AAAAA 1 第 \(1\) 步就滾進終點,第 \(5\) 步又經過一次:要輸出第一次

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

常犯錯誤
  • 只檢查停下的那一格:滾過終點不算到達,範例 2 印 -1。
  • 沒有第 0 步(放好彈珠就直接做第 1 個操作):範例 1、2 都印 -1。
  • C 寫成 d = (d - 1) % 4:範例全過,自測表第 1 列抓得到。
  • A 寫成 d = d + 1、沒有取餘:範例全過(範例的 d 最大只到 \(2\)),自測表第 3 列抓得到。
  • 起點當成牆(前方是 . 或 E 才走):範例全過,自測表第 3 列印 -1。
  • 到達終點只寫 break、最後才印答案:外層照跑,答案被後來再經過終點的步數蓋掉——範例全過,自測表第 4 列印 5。