--- tags: Programming Contest --- # AtCoder Beginner Contest 176 題解 [題目連結](https://atcoder.jp/contests/abc176/tasks) ## 心得 很可惜沒在時間內解出第五題,想到正確方法,但來不及寫出來,還好最終有加到 rating。 ## A, B, C 都是簽到題,手速拼起來。 ## D. Wizard in Maze 一開始就想到可以轉換成最短路徑:每個點與 moveA 的點建成本為 0 的邊,與 moveB 建成本為 1 的邊,求起點與終點的最短路徑長度。 最一開始想到 scipy 中有 sparse matrix 與 dijkstra 可以直接拿來用,所以就用 python 寫了一個版本。但範測一直過不了,最後只好轉用 c++ 寫。 賽後研究才發現是 scipy 的 sparse matrix (csr_matrix) 在處理 duplicated entries 跟我想得不一樣,他是累加在一起。修掉這問題後,大部份測資會 AC,但有兩筆 TLE。看了別人的 submission,發現大部份人想到的方法是修改 BFS,若是 moveA,將相鄰點加進 queue 的開頭,若是 moveB,則是加進 queue 的尾端。這樣在走的時候,就會優先走 moveA 的點。很巧妙的方法,完全沒往這方向想,看了討論區,這方法還是個有名稱的方法,叫 [0-1 BFS](https://cp-algorithms.com/graph/01_bfs.html),完全沒聽說過啊。 c++17 可以用`[...] = data` 將 `tuple` 或 `pair` 解構,寫起來真的舒服。 以下給出使用 dijkstra 的 AC Code: ```cpp #include <iostream> #include <vector> #include <algorithm> #include <queue> #include <tuple> #include <functional> using namespace std; typedef pair<int, int> pii; const int inf = 0x3f3f3f3f; int solve() { int H, W; int Cr, Cc; int Dr, Dc; cin >> H >> W; cin >> Cr >> Cc; Cr--; Cc--; cin >> Dr >> Dc; Dr--; Dc--; vector<string> S(H); for (int r = 0; r < H; r++) { cin >> S[r]; } if (S[Cr][Cc] == '#' || S[Dr][Dc] == '#') { return -1; } auto deltas = vector<tuple<int, int, int>>(); deltas.push_back(tuple(0, +1, 0)); deltas.push_back(tuple(0, -1, 0)); deltas.push_back(tuple(+1, 0, 0)); deltas.push_back(tuple(-1, 0, 0)); for (int dr = -2; dr <= 2; dr++) { for (int dc = -2; dc <= 2; dc++) { if (dr == 0 and dc == 0) continue; deltas.push_back(tuple(dr, dc, 1)); } } auto dis = vector<int>(H * W, inf); auto que = priority_queue<pii, vector<pii>, greater<pii>>(); dis[Cr * W + Cc] = 0; que.push(pii(0, Cr * W + Cc)); while (!que.empty()) { auto [d, u] = que.top(); que.pop(); if (dis[u] < d) continue; for (auto&& [dr, dc, w] : deltas) { auto nr = u / W + dr; auto nc = u % W + dc; if (nr < 0 || nr >= H) continue; if (nc < 0 || nc >= W) continue; if (S[nr][nc] == '#') continue; if (dis[nr * W + nc] > dis[u] + w) { dis[nr * W + nc] = dis[u] + w; que.push(pii(dis[nr * W + nc], nr * W + nc)); } } } if (dis[Dr * W + Dc] == inf) { return -1; } return dis[Dr * W + Dc]; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cout << solve() << endl; return 0; } ``` ## E. Bomber 一開始看到題目,覺得是二分匹配,後來才發現只能放 1 個炸彈。 最一開始當然是覺得每個位置都放放看,將炸彈放在位置 (r, c) 時,透過預計算每個 row、每個 col 各有多少個目標,我們可以 $O(1)$ 的算出可以炸掉多少個目標,即 `cntR[r] + cntC[c] - (1 if S[r][c] is target else 0)`,答案為最大值。 但這題的難點是 H, W 都很大,光是枚舉所有的位置所需的 $O(H * W)$ 就會 TLE。於是我就想要如何加速,上述方法可以看成枚舉每個 row,在 row `r` 時,我們想找出此 row 的 `cntC[c] - (1 if S[r][c] is target else 0)`,這會是一個長度為 W 的陣列,我們求出其中的最大值,再加上 `cntR[r]`,就是這個 row 的最佳解。 例如這筆測資(x 代表有 target): ``` xx. ... x.x ``` 各個 row 的 `cntC[c] - (1 if S[r][c] is target else 0)` 與得到的答案如下: | | col 1 | col 2 | col 3 | max | cntR[r] + max | | ----- | ----------- | ----------- | ----------- | --- | ------------- | | row 1 | (2 - 1) = 1 | (1 - 1) = 0 | (1 - 0) = 1 | 1 | 2 + 1 = 3 | | row 2 | (2 - 0) = 0 | (1 - 0) = 1 | (1 - 0) = 1 | 2 | 0 + 2 = 2 | | row 3 | (2 - 1) = 1 | (1 - 0) = 1 | (1 - 1) = 0 | 1 | 2 + 1 = 3 | 接下來問題就是如何高速的求出 `cntC[c] - (1 if S[r][c] is target else 0)` 了,在發現題目說 targets 的數量不超過 3 * 10^5 後,我就想到了可以用線段樹: 線段樹一開始存的是 `cntC`。又因為當我要進入 row `r` 時,因為我知道 row `r` 所有 target 的位置,我可以將線段樹中這些位置都**減去 1**,此時線段樹中存的就是 `cntC[c] - (1 if S[r][c] is target else 0)`。而我們想知道的最大值也可以用透過線段樹的 query 得出。當我們離開 row `r` 時,記得將那些 -1 的位置**加回來**,讓線段樹變成存 `cntC`。 處理 target 的部份 amortize 下去只有 $O(M)$,整體時間 $O(H + W + M + H log(W))$ = $O(H log(W))$,可以在時間內解出。看了別人的 submission,發現用線段樹的人不多呢,晚點再來研究他們是怎麼解的。 以下為 c++ 的 AC Code: ```cpp #include <iostream> #include <vector> #include <algorithm> using namespace std; // 0-based index // tree size: 2 * NN - 1 // leaves indices: [NN - 1, 2 * NN - 1) template<class T> struct SegTree { int NN; T default_value; vector<T> data; T aggr(T a, T b) { return max(a, b); } SegTree(int N, T val) { default_value = val; NN = 1; while (NN < N) NN <<= 1; data = vector<T>(2 * NN - 1, val); } SegTree(const vector<T> v, T val) { default_value = val; int N = v.size(); NN = 1; while (NN < N) NN <<= 1; data = vector<T>(2 * NN - 1, val); copy(v.begin(), v.end(), data.begin() + (NN - 1)); build(0, 0, NN); } void build(int u, int l, int r) { if (r - l == 1) return; int m = (l + r) / 2; build(u * 2 + 1, l, m); build(u * 2 + 2, m, r); data[u] = aggr(data[2 * u + 1], data[2 * u + 2]); } T query(int a, int b, int u, int l, int r) { if (l >= b || r <= a) return default_value; if (l >= a && r <= b) return data[u]; int m = (l + r) / 2; T res1 = query(a, b, u * 2 + 1, l, m); T res2 = query(a, b, u * 2 + 2, m, r); return aggr(res1, res2); } void update(int idx, T x, int u, int l, int r) { if (idx < l || idx >= r) return; if (idx == l && r - l == 1) { data[u] = x; return; } int m = (l + r) / 2; update(idx, x, u * 2 + 1, l, m); update(idx, x, u * 2 + 2, m, r); data[u] = aggr(data[2 * u + 1], data[2 * u + 2]); } void increment(int idx, T x, int u, int l, int r) { update(idx, data[NN - 1 + idx] + x, u, l, r); } }; template <class T> ostream & operator << (ostream &out, const SegTree<T> &seg) { out << "SegTree(NN=" << seg.NN << ", "; for (int u = seg.NN - 1, sz = seg.NN; sz >= 1; u /= 2, sz /= 2) { out << "[ "; for (int i = 0; i < sz; i++) { out << seg.data[u + i] << " "; } out << "]"; } out << ")"; return out; } int solve() { int H, W, M; cin >> H >> W >> M; auto cntR = vector<int>(H, 0); auto cntC = vector<int>(W, 0); auto t = vector<vector<int>>(H, vector<int>()); for (int i = 0; i < M; i++) { int r, c; cin >> r >> c; r--; c--; cntR[r]++; cntC[c]++; t[r].push_back(c); } auto seg = SegTree<int>(cntC, 0); // cout << seg << endl; int ans = -1; for (int r = 0; r < H; r++) { for (auto c : t[r]) { seg.increment(c, -1, 0, 0, seg.NN); } // cout << seg << endl; int val = cntR[r] + seg.query(0, W, 0, 0, seg.NN); ans = max(ans, val); for (auto c : t[r]) { seg.increment(c, +1, 0, 0, seg.NN); } } return ans; } int main () { ios::sync_with_stdio(false); cin.tie(nullptr); cout << solve() << endl; return 0; } ``` :::success [< 回到所有題解](https://hackmd.io/@amoshyc/HkWWepaPv/) :::