Day 057-Q4 — 包除原理 + 格子路DP(禁止点制約)

2026-06-10 赤色 Master / Phase 8+ ★★★★★★★★★ 包除原理 / 格子路 / LGV補題 / 組み合わせ数え上げ

問題

$W \times H$ のグリッドの左下 $(0,0)$ から右上 $(W, H)$ へ、右方向 $(+1,0)$ または上方向 $(0,+1)$ の移動のみで進む格子路を考える。

$K$ 個の「禁止点」$\{(x_1,y_1), \ldots, (x_K,y_K)\}$ が与えられ、これらの点を一切通らない格子路の総数を $\bmod{998244353}$ で求めよ。

制約

パラメータ範囲
$W, H$$1 \le W, H \le 10^6$
$K$$0 \le K \le 500$
禁止点$(0,0)$ と $(W,H)$ を含まない、重複なし

入出力例

入力例 1

4 4 2
2 2
3 1

出力例 1

48

通常の格子路数 $\binom{8}{4}=70$。禁止点 $(2,2)$ と $(3,1)$ を通る路を包除原理で除いた結果 = 48。

概念図: 禁止点DP の帰納構造

禁止点 DP: f(p) = p に禁止点を通らず到達する経路数 グリッド W=4, H=4 (0,0) (4,4) (2,2) (3,1) 禁止路 DP の帰納式 f(p) = ways(O, p) - Σ f(q) · ways(q, p) [q が p に先行する禁止点] 先行条件: x_q ≤ x_p かつ y_q ≤ y_p ways(a, b): a→b の格子路数 = C(dx+dy, dx) 最終答え: ans = ways(O, G) - Σ f(p) · ways(p, G) [G = (W,H)] 計算量: O(K²) for DP + O(W+H) for 前処理

ヒント(段階的開示)

ヒント1: 方向性
包除原理 + DP で解く。禁止点を通る路を直接数えるのは難しいため、 「禁止点 $p$ に初めて到達する」DP を組む。禁止点を $(x+y)$ の小さい順にソートすることで帰納的に計算できる。
ヒント2: アプローチ
  1. 禁止点を $(x+y)$ の小さい順(次に $x$)にソート
  2. $f(p)$ = $(0,0)$ から $p$ へ、禁止点を一切通らずに到達する格子路数
  3. $f(p) = \binom{x_p+y_p}{x_p} - \sum_{q \prec p} f(q) \cdot \binom{(x_p-x_q)+(y_p-y_q)}{x_p-x_q}$
  4. 答え = $\binom{W+H}{W} - \sum_p f(p) \cdot \binom{(W-x_p)+(H-y_p)}{W-x_p}$
ヒント3: コード骨格
forbidden.sort(key=lambda p: (p[0]+p[1], p[0]))
K = len(forbidden)
f = [0] * K

for i in range(K):
    xi, yi = forbidden[i]
    f[i] = comb(xi+yi, xi)
    for j in range(i):
        xj, yj = forbidden[j]
        if xj <= xi and yj <= yi:  # j が i に先行
            f[i] = (f[i] - f[j] * comb((xi-xj)+(yi-yj), xi-xj)) % MOD

ans = comb(W+H, W)
for i in range(K):
    xi, yi = forbidden[i]
    ans = (ans - f[i] * comb((W-xi)+(H-yi), W-xi)) % MOD

模範解答 (Python)

import sys
input = sys.stdin.readline
MOD = 998244353

def solve():
    W, H, K = map(int, input().split())
    MAXN = W + H + 10

    fact = [1] * (MAXN + 1)
    for i in range(1, MAXN + 1):
        fact[i] = fact[i-1] * i % MOD
    inv_fact = [1] * (MAXN + 1)
    inv_fact[MAXN] = pow(fact[MAXN], MOD - 2, MOD)
    for i in range(MAXN - 1, -1, -1):
        inv_fact[i] = inv_fact[i+1] * (i+1) % MOD

    def comb(n, r):
        if r < 0 or r > n or n < 0:
            return 0
        return fact[n] * inv_fact[r] % MOD * inv_fact[n-r] % MOD

    def ways(ax, ay, bx, by):
        dx, dy = bx - ax, by - ay
        if dx < 0 or dy < 0:
            return 0
        return comb(dx + dy, dx)

    forbidden = []
    for _ in range(K):
        x, y = map(int, input().split())
        forbidden.append((x, y))

    forbidden.sort(key=lambda p: (p[0] + p[1], p[0]))

    f = [0] * K
    for i in range(K):
        xi, yi = forbidden[i]
        f[i] = ways(0, 0, xi, yi)
        for j in range(i):
            xj, yj = forbidden[j]
            if xj <= xi and yj <= yi:
                f[i] = (f[i] - f[j] * ways(xj, yj, xi, yi)) % MOD

    ans = ways(0, 0, W, H)
    for i in range(K):
        xi, yi = forbidden[i]
        ans = (ans - f[i] * ways(xi, yi, W, H)) % MOD

    print(ans % MOD)

solve()

Step-by-Step 解説

Step 1: 格子路の基本公式

$(0,0)$ から $(W,H)$ への格子路数は $\binom{W+H}{W}$。右に $W$ 回・上に $H$ 回の移動順序の選び方。

Step 2: 禁止点 DP の設計

「禁止点を一切通らない経路数 $f(p)$」を最初に通る禁止点で分類する帰納式を作る。点 $p$ に先行する禁止点($x_q \le x_p$ かつ $y_q \le y_p$)を通る経路は $f(q)$ を使って計算。

Step 3: ソートの必要性

$i$ の計算に $j < i$ の $f(j)$ が必要なので、禁止点を「$p$ に先行する $q$ が常に小さいインデックスになる」順序でソートする。$(x+y)$ 順がこれを満たす。

Step 4: 最終答えの計算

$(0,0)$ から $(W,H)$ への全経路から、「少なくとも1つの禁止点を通る経路」を除く。

計算量

処理計算量
前処理(階乗テーブル)$O(W+H)$
禁止点DP$O(K^2)$
最終答え$O(K)$
全体$O(W+H+K^2)$

よくあるミス

ミス原因正しい書き方
禁止点のソート順$(x+y)$ が同じ場合の比較不足(x+y, x) でソート
先行条件の誤り一方の座標のみで判定xj <= xi and yj <= yi の両条件
負の剰余Python の % は非負だが念のため% MOD を最終答えにも適用
ways(p, q) の方向$p$ が $q$ より右上の場合dx >= 0 and dy >= 0 を確認

次のステップ

  • 発展: $K$ 本の格子路が互いに交差しない数え上げ(Lindström-Gessel-Viennot 補題)
  • 類題: AtCoder ABC 155 F, Codeforces 559E
  • 応用: ヤング図形の標準タブロー数(フック長公式との関係)

自己評価

自分の回答:

気づき・メモ: