Editorial for 電子畫布 (APCS 2024-06 中級)


簡潔題意

一張 \(H\) 列、\(W\) 欄的畫布,每一格一開始都是 \(0\),列與欄都從 \(0\) 開始編號。接下來進行 \(N\) 次畫筆操作,每次給四個整數 \(r\)、\(c\)、\(t\)、\(x\):所有與 \((r, c)\) 曼哈頓距離不超過 \(t\) 的格子 \((i, j)\)——也就是滿足 \(|i - r| + |j - c| \le t\) 的格子——都要加上 \(x\)。同一格被多枝筆畫到,顏色值就一路累加。輸出完成全部操作後的整張畫布(\(1 \le H, W \le 20\)、\(1 \le N \le 100\)、\(0 \le t \le 20\)、\(1 \le x \le 10\))。

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

  • 子題組 1(\(60\) 分):\(H = 1\)。
  • 子題組 2(\(40\) 分):無額外限制。

先拿下子題組 1(60 分):畫布只有一列

\(H = 1\) 時整張畫布就是一行。這時 \(r\) 只能是 \(0\)(題目保證 \(0 \le r < H\)),格子的 \(i\) 也只能是 \(0\),所以曼哈頓距離裡的 \(|i - r|\) 永遠是 \(0\),條件只剩下

\[|j - c| \le t.\]

做法就是照字面模擬:開一個一維陣列當畫布,每讀進一枝筆,就掃過每一欄,距離不超過 \(t\) 的就加上 \(x\)。

#include <iostream>
#include <cstdlib>   // abs() 住在這(上冊 7.6)
using namespace std;

const int MAX_SIZE = 20;

int main() {
    int H, W, N;
    cin >> H >> W >> N;   // 子題組 1 保證 H = 1

    int canvas[MAX_SIZE] = {};   // 只有一列,一維陣列就夠了

    while (N--) {   // 重複 N 次,處理每一次畫筆操作
        int r, c, t, x;
        cin >> r >> c >> t >> x;   // r 一定是 0,但還是要讀掉,不然後面的數字會錯位
        for (int j = 0; j < W; j++) {
            if (abs(j - c) <= t) {   // 距離只剩欄方向這一項
                canvas[j] += x;      // 顏色值累加
            }
        }
    }

    for (int j = 0; j < W; j++) {
        cout << canvas[j];
        if (j + 1 < W) {
            cout << ' ';     // 數字之間隔一個空白
        } else {
            cout << endl;    // 最後換行
        }
    }
    return 0;
}

兩個地方別踩:

  • \(r\) 在子題組 1 裡永遠是 \(0\),但每行還是有四個數字——就算用不到也要把它讀掉;少讀一個,後面所有數字都會錯位(常犯錯誤有實例)。
  • 距離條件是「不超過」,要寫 <= 不是 <

這份程式交上去,範例 2 和子題組 2 會 WA——這是正常的,逐筆給分之下子題組 1 的 \(60\) 分穩穩到手。

從 60 分到 100 分:只差三個地方

完整版的畫布是二維的,但整個想法跟子題組 1 一模一樣:每一枝筆,掃過整張畫布,距離夠近的格子就加。會不會太慢?最多 \(N \times H \times W = 100 \times 20 \times 20 = 40000\) 次判斷,離危險區還遠得很(估算方法見上冊 4.9)。

距離條件照題目字面翻譯就好:\(|i - r| + |j - c| \le t\) 寫成 abs(i - r) + abs(j - c) <= tabs() 見上冊 7.6)。要小心它塗出來的形狀:兩個方向的距離是相加之後才拿去比,所以塗到的範圍是「菱形」。以 \(t = 1\)、筆停在 \(3 \times 3\) 畫布正中央、\(x = 3\) 為例,塗到的是中心與上下左右四格:

0 3 0
3 3 3
0 3 0

如果把條件拆成兩個分開判斷(\(|i - r| \le t\) 且 \(|j - c| \le t\)),塗出來的會是正方形——上面這張圖會整片變成 3。這是本題最經典的錯法,詳見常犯錯誤第 1 條。

跟子題組 1 的程式比,只改了三個地方:

  1. 陣列多一維:canvas[MAX_SIZE] 變成 canvas[MAX_SIZE][MAX_SIZE](二維陣列見上冊 6.9)。
  2. 距離補上列方向那一項:abs(j - c) 變成 abs(i - r) + abs(j - c)
  3. 輸出多包一層列的迴圈,每列印完換行。
