Day 172-Q2 — 2026-10-08 | 端点を指定したハミルトン路の数(括弧 2 種+「端点につながる線」の 3 記号のプラグ DP)

2026-10-08 赤色 Master (Phase 8+) ★★★★★★★★★ 高度 DP(焦点帯 M1)。Day171-Q3 は閉路(ハミルトン閉路)で、線はいつも対になっていたので括弧 2 種で足りた。今回は始点と終点が決まった路。端点から出た線には相手の括弧が無いので、第 3 の記号「端点につながる線」が要る

問題

$H$ 行 $W$ 列の盤面がある。各マスは .(通れる)、#(壁)、S(始点)、T(終点)のいずれかで、S と T はちょうど 1 つずつある。

S から出発し、上下左右に隣り合うマスへ移動しながら、壁以外のすべてのマスをちょうど 1 回ずつ通って T で終わる経路(ハミルトン路)の数を $998244353$ で割った余りを求めよ。

入力形式

H W
G_1
G_2
...
G_H

制約

#条件
1$1\le H,W$、$H\times W\le 300$、$\min(H,W)\le 10$
2$G_i$ は . # S T からなる長さ $W$ の文字列。S と T はちょうど 1 つずつ

入出力例

入力例1

3 3
S..
...
..T

出力例1

2

(左上から右下へ。「行ごとに折り返す」と「列ごとに折り返す」の 2 通り)

入力例2

3 4
S..T
....
....

出力例2

4

入力例3

4 5
T....
...#.
.....
S...#

出力例3

4

(壁があっても同じ DP で扱える。3 例とも DFS の全探索と一致)

背景と出典

盤面の閉路・路の数え上げを「境界を横切る線のつながり方」で状態に持つ方法は、プラグ DP(broken profile DP、連結性 DP)と呼ばれる。線の対応を括弧列で表す方法は、日本の競技プログラミングでは「括弧列による連結性の表現」として知られている、と私は記憶している(本セッションでは原典を確認していない)。

閉路(Day171-Q3)では、境界を横切る線は必ず 2 本ずつ対になり、左の端を (、右の端を ) と書けば括弧列になった。路では、S と T から出た線は相手がいない。そこで記号 3(「端点につながる線」)を足し、各位置を 0・(・)・3 の 4 値(2 ビット)で持つ。

本問は自作。正方形の対角の角を結ぶ場合の答え $3\times3,5\times5,7\times7,9\times9$ で $2,104,111712,2688307514$ を得た。これは OEIS A001184 として私が記憶している値と一致する(OEIS は本セッションでは参照していない)。

概念図

入力例 1 の 1 本の路と、マス (1,0) まで処理した時点の境界の状態 S T ( ) 3 塗ったマス = 処理済み。水色の線 = 境界 境界の 4 か所(位置 0〜3)の状態: ( ) 0 3 ( と ) : 処理済みの側で互いにつながった 2 本((1,0) を通る) 3 : 処理済みの側で S につながった線(S→(0,1)→(0,2)→下へ) 閉路の問題(Day171-Q3)では記号は ( ) の 2 種で足りた 路では端点の線に相手がいないので 3 を足す 括弧を端点で止める → 相手の括弧が 3 に変わる ( と ) が出会う → 閉路なので禁止 3 と 3 が出会う → S から T までの路が完成

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

ヒント1(方向性)

1 マスずつ左上から処理し、「処理済みのマスとまだのマスの境界」を横切る線だけを状態に持つ。境界は $W+1$ か所。通常のマスは線が 2 本(次数 2)、S と T は 1 本(次数 1)。

ヒント2(アプローチ)

閉路では線は対になるので括弧列で表せた。路では端点から出た線に相手がいない。記号を 1 つ増やして「この線をたどると端点に行く」を表す。括弧の線を端点で止めると、その相手の括弧が「端点につながる線」に変わる。

ヒント3(誘導)

左の線 $L$ と上の線 $U$ の組で場合分けする。

