語法書 / AA 競程語法書 下冊 / 第十三單元 / 沿一個方向一直走:射線與標記表

13.5 沿一個方向一直走:射線與標記表

圖 13-3:從一格出發——四條射線、四鄰格、八鄰格

第二種常見的動作是沿著固定方向一直走,直到出界或撞到東西——像一道射線。寫法就是把上一節的「算下一格、檢查、踏上去」放進 while

int r = sr, c = sc, steps = 0;           // 從起點出發
while (true) {
    int nr = r + dr[d], nc = c + dc[d];
    if (!inside(nr, nc) || grid[nr][nc] == '#') break;   // 出界或撞牆:停
    r = nr;  c = nc;                                       // 踏上去
    steps++;
}

方向不一定是上下左右——一個固定向量 (s, t) 也行,只是每一步跳得比較遠;那就是魔王迷宮。

例題:魔王迷宮(APCS 2021 年 9 月中級)

題目n \times m 的棋盤(1 \le n, m \le 100)上有 k 隻魔王(1 \le k \le 500),第 i 隻起點 (r_i, c_i)、固定移動向量 (s_i, t_i)-100 \le s_i, t_i \le 100)。每回合依序:① 所有魔王同時在所在格放一顆炸彈 ② 同時各自移動一次 ③ 移出棋盤的魔王消失 ④ 其餘魔王中,移到已有炸彈格子的會引爆該格——該格所有魔王與炸彈同時消失。持續到沒有魔王為止,輸出還放有至少一顆炸彈的格子數。

每隻魔王就是一條射線:出界就結束。多出來的東西有兩樣——一張和棋盤一樣大的炸彈表 bomb[r][c],記錄這格有沒有炸彈;以及「同時判定」:兩隻魔王同一回合踩進同一個有炸彈的格子,兩隻都要消失,所以判定的時候先不清炸彈,全部判完再一起清。

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

int main() {
    int n, m, k;
    cin >> n >> m >> k;
    vector<int> r(k), c(k), s(k), t(k);   // 第 i 隻魔王的位置與移動向量
    vector<bool> alive(k, true);
    for (int i = 0; i < k; i++) cin >> r[i] >> c[i] >> s[i] >> t[i];

    vector<vector<bool>> bomb(n, vector<bool>(m, false));       // 這格有沒有炸彈
    vector<vector<bool>> exploded(n, vector<bool>(m, false));   // 這一回合被引爆的格子
    int alive_count = k;

    while (alive_count > 0) {
        // 步驟 1、2:所有活著的魔王放炸彈,然後同時移動一步
        for (int i = 0; i < k; i++) {
            if (!alive[i]) continue;
            bomb[r[i]][c[i]] = true;
            r[i] += s[i];
            c[i] += t[i];
        }

        // 步驟 3、4:出界的消失;踩到炸彈的消失,並記下要清掉的格子
        vector<pair<int, int>> cells_to_clear;
        for (int i = 0; i < k; i++) {
            if (!alive[i]) continue;
            if (r[i] < 0 || r[i] >= n || c[i] < 0 || c[i] >= m) {
                alive[i] = false;
                alive_count--;
            } else if (bomb[r[i]][c[i]]) {
                alive[i] = false;
                alive_count--;
                if (!exploded[r[i]][c[i]]) {
                    exploded[r[i]][c[i]] = true;
                    cells_to_clear.push_back({r[i], c[i]});
                }
            }
        }

        // 全部判定完才清炸彈:兩隻魔王同回合踩同一格,兩隻都要消失
        for (int i = 0; i < (int)cells_to_clear.size(); i++) {
            int x = cells_to_clear[i].first;
            int y = cells_to_clear[i].second;
            bomb[x][y] = false;
            exploded[x][y] = false;
        }
    }

    int answer = 0;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            if (bomb[i][j]) answer++;
    cout << answer << '\n';
    return 0;
}

k 隻魔王的資料用四個 vector 平行存放,r[i]c[i]s[i]t[i] 是同一隻的四個欄位;下一單元學了結構體之後,這種「一筆資料好幾個欄位」會有更順手的寫法。至於會不會跑不完:向量是 (0, 0) 的魔王第一回合就踩到自己剛放的炸彈;其餘每回合至少移動一格,100 \times 100 的棋盤最多 100 回合就出界。

掃到第一個物體、反覆做到不能做

射線的另一個用途是「往一個方向看,最先撞到的是誰」——卡牌遊戲的「同一列或同一欄、且中間沒有其他牌」就是這件事:從一張牌往另一張的方向掃,中間每一格都得是空的。

卡牌遊戲(APCS 2023 年 10 月中級):n \times m 的表格(1 \le n \le 201 \le m \le 40)每格一張卡牌,每個數值恰好出現兩次。若同數值的兩張牌在同一列或同一欄、且它們之間沒有尚未移除的牌,就可以移除這兩張並獲得該數值的分數;移除後的格子變成空格。可以重複操作、順序自選,求最多能得多少分。

移除只會讓別的牌更容易連通、不會更難,所以只要「有牌可拿就拿」,最後拿到的一定最多。程式就是反覆掃一整輪,直到某一輪什麼都沒拿到為止

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

int main() {
    int n, m;
    cin >> n >> m;
    vector<vector<int>> card(n, vector<int>(m));
    vector<vector<pair<int, int>>> pos(1001);       // pos[x]:數值 x 的兩張牌各在哪
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++) {
            cin >> card[i][j];
            pos[card[i][j]].push_back({i, j});
        }

    vector<vector<bool>> removed(n, vector<bool>(m, false));   // 這格的牌拿掉了沒
    long long score = 0;
    bool changed = true;
    while (changed) {                                // 反覆做,直到一整輪都沒有牌可拿
        changed = false;
        for (int x = 0; x <= 1000; x++) {
            if (pos[x].empty()) continue;
            auto [r1, c1] = pos[x][0];
            auto [r2, c2] = pos[x][1];
            if (removed[r1][c1]) continue;           // 這一對已經拿掉了
            bool clear = false;
            if (r1 == r2) {                          // 同一列:中間的格子都要是空的
                clear = true;
                for (int c = min(c1, c2) + 1; c < max(c1, c2); c++)
                    if (!removed[r1][c]) clear = false;
            } else if (c1 == c2) {                   // 同一欄
                clear = true;
                for (int r = min(r1, r2) + 1; r < max(r1, r2); r++)
                    if (!removed[r][c1]) clear = false;
            }
            if (clear) {
                removed[r1][c1] = removed[r2][c2] = true;
                score += x;
                changed = true;
            }
        }
    }
    cout << score << '\n';
    return 0;
}

pos[x] 是「數值 x 的兩張牌各在哪」——用數值當索引開一個 vector 的陣列(10.3),比每次在表格裡找快得多;auto [r1, c1] = pos[x][0];12.5 的結構化綁定。「反覆掃到沒有變化」的 while (changed) 也是實作題的固定句型,動線安排那種「加東西、拆東西」的題目會再遇到它。