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) <= t(abs() 見上冊 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 的程式比,只改了三個地方:
- 陣列多一維:
canvas[MAX_SIZE]變成canvas[MAX_SIZE][MAX_SIZE](二維陣列見上冊 6.9)。 - 距離補上列方向那一項:
abs(j - c)變成abs(i - r) + abs(j - c)。 - 輸出多包一層列的迴圈,每列印完換行。
#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 1 / 0 0 0 5 |
5 |
\(t = 0\) 的筆也要塗中心那一格;條件寫成 < 會印 0 |
3 3 1 / 1 1 1 3 |
0 3 0 / 3 3 3 / 0 3 0 |
塗出來要是菱形;印成整片 3 就是判斷寫成了正方形 |
2 2 2 / 0 0 0 4 / 0 0 0 6 |
10 0 / 0 0 |
同一格兩枝筆要累加成 \(10\);印 6 就是用 = 蓋掉了前一枝 |
2 3 1 / 1 1 20 9 |
9 9 9 / 9 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,每列印完記得換行。