#include <iostream>
#include <cstdlib>   // abs() 住在這(上冊 7.6)
using namespace std;

const int MAX_SIZE = 20;

int main() {
    int H, W, N;
    cin >> H >> W >> N;

    int canvas[MAX_SIZE][MAX_SIZE] = {};   // 整張畫布先清成 0

    while (N--) {   // 重複 N 次,處理每一次畫筆操作
        int r, c, t, x;
        cin >> r >> c >> t >> x;
        for (int i = 0; i < H; i++) {
            for (int j = 0; j < W; j++) {
                if (abs(i - r) + abs(j - c) <= t) {   // 曼哈頓距離不超過 t 的格子
                    canvas[i][j] += x;                // 顏色值累加
                }
            }
        }
    }

    for (int i = 0; i < H; i++) {
        for (int j = 0; j < W; j++) {
            cout << canvas[i][j];
            if (j + 1 < W) {
                cout << ' ';     // 同一列的數字之間隔一個空白
            } else {
                cout << endl;    // 一列印完才換行
            }
        }
    }
    return 0;
}

幾個寫法上的細節:

  • int canvas[MAX_SIZE][MAX_SIZE] = {};= {} 把整張畫布清成 \(0\)(上冊 6.5)——題目說每一格起初都是 \(0\),就是這一行在做的事。
  • while (N--) 是「重複 \(N\) 次」的慣用寫法(上冊 4.7);沒把握的話寫 for (int k = 0; k < N; k++) 效果完全一樣。
  • 輸出用 if (j + 1 < W) 判斷「這一列後面還有沒有數字」:還有就印空白、沒有就換行,行尾不會多出空白。

測過再交:範例沒考到的三種筆

兩個範例合起來,有幾種情況完全沒出現過:停 \(0\) 秒的筆(\(t = 0\),只塗中心一格)、大到整張蓋滿的筆,以及同一格被重複畫到時累加有沒有真的做。自己造小測資補上:

輸入 正確輸出 這一筆在測什麼
1 1 10 0 0 5 5 \(t = 0\) 的筆也要塗中心那一格;條件寫成 < 會印 0
3 3 11 1 1 3 0 3 03 3 30 3 0 塗出來要是菱形;印成整片 3 就是判斷寫成了正方形
2 2 20 0 0 40 0 0 6 10 00 0 同一格兩枝筆要累加成 \(10\);印 6 就是用 = 蓋掉了前一枝
2 3 11 1 20 9 9 9 99 9 9 \(t\) 大到整張蓋滿,每一格都要中

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

常犯錯誤
  • 把距離條件拆成兩個分開判斷:寫成 abs(i - r) <= t && abs(j - c) <= t,塗出來是正方形不是菱形。這個錯特別陰險——\(H = 1\) 時一列上的正方形跟菱形是同一回事,所以範例 1 全對、子題組 1 的 \(60\) 分照拿,到範例 2 才現形(第一列正確是 0 0 5 5 5 0 2,它印 0 5 5 5 5 7 2)。自測表第 2 列就是在堵它。
  • 距離寫成 < t 漏了等號:菱形最外圈整圈沒塗到,範例 1 就現形(正確開頭是 0 6 10,它印 0 0 6);\(t = 0\) 時更慘,整枝筆等於沒畫(自測表第 1 列)。
  • canvas[i][j] = x; 忘了累加:每一格只剩下最後畫到它的那枝筆,範例 1 會印出 0 接一整排 6,該是 \(17\) 的地方全變 6
  • 覺得 \(r\) 用不到就不讀:子題組 1 裡 \(r\) 全是 \(0\),看起來可以跳過——但輸入的數字是排隊進來的,一行四個少讀一個,後面全部錯位,範例 1 會印出 5 5 5 5 開頭的一排錯誤數字。
  • \(r\)、\(c\) 拿反:\(r\) 是列(第幾橫排)、\(c\) 是欄(第幾直行),跟 canvas[i][j] 的 \(i\)、\(j\) 同一個順序。拿反的話範例 1 直接印出 \(20\) 個 0——第一枝筆停在 \((0, 13)\),拿反後 \(|i - c|\) 一開場就是 \(13\),比 \(t = 5\) 大,一格都塗不到。
  • 整張畫布印成一行:\(H = 1\) 時本來就只有一行,所以子題組 1 照樣全對,範例 2 才現形。本站評測對行內多的空白寬容,但行的結構要對——範例 2 的 \(42\) 個數字擠在同一行就是 WA,每列印完記得換行。