問題
$H \times W$ のグリッドが与えられる。各マスは .(空きマス)または #(壁・使用不可)のいずれかである。
空きマスをすべて $1\times 2$ または $2\times 1$ のドミノで、重複・はみ出しなく敷き詰める方法の総数を $998244353$ で割った余りを求めよ。壁のマスは覆う必要がない(盤面から除外されているとみなす)。
入力形式
H W
row_1
row_2
:
row_H
制約
$1 \le H, W \le 12$
各行は
. と # のみからなる長さ $W$ の文字列入出力例
入力例1
2 2
..
..
出力例1
2
2×2 の空きマス全体を、横ドミノ2枚で敷くパターンと、縦ドミノ2枚で敷くパターンの2通り。
入力例2
2 3
..#
#..
出力例2
1
壁を避けると空きマスは4マス。(0,0)-(0,1) と (1,1)-(1,2) を横ドミノにする1通りのみが有効。
概念図
ヒント(段階的開示)
ヒント1: 方向性
すべての敷き詰め方を単純に列挙すると指数的に爆発する。マスを左上から1マスずつ順番に処理していくとき、「まだ確定していないが、直前に処理した行から縦ドミノで下に伸びてくることが決まっているマス」の集合だけを覚えておけば十分ではないか、と考えよ。
ヒント2: アプローチ
輪郭線DP(broken profile DP): 行ごとに処理する。
dp[mask] を「これまでの行を全て正しく埋め終えていて、現在処理しようとしている行のうち、上の行から縦ドミノで強制的に埋まっている列の集合が mask である場合の敷き詰め方の数」と定義する。1行分の遷移は、その行の列を左から順に走査する再帰関数で全パターンを列挙し、次の行に引き継ぐビットマスクを作る。ヒント3: 誘導(コード骨格)
def make_transitions(row, mask_in, W, has_next_row):
results = []
def rec(j, mask_out):
if j == W:
results.append(mask_out)
return
if row[j] == '#':
if (mask_in >> j) & 1:
return # 壁に縦ドミノが伸びてきたら矛盾
rec(j + 1, mask_out)
return
if (mask_in >> j) & 1:
rec(j + 1, mask_out)
return
if has_next_row:
rec(j + 1, mask_out | (1 << j))
if j + 1 < W and row[j + 1] != '#' and not ((mask_in >> (j + 1)) & 1):
rec(j + 2, mask_out)
rec(0, 0)
return results
最後の行を処理し終えたあと、mask == 0(下にはみ出すドミノがない)のときの dp 値だけが正しい完全被覆の数になる。
模範解答 (Python)
import sys
def solve():
data = sys.stdin.read().split('\n')
H, W = map(int, data[0].split())
grid = [data[i + 1] for i in range(H)]
MOD = 998244353
dp = {0: 1}
for i in range(H):
row = grid[i]
has_next_row = (i + 1 < H)
ndp = {}
for mask_in, ways in dp.items():
def rec(j, mask_out):
if j == W:
ndp[mask_out] = (ndp.get(mask_out, 0) + ways) % MOD
return
if row[j] == '#':
if (mask_in >> j) & 1:
return
rec(j + 1, mask_out)
return
if (mask_in >> j) & 1:
rec(j + 1, mask_out)
return
if has_next_row:
rec(j + 1, mask_out | (1 << j))
if j + 1 < W and row[j + 1] != '#' and not ((mask_in >> (j + 1)) & 1):
rec(j + 2, mask_out)
rec(0, 0)
dp = ndp
print(dp.get(0, 0) % MOD)
solve()
計算量: 各行につき到達可能な mask の個数を $M \le 2^W$ として $O(H \cdot M \cdot W)$。$W \le 12$ なら十分高速。
Step-by-Step 解説
1状態設計
dp[mask] は「現在処理しようとしている行の各列のうち、上の行からの縦ドミノで強制的に埋まっている列の集合」をキーとする辞書。初期状態は dp={0:1}。21行分の遷移を再帰で列挙
壁マス/上から埋まっているマス/空きマス(縦ドミノで下に伸ばす・横ドミノで消費する)の場合分けを行う。
壁マス/上から埋まっているマス/空きマス(縦ドミノで下に伸ばす・横ドミノで消費する)の場合分けを行う。
3最終行の判定
全行処理後、
全行処理後、
mask==0 の dp 値のみが正しい完全被覆に対応する。よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 最終行の後で mask≠0 の値も答えに加算 | 「盤外にはみ出す縦ドミノ」の存在を見落とす | 答えは必ず dp.get(0,0) のみ採用 |
| 壁のマスに縦ドミノが伸びてきたケースを弾かない | 壁は「存在しないマス」であることを忘れる | 壁かつ mask_in のビットが立っていれば即座に return |
| 横ドミノの相手マスの状態確認漏れ | 境界・状態チェックの漏れ | j+1<W かつ壁でなくかつ mask_inビットが0であることを確認 |
| 空きマス数が奇数でも全探索してTLE | 早期打ち切りをしない | 事前に偶奇をチェックし奇数なら0を出力してもよい |
次のステップ
- 発展: L字トロミノ(3マス1枚)を許す敷き詰め数え上げへの拡張
- 発展: $W$ が小さく $H$ が非常に大きい場合、行の遷移を行列とみなして行列累乗で高速化
- 次回予告: Gale-Ryser定理(二部次数列の実現可能性判定)