$L,U$通常マス端点マス
0,0( を下・) を右に出す3 を下か右に 1 本出す
片方 $x$$x$ を下か右へ延ばす$x=3$ なら路の完成、括弧なら相手を 3 に
) (つなぐだけ不可(次数 2 になる)
( ( / ) )相手の括弧を付け替え不可
( )閉路になるので禁止不可
3 と括弧括弧の相手を 3 に不可
3 3路の完成不可

模範解答 (Python)

模範解答を表示する(まず自力で解いてから)
import sys

MOD = 998244353


def match(st, j, w):
    # 位置 j の括弧('('=1 / ')'=2)と対になる括弧の位置を返す
    d = 0
    if (st >> 2 * j) & 3 == 1:
        for i in range(j, w + 1):
            c = (st >> 2 * i) & 3
            if c == 1:
                d += 1
            elif c == 2:
                d -= 1
                if d == 0:
                    return i
    else:
        for i in range(j, -1, -1):
            c = (st >> 2 * i) & 3
            if c == 2:
                d += 1
            elif c == 1:
                d -= 1
                if d == 0:
                    return i
    raise AssertionError


def setp(st, i, c):
    return st & ~(3 << 2 * i) | (c << 2 * i)


def main(mod=MOD):
    data = sys.stdin.read().split()
    h, w = int(data[0]), int(data[1])
    g = data[2:2 + h]
    if w > h:                                   # 狭い方を幅にする(状態数は幅で決まる)
        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):
            c = g[i][j]
            down_ok = i + 1 < h and g[i + 1][j] != '#'
            right_ok = j + 1 < w and g[i][j + 1] != '#'
            ndp = {}

            def add(s, v):
                ndp[s] = (ndp.get(s, 0) + v) % mod if mod else ndp.get(s, 0) + v

            for st, v in dp.items():
                L = (st >> 2 * j) & 3                # 左から入る線(位置 j)
                U = (st >> 2 * (j + 1)) & 3          # 上から入る線(位置 j+1)
                base = setp(setp(st, j, 0), j + 1, 0)
                if c == '#':
                    if L == 0 and U == 0:
                        add(st, v)
                    continue
                if c in 'ST':                        # 端点: 次数 1
                    if L and U:
                        continue
                    if L == 0 and U == 0:            # ここから線を 1 本出す(記号 3 = 端点につながる線)
                        if down_ok:
                            add(setp(base, j, 3), v)
                        if right_ok:
                            add(setp(base, j + 1, 3), v)
                        continue
                    x, pos = (L, j) if L else (U, j + 1)
                    if x == 3:                       # 反対の端点からの線が届いた = 路が完成
                        add(base, v)
                    else:                            # 括弧の線を端点で止めると、相手の括弧が端点につながる線になる
                        add(setp(base, match(st, pos, w), 3), v)
                    continue
                # 通常のマス: 次数 2
                if L == 0 and U == 0:
                    if down_ok and right_ok:
                        add(setp(setp(base, j, 1), j + 1, 2), v)
                elif L == 0 or U == 0:
                    x = L | U
                    if down_ok:
                        add(setp(base, j, x), v)
                    if right_ok:
                        add(setp(base, j + 1, x), v)
                else:
                    if L == 1 and U == 2:            # 閉路ができる: ハミルトン路では禁止
                        continue
                    if L == 2 and U == 1:            # ')(' はそのままつながる
                        add(base, v)
                    elif L == 1 and U == 1:          # '((' : 上の線の相手 ')' が '(' になる
                        add(setp(base, match(st, j + 1, w), 1), v)
                    elif L == 2 and U == 2:          # '))' : 左の線の相手 '(' が ')' になる
                        add(setp(base, match(st, j, w), 2), v)
                    elif L == 3 and U == 3:          # S 側と T 側の線がつながる = 路が完成
                        add(base, v)
                    else:                            # 端点の線と括弧: 括弧の相手が端点の線になる
                        b, pos = (U, j + 1) if L == 3 else (L, j)
                        add(setp(base, match(st, pos, w), 3), v)
            dp = ndp
        dp = {st << 2: v for st, v in dp.items() if (st >> 2 * w) & 3 == 0}   # 行末: 右端の線は 0 のはず
    return dp.get(0, 0)


if __name__ == "__main__":
    print(main())

計算量: 幅 $w=\min(H,W)$ の状態数を $S_w$ として $O(HW\cdot S_w\cdot w)$(括弧の対応探しが $O(w)$)。$S_w$ は実測で幅 8 が最大 513、幅 10 が最大 3333。$10\times30$ で 0.56〜1.03 s、$10\times20$ で 0.37〜0.60 s(Python 3.14.7、M1 MacBook Pro、負荷平均 3〜16 での実測)。

Step-by-Step 解説

1境界の線だけを持つ

左上から 1 マスずつ処理する。処理済みと未処理の境界には $W+1$ か所の「線が横切りうる場所」がある。位置 $j$ がいま処理するマスの左、位置 $j+1$ が上。経路の途中の形は気にせず、「境界を横切る線どうしが、処理済みの側でどうつながっているか」だけを覚える。

2括弧で対を表す(閉路と同じ)

