語法書 / AA 競程語法書 下冊 / 第十三單元 / 方向表:dr[] 與 dc[]

13.3 方向表:dr[] 與 dc[]

圖 13-2:方向表——順時針排,右轉就是編號加一

「往上走一格」是 r 減一,「往右走一格」是 c 加一。四個方向寫四段 if 也能過,但地圖題的方向會一直用到——每一題都寫四段,既長又容易寫錯。競程的做法是把方向打成表:兩個陣列 drdc,第 d 個元素就是「往第 d 個方向走一步,rc 各要加多少」。

// 0 上、1 右、2 下、3 左(順時針)
const int dr[4] = {-1, 0, 1, 0};
const int dc[4] = { 0, 1, 0, -1};

// 從 (r, c) 往方向 d 走一步
int nr = r + dr[d];
int nc = c + dc[d];

方向從此變成一個 0 \sim 3 的整數 d,「往方向 d 走一步」是固定的兩行,四個方向全部通用。

為什麼要順時針排

表裡的順序是刻意的:上、右、下、左順時針排,那麼「右轉」剛好就是編號加一,轉到 3 再加一要回到 0,所以取餘:

動作 寫法 為什麼
右轉 90^\circ d = (d + 1) % 4; 順時針的下一個
左轉 90^\circ d = (d + 3) % 4; 左轉一次=右轉三次;寫 d - 1d = 0 時會變成 -1
迴轉 180^\circ d = (d + 2) % 4; 轉兩格

如果把表排成「上、下、左、右」,轉向就得寫四段 if——表怎麼排,決定了後面好不好寫。

// 八方位:前四個是上右下左,後四個是左上、右上、左下、右下
const int dr8[8] = {-1, 0, 1, 0, -1, -1, 1, 1};
const int dc8[8] = { 0, 1, 0, -1, -1, 1, -1, 1};

例題:蜜蜂觀察(APCS 2024 年 1 月中級)

題目m \times n 的蜂巢(1 \le m, n \le 20),每格一個英文字母。蜜蜂從左下角出發,依序執行 k 步(1 \le k \le 100),每步是六個方向之一:0 右上(上一列、同一欄)、1 右、2 右下(下一列、下一欄)、3 左下(下一列、同一欄)、4 左、5 左上(上一列、上一欄)。會離開蜂巢的那一步就留在原地,但仍算執行完成。輸出每一步之後所在格子的字母串起來的字串,以及其中不同字母的種類數。

規則只有一條(出界留原地),難的地方全在「六個方向的座標變化」。把題目給的六條定義照抄成表,之後每一步就是查表、判邊界、更新位置:

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

// 方向 0~5 依題目定義:右上、右、右下、左下、左、左上(方向表放檔頭,資料與邏輯分開)
const int dr[6] = {-1, 0, 1, 1, 0, -1};
const int dc[6] = {0, 1, 1, 0, -1, -1};

int main() {
    int m, n, k;
    cin >> m >> n >> k;

    vector<string> hive(m);
    for (int i = 0; i < m; i++) cin >> hive[i];

    int r = m - 1;   // 一開始在左下角
    int c = 0;
    string path;
    bool seen[128] = {};   // 記錄哪些字母出現過(用字元當索引,也是打表)
    int distinct = 0;

    for (int i = 0; i < k; i++) {
        int d;
        cin >> d;
        int nr = r + dr[d];
        int nc = c + dc[d];
        if (0 <= nr && nr < m && 0 <= nc && nc < n) {   // 會出界就留在原地
            r = nr;
            c = nc;
        }
        char ch = hive[r][c];
        path.push_back(ch);
        if (!seen[(int)ch]) {
            seen[(int)ch] = true;
            distinct++;
        }
    }

    cout << path << '\n';
    cout << distinct << '\n';
    return 0;
}

這一題的方向編號是題目給的,順序也是題目給的——照題目定義排表就好;只有在自己選編號的時候(下一節),才要記得順時針排。

由前後方向判斷轉向

方向變成整數以後,「這一次是左轉還是右轉」也能用算的:新方向減舊方向,加 4 再取餘(避免負數),差 1 是右轉、差 3 是左轉、差 2 是迴轉。

路徑偵測(APCS 2023 年 6 月初級)就是這一招:從原點出發依序經過 n 個點(1 \le n \le 100),相鄰兩點只在水平或垂直方向移動、第一個點在 x 軸正向(初始面向東),輸出左轉、右轉、迴轉各幾次。座標是數學課的 x 往東、y 往北,先寫一個「從甲點到乙點是朝哪個方向」的函式,再算前後兩段方向的差:

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

// 方向編號順時針排:0 上(北)、1 右(東)、2 下(南)、3 左(西)
int direction(int x1, int y1, int x2, int y2) {   // 從 (x1,y1) 走到 (x2,y2) 是朝哪個方向
    if (y2 > y1) return 0;   // 往北
    if (x2 > x1) return 1;   // 往東
    if (y2 < y1) return 2;   // 往南
    return 3;                // 往西
}

int main() {
    int n;
    cin >> n;
    int px = 0, py = 0;              // 目前所在的點,一開始在原點
    int prevDir = 1;                 // 一開始面向東
    int left = 0, right = 0, uturn = 0;
    for (int i = 0; i < n; i++) {
        int x, y;
        cin >> x >> y;
        int dir = direction(px, py, x, y);
        int diff = (dir - prevDir + 4) % 4;   // 新方向減舊方向,加 4 再取餘避免負數
        if (diff == 1) right++;               // 順時針轉一格=右轉
        else if (diff == 3) left++;           // 逆時針轉一格=左轉
        else if (diff == 2) uturn++;          // 轉了兩格=迴轉
        px = x;  py = y;  prevDir = dir;
    }
    cout << left << ' ' << right << ' ' << uturn << '\n';
    return 0;
}

(dir - prevDir + 4) % 4 那個 + 4 和左轉寫 + 3 是同一件事:C++ 的 % 對負數會得到負的餘數(上冊 2.6),先加一圈再取餘就永遠是 0 \sim 3