Day 167-Q2 — 2026-10-01 | 壁付き盤面のドミノ敷き詰めの数え上げ(マスごとのプロファイル DP と、狭い方を幅にする転置)

2026-10-01 赤色 Master (Phase 8+) ★★★★★★★★★ DP(焦点帯 M1)。盤面を 1 マスずつ走査し、「次に走査するマスの手前に張り出している占有」を $W$ ビットで持つ。行ごとに全遷移を列挙せず、マス単位で遷移を 2 択に分けることで定数を落とす。壁の扱いと転置を外す誤りを数で確かめる 10/1 のバックフィル分(出題日を 2026-10-01 として生成)

問題

$H\times W$ の盤面があり、各マスは空き . か壁 # である。壁でないマス全てを、$1\times2$ のドミノ(縦置きも横置きも可)で重ならず隙間なく敷き詰める方法の数を $998244353$ で割った余りで求めよ。壁のマスにはドミノを置けない。

入力形式

H W
S_1
...
S_H

制約

#条件
1$1\le H,W\le100$、かつ $\min(H,W)\le14$
2$S_i$ は長さ $W$ の . と # からなる文字列

入力例1

2 2
..
..

出力例1

2

入力例2

3 4
..#.
....
.#..

出力例2

1

入力例3

3 3
...
.#.
...

出力例3

2

(中央が壁の 3×3。外周の 8 マスを環状に並べて 2 通り)

入力例4

8 8
........
........
........
........
........
........
........
........

出力例4

12988816

(8×8 のチェス盤のドミノ敷き詰めの数。この値は有名な数として記憶しており、プログラムの出力と一致した。出典は未確認)

背景と出典

ドミノ敷き詰めの数え上げは、格子の完全マッチングの個数に等しく、壁が無い場合は Kasteleyn の行列式による公式(FKT 法)があると記憶している(本セッションでは原典を確認していない)。壁があっても平面グラフなので原理的には Pfaffian で計算できるが、競技では $\min(H,W)\le14$ 程度ならプロファイル DP(Broken Profile DP)が標準的で、実装も短い。

この問題で確かめたいのは次の 3 点。(1) マス単位のプロファイル DP が、素朴なバックトラックと全件一致するか。(2) 壁を無視して置く誤り(置き先が壁でも許す)が何件で外れるか。(3) 幅を狭い方に転置するか否かで、$14\times100$ の計算時間がどう変わるか。

ヒント(クリックで段階的に開示)

ヒント1(方向性)

盤面を左上から右へ、行ごとに 1 マスずつ見る。いま見ているマスが「すでに埋まっている」か「空いている」かで場合分けする。空いているなら、そのマスを覆うドミノは右か下に向かう。

ヒント2(アプローチ)

状態は「走査済みのマスのうち、まだ走査していないマスを覆っているもの」を $W$ ビットで持てば足りる。ビット $j$ が立っていれば「列 $j$ の、いま走査する位置にあるマスは、上から縦に張り出したドミノで埋まっている」。マス $(i,j)$ でビット $j$ が立っていれば消して先へ。立っていなければ、縦置き(ビット $j$ を立てて次の行へ張り出す)か、横置き(右のマスが空きなら、ビット $j+1$ を立てる)のどちらか。

ヒント3(誘導)

壁のマスでは、ビット $j$ が立っていることはない(壁へは置かない)ので、何も置かずに先へ進めればよい。置き先が壁かどうかは置く側が判定する(g[i+1][j]=='.' と g[i][j+1]=='.')。$W>H$ なら盤面を転置して、状態数を $2^{\min(H,W)}$ に抑える。到達できる状態は半分程度なので、配列でなく辞書で持てば速い。

模範解答 (Python)

模範解答を表示する(まず自力で解いてから)
import sys
MOD = 998244353
def count(g):
    H, W = len(g), len(g[0])
    if W > H:                                  # 狭い方を幅にする(状態数は高々 2^min(H,W))
        g = ["".join(g[i][j] for i in range(H)) for j in range(W)]
        H, W = W, H
    dp = {0: 1}
    for i in range(H):
        for j in range(W):
            b = 1 << j
            new = {}
            def add(m, v):
                new[m] = (new.get(m, 0) + v) % MOD
            wall = g[i][j] == '#'
            down = i + 1 < H and g[i+1][j] == '.'
            right = j + 1 < W and g[i][j+1] == '.'
            for m, v in dp.items():
                if m & b:                      # このマスは上から縦置きで埋まっている
                    add(m ^ b, v)              # (壁には置かないので wall と同時には起きない)
                elif wall:
                    add(m, v)                  # 壁: 何も置かず次へ
                else:
                    if down: add(m | b, v)                         # 縦置き: 下のマスを埋める
                    if right and not (m & (b << 1)): add(m | (b << 1), v)   # 横置き: 右のマスを埋める
            dp = new
    return dp.get(0, 0)
def main():
    H, W = map(int, sys.stdin.readline().split())
    g = [sys.stdin.readline().strip() for _ in range(H)]
    print(count(g))
if __name__ == "__main__": main()

計算量: $O(HW\cdot2^{\min(H,W)})$。実測(Python 3.14.7、M1 Mac)で $14\times100$ の壁なしが 5.72 s、$16\times16$ が 1.44 s、$18\times18$ が 8.89 s。到達する状態だけを辞書で持つ。

