AtCoder Beginner Contest 296 Ex - Unite
グリッドマンユニバースを見に行かないといけない
問題
白黒で塗られた $ N \times M $ のグリッドがある。 黒マス全体を連結にするために、追加で黒に塗るべきマスは最小でいくつ?
制約
- $ 1 \leq N \leq 100 $
- $ 1 \leq M \leq 7 $ ← 小さい!
前提
とくになし
解法
「連結性 DP」「面倒 DP」などと呼ばれているやつ。
$ \mathrm{dp}(i, S) := ( $$ i $ 行目まで決めて、連結性が $ S $ であるときに塗るべきマスの個数の最小値$)$
のようにして DP する。遷移を計算する際には $ i $ 行目の白黒のパターン $ 2 ^ M $ 通りを全探索する。
連結性について詳しく説明する。
例えば、上の図において左の二つの塗り方は同じ状態に圧縮できる。一番下の行はどちらも 黒白黒黒白黒白
であり、$ 1 $ 列目と $ 6 $ 列目、$ 3 $ 列目と $ 4 $ 列目の黒マスがそれぞれ連結になっている。よって、白マスには $ 0 $、黒マスには左から順に連結成分番号を振っていくことにすると、二つの塗り方はいずれも $ (1, 0, 2, 2, 0, 1, 0) $ という数列で表現できる。
実装が大変そうだが、私はいつも $ i - 1 $ 行目と $ i $ 行目を合わせた $ 2M $ 頂点のグラフに対する Union-Find 木を用意し、
- $ i - 1 $ 行目において、連結成分番号が同じ黒マスをマージ
- $ i $ 行目の各黒マスと隣接する黒マスをマージ
- 長さ $ M $ の配列 $ a $ を用意し、$ a _ j $ には $ (i, j) $ が白なら $ 0 $、黒なら $ (i, j) $ が属する集合の代表元 $ + 1 $ を入れる
- map などを持ちながら $ a $ を順番に舐めて、連結成分番号を $ 1 $ から振り直す
のようにしている。
また、今回は $ i , S $ に加えて、これ以上黒マスが登場しないかを状態として持つ(耳 DP)。
状態を $8$ 進数で表現して int 型に詰め込むと高速化できる。$ 2 ^ k $ 進数にすると、値をビット演算で取り出せるので都合が良い。
コード
遷移を計算する部分で $ N $ と $ M $ を取り違えないように、$ N $ は main 内、$ M $ はグローバルで宣言している。
https://atcoder.jp/contests/abc296/submissions/40333675
int m; int next_phase(vector<int> &v, int phase, int S) { if (phase == 0) { return (S == 0 ? 0 : 1); } else if (phase == 1) { return (S == 0 ? 2 : 1); } else { return (S == 0 ? 2 : -1); } } optional<pair<vector<int>, int>> next_state(pair<vector<int>, int> state, int S) { auto [v, phase] = state; auto np = next_phase(v, phase, S); if (np == -1) return nullopt; if (S == 0) { if (*max_element(v.begin(), v.end()) >= 2) { return nullopt; } else { vector<int> res(m, 0); return make_pair(res, np); } } atcoder::dsu uf(m * 2); rep(i, m) For(j, i + 1, m) { if (v[i] != 0 && v[i] == v[j]) uf.merge(i, j); } rep(i, m) { if (!(S >> i & 1)) continue; if (i + 1 < m && (S >> (i + 1) & 1)) { uf.merge(i + m, i + 1 + m); } if (v[i] != 0) { uf.merge(i, i + m); } } rep(i, m) { if (v[i] == 0) continue; bool flag = false; rep(j, m) { if (uf.same(i, j + m)) { flag = true; break; } } if (!flag) return nullopt; } vector<int> nv(m, 0); rep(i, m) { if (!(S >> i & 1)) continue; nv[i] = uf.leader(i + m) + 1; } map<int, int> mp; int cur = 1; rep(i, m) { if (nv[i] == 0) continue; if (mp.find(nv[i]) == mp.end()) { mp[nv[i]] = cur; ++cur; } nv[i] = mp[nv[i]]; } return make_pair(nv, np); } int main() { int n; scanf("%d%d", &n, &m); int mask[n]; int offset = 0; rep(i, n) { string s; cin >> s; mask[i] = 0; rep(j, m) if (s[j] == '#') mask[i] += (1 << j), ++offset; } map<pair<vector<int>, int>, int> dp; // (state, phase) -> num { vector<int> init(m, 0); dp[{init, 0}] = 0; } rep(i, n) { map<pair<vector<int>, int>, int> nxt; rep(S, 1 << m) { if ((S & mask[i]) != mask[i]) continue; for (auto [state, val] : dp) { auto ns = next_state(state, S); if (!ns) continue; int tmp = val + __builtin_popcount(S); if (nxt.find(ns.value()) == nxt.end()) { nxt[ns.value()] = tmp; } else { chmin(nxt[ns.value()], tmp); } } } dp.swap(nxt); } int ans = n * m; for (auto [state, val] : dp) { auto [v, phase] = state; if (*max_element(v.begin(), v.end()) <= 1) { chmin(ans, val - offset); } } printf("%d\n", ans); }