13.3 方向表:dr[] 與 dc[]
「往上走一格」是 r 減一,「往右走一格」是 c 加一。四個方向寫四段 if 也能過,但地圖題的方向會一直用到——每一題都寫四段,既長又容易寫錯。競程的做法是把方向打成表:兩個陣列 dr/dc,第 d 個元素就是「往第 d 個方向走一步,r 和 c 各要加多少」。
// 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 - 1 在 d = 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。