Step-by-Step 解説

11 マスずつ走査する理由

行ごとに「次の行へ張り出すパターン」を全部列挙すると、1 行で $2^W\times2^W$ の遷移を見る必要がある(工夫すれば減らせるが実装が重い)。マスごとに分けると、1 マスの遷移が高々 2 通り(縦・横)になり、状態は $2^W$ のまま、$HW$ 回の更新で済む。

2ビットの意味

ビット $j$ は「現在の走査位置に着いたときに、列 $j$ のマスが埋まっているか」。走査位置が $(i,j)$ のとき、ビット $0\ldots j-1$ は次の行のマス、ビット $j\ldots W-1$ は今の行のマスを指す。縦置きで下のマスを埋めるときは、ビット $j$ を「次の行の列 $j$」の意味に切り替えて立てる。横置きで右を埋めるときは、今の行の列 $j+1$ のビットを立てる。

3壁の扱い

壁のマスにはドミノが置かれないので、ビット $j$ は常に 0。何も置かずに状態を引き継ぐ。ドミノを置くときは、置き先の . を確かめる。この確認を落とすと、壁の上にドミノを置いてしまう。

4転置

$W>H$ のとき盤面を転置し、幅を狭い方にする。$14\times100$ では $2^{14}$ 状態で済むが、転置せずに幅 100 で計算すると、到達可能な状態の数も実用にならない。転置はドミノの数え上げを変えない(回転しても敷き詰めの集合の大きさは同じ)。

5答えは最後に全部が 0 の状態

全マスを走査し終えたとき、次の行へ張り出しが残っていてはいけない。状態 0 の値が答え。到達できなければ 0。

迷った他案と採らなかった理由

案計算量採否理由
マスごとのプロファイル DP(本解)$O(HW\,2^{\min})$採用400 件のバックトラックと一致。$14\times100$ が 5.72 s
行ごとに遷移を全列挙(行マスク → 行マスク)$O(H\,4^{W})$ か工夫して $O(H\,2^W\cdot W)$不採用実装が重く、$W=14$ では素朴版は $4^{14}$ で間に合わない
転送行列のべき乗($H$ が巨大のとき)$O(8^W\log H)$不採用本問は $H\le100$。壁があるので行ごとに行列が変わり、べき乗にならない
Kasteleyn の行列式(FKT 法)$O((HW)^3)$不採用壁付きの Pfaffian の符号付けが必要で実装が重い。今回の制約なら DP が短い

前提が変わったら:

  • $H\times W$ が両方 30 以上になると、$2^{30}$ 状態で DP は成り立たない。Kasteleyn の公式(平面グラフの Pfaffian)が必要になる。実装していない
  • ドミノでなく 1×3 のトロミノや L 字のタイルになると、状態に「張り出し 2 マス分」を持つ必要があり、ビット幅が $2W$ に増える
  • 敷き詰めの数でなく最小コストを問う場合は、+ を min に替えるだけで同じ形になる

よくあるミス

ミス原因正しい書き方
置き先が壁でも置く置く側で置き先の . を確かめないg[i+1][j]=='.'、g[i][j+1]=='.' を条件に入れる(139/400 件で誤答)
横置きで右のビットを確かめない右のマスがすでに埋まっているのを見落とすnot (m & (b << 1))
行の終わりで横置きを許す右の列が次の行に回り込むj + 1 < W の判定
転置を忘れる幅 $W$ をそのまま使うif W > H: で転置して幅を狭くする

次のステップ

  • 発展1: 壁を無視して $H$ が $10^{18}$、$W\le8$ のときの敷き詰め数を、転送行列のべき乗で求める
  • 発展2: 各マスにコストがあり、ドミノで覆うマスの和を最大にする(敷き詰めきれなくてよい)問題に変える
  • 次回予告: 焦点帯 M1 から 3 問以上を含む通常のローテーション

検証ログ(本セッションで実行した数値)

  • バックトラックとの照合(実測): $H,W\le6$、壁の確率 0/0.1/0.25 のランダム盤面 400 件で全件一致(バックトラックは左上の空きマスから右・下に置く再帰)
  • 既知値との照合(実測): $2\times n$($n=1..8$)は 1, 2, 3, 5, 8, 13, 21, 34 でフィボナッチ数、$3\times4$ は 11、$4\times4$ は 36、$8\times8$ は 12988816(上の記憶と一致。出典は未確認)
  • 誤った式(実測): 置き先が壁かを確かめないと、400 件中 139 件で外れる
  • 転置の一致(実測): $12\times40$ と $40\times12$ で同じ値 630154621
  • 実行時間(実測): $14\times100$ 壁なし 5.72 s(答え 544239203)。壁 3% を無作為に置いた $14\times100$ は 0.18 s(答え 0。到達できる状態が少ない)。$16\times16$ が 1.44 s、$18\times18$ が 8.89 s
  • 扱っていない範囲: Kasteleyn の公式の実装、$\min(H,W)>14$ の場合、トロミノなど他のタイル、転置なしの時間の測定(幅 100 の状態数が実用にならないことは理屈のみで、実測していない)

自己評価

自分の回答

気づき・メモ