処理済みの側で 2 本の線がつながっているとき、左の端を (、右の端を ) とする。線は交差しないので、これは正しい括弧列になる。(( が合流すると、上の線の相手の ) が ( に変わる(Day171-Q3 と同じ)。

3端点の線は記号 3

S や T から出た線は、処理済みの側で端点につながっていて、相手の線がいない。これを 3 と書く。括弧の線が端点マスで止まると、その相手の括弧は「たどると端点に行く線」になるので 3 に書き換える。3 は同時に最大 2 本(S 側と T 側)。

4禁止と完成

通常マスで ( と ) が出会うと、その 2 本は既につながっているので閉路ができる。ハミルトン路は 1 本の路なので禁止。3 と 3 が出会う(または端点マスに 3 が届く)と S から T までの路が完成する。完成後に残りのマスがあれば、そのマスは線を出すしかなく、最後に状態 0 に戻れないので自然に数えられない。

5狭い方を幅にする

状態数は幅で決まる。$8\times16$ の盤面を転置せずに幅 16 で処理すると、状態は最大 1152167 個・129.57 s。転置して幅 8 にすると最大 901 個・0.26 s(実測)。

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

案計算量採否理由
DFS の全探索指数検算用のみ30 マス程度まで。1250 盤面の照合に使用
連結成分の番号を最小表現で持つ(括弧を使わない)状態の正規化が毎回 $O(w)$不採用括弧列の方が状態の符号化が 2 ビット×位置で済み、Day171-Q3 の実装をそのまま拡張できる
端点を「仮の 1 マス」で外とつないで閉路に帰着盤面の外に通路が要る不採用端点が盤面の内側にあると外への通路が他のマスと交差し、帰着できない(私の判断。実装・計測はしていない)
括弧 2 種+端点の線 3(本解)$O(HW S_w w)$採用$10\times30$ で 0.56〜1.03 s

前提が変わったら:

  • 端点を指定しない(どこから始まりどこで終わってもよい)なら、各マスで「ここを端点にする」選択を許し、3 が 2 本使われたかを状態に 2 ビット足す。状態数は増えるが同じ枠組みで書ける
  • 幅が 12 以上なら、状態数が数万〜数十万になり、Python の辞書では遅い。状態を整数配列に詰めてハッシュを自作するか、C++ にする
  • 数ではなく 1 本の経路を出力するなら、DP を逆にたどる復元が要る。辞書を各マスごとに保存するとメモリが $O(HW S_w)$ になる

よくあるミス

ミス原因正しい書き方
( ) の合流を許す閉路の問題(Day171-Q3)では最後のマスで閉じてよかった路では常に禁止。許すと路+別の閉路の組まで数え、484 盤面中 53 盤面で誤答(実測)
端点の線を括弧と同じに扱う相手がいない線を表せない記号 3 を足す。括弧を端点で止めたら相手を 3 に
端点マスで $L,U$ の両方から線を受ける次数 1 を忘れるif L and U: continue
転置しない盤面をそのままの向きで処理$8\times16$ を幅 16 で処理すると 129.57 s(実測)。幅 8 なら 0.26 s
市松模様の偶奇を確かめずに答えが 0 で驚くS と T の色の条件マス数が偶数なら S と T は異なる色、奇数なら同じ色(多い方の色)でないと 0。入力例 1 の $4\times4$ 版(対角)は 0

次のステップ

発展1: 端点を指定しないハミルトン路の数(各マスが端点になりうる)

  • 発展2: 盤面を $k$ 本の互いに素な路で覆う数(3 の本数を状態に持つ)
  • 発展3: 原典(括弧列によるプラグ DP の解説)と OEIS A001184 を確認する(本セッションでは未確認)

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

DFS 全探索との照合(実測): $H,W\le5$ のランダム盤面(壁 0〜35%、S T は任意の位置)766 盤面、30 マス以下の盤面 484 盤面、計 1250 盤面で不一致 0(答えが 0 でないもの 314)

  • 正方形の角どうし(実測): 左上→右下は $n=3,5,7,9$ で $2,104,111712,2688307514$(偶数の $n$ は 0)。左上→左下は $n=2..10$ で $1,2,8,86,1770,88418,8934966,2087813834,1013346943033$。このうち $n\le6$ は DFS と一致。左上→左下は OEIS A000532 だと記憶していたが、記憶していた $n=5$ の値 52 が DFS の 86 と食い違ったので、照合には使わなかった
  • 誤った方法(実測): ( ) の合流を許す版は 484 盤面中 53 盤面で誤答
  • 入力例(実測): 例 1 2、例 2 4、例 3 4(DFS と一致)
  • 実行時間(実測、負荷平均 3〜16): 幅 10 で $H=20$ 0.37〜0.60 s・$H=30$ 0.56〜1.03 s(最大状態数 3333)、幅 9 で $H=30$ 0.49 s、幅 8 で $H=30$ 0.24〜1.63 s(負荷の高い時間帯で振れた。最大状態数 513)。$8\times16$ を転置しない版は 129.57 s(状態 1152167)、転置すると 0.26 s(状態 901)
  • 扱っていない範囲: 端点の指定なし、複数の路での被覆、経路の復元、幅 11 以上、原典と OEIS の確認

自己評価

自分の回答

気づき・メモ