Day 074-Q1 — Aho-Corasick + DP on Automaton(禁止パターン回避・行列累乗)

2026-06-27 赤色 Master / Phase 8+ ★★★★★★★★★ Aho-Corasick・文字列DP・行列累乗

問題

$M$ 個の「禁止パターン」$P_1, P_2, \ldots, P_M$(英小文字)が与えられる。長さちょうど $N$ の英小文字列のうち、どの禁止パターンも部分文字列として含まない文字列の個数を $998244353$ で割った余りを求めよ。

制約

パラメータ範囲備考
$N$$1 \le N \le 10^{18}$文字列長(巨大)
$M$$1 \le M \le 10$禁止パターン数
$\sum |P_i|$$\le 200$パターン総長
文字集合a-z英小文字 26 種

入出力例

入力例1

5 2
ab
bc

出力例1

6696576

入力例2

3 1
aaa

出力例2

17549

概念図: Aho-Corasick オートマトンと状態遷移行列

禁止パターン {"ab","bc"} のオートマトン 0 root 1 'a' 2 'b' 3 BAD(ab) 4 BAD(bc) 'a' 'b' 'b'→BAD 'c'→BAD fail 遷移行列 T 安全な状態: {0,1,2} T[0][1] += 1 ('a') T[0][2] += 1 ('b') T^N → 答え O(K³ log N) 安全な状態 BAD状態(禁止パターン出現)

ヒント

ヒント1(方向性)

$N$ が $10^{18}$ と巨大なので、オートマトン上の状態遷移行列を $N$ 乗する発想が必要。Aho-Corasick でオートマトンを構築し、各状態が「安全(どの禁止パターンも現れていない)」かを管理する。

ヒント2(アプローチ)
  1. 全禁止パターンを Aho-Corasick オートマトンに登録する
  2. 各状態 $s$ に対して 26 通りの遷移先 goto[s][c] を完全に求める(BFS で fail リンク経由の遷移を完全化)
  3. 「安全な状態」のみを使う $K \times K$ 遷移行列 $T$ を作る
  4. 行列 $T$ を $N$ 乗し、初期状態からの到達数の総和が答え
ヒント3(ほぼ答え)
# Aho-Corasick の fail 伝播(BFS)
q = deque()
for c in range(26):
    if goto_list[0][c] == -1:
        goto_list[0][c] = 0  # ルートの未登録 → ルート自身へ
    else:
        fail[goto_list[0][c]] = 0
        q.append(goto_list[0][c])

while q:
    u = q.popleft()
    if output[fail[u]]:
        output[u] = True  # fail 先が BAD → 自分も BAD
    for c in range(26):
        v = goto_list[u][c]
        if v == -1:
            goto_list[u][c] = goto_list[fail[u]][c]
        else:
            fail[v] = goto_list[fail[u]][c]
            q.append(v)

模範解答

import sys
from collections import deque
input = sys.stdin.readline

MOD = 998244353

def solve():
    N, M = map(int, input().split())
    patterns = [input().strip() for _ in range(M)]

    ALPHA = 26
    goto_list = [[-1] * ALPHA]
    fail = [0]
    output = [False]

    # Trie 挿入
    for pat in patterns:
        cur = 0
        for ch in pat:
            c = ord(ch) - ord('a')
            if goto_list[cur][c] == -1:
                goto_list[cur][c] = len(goto_list)
                goto_list.append([-1] * ALPHA)
                fail.append(0)
                output.append(False)
            cur = goto_list[cur][c]
        output[cur] = True

    # BFS で fail リンク & goto 完全化
    q = deque()
    for c in range(ALPHA):
        if goto_list[0][c] == -1:
            goto_list[0][c] = 0
        else:
            fail[goto_list[0][c]] = 0
            q.append(goto_list[0][c])

    while q:
        u = q.popleft()
        if output[fail[u]]:
            output[u] = True
        for c in range(ALPHA):
            v = goto_list[u][c]
            if v == -1:
                goto_list[u][c] = goto_list[fail[u]][c]
            else:
                fail[v] = goto_list[fail[u]][c]
                q.append(v)

    # 安全状態で遷移行列構築
    safe = [i for i in range(len(goto_list)) if not output[i]]
    idx = {s: i for i, s in enumerate(safe)}
    K = len(safe)

    T = [[0] * K for _ in range(K)]
    for s in safe:
        for c in range(ALPHA):
            nxt = goto_list[s][c]
            if not output[nxt]:
                T[idx[s]][idx[nxt]] = (T[idx[s]][idx[nxt]] + 1) % MOD

    # 行列累乗
    def mat_mul(A, B):
        n = len(A)
        C = [[0] * n for _ in range(n)]
        for i in range(n):
            for k in range(n):
                if A[i][k] == 0:
                    continue
                for j in range(n):
                    C[i][j] = (C[i][j] + A[i][k] * B[k][j]) % MOD
        return C

    def mat_pow(Mat, p):
        n = len(Mat)
        R = [[int(i == j) for j in range(n)] for i in range(n)]
        while p:
            if p & 1:
                R = mat_mul(R, Mat)
            Mat = mat_mul(Mat, Mat)
            p >>= 1
        return R

    res = mat_pow(T, N)
    init = idx[0]
    print(sum(res[init]) % MOD)

solve()

Step-by-Step 解説

Step 1: Aho-Corasick の構築

複数パターンを同時に検索するデータ構造。Trie + failure link(KMP の失敗関数の一般化)で構成される。BFS で各状態の遷移を「完全関数」化する(どの文字を入力しても次の状態が一意に定まる)。

output[u] = True は「状態 u に到達 = 禁止パターンが現れた」を意味し、fail リンクを伝播させる。

Step 2: 安全状態と遷移行列

output[u] = False の状態のみを残し、$K \times K$ の遷移行列 $T$ を作る。$T[i][j]$ = 状態 $i$ から 1 文字追加したとき状態 $j$ に遷移する文字の種類数。

Step 3: 行列累乗

長さ $N$ の文字列 → $N$ 回の遷移 → $T^N$ の (0→j) 成分の総和が答え。$N \le 10^{18}$ なので繰り返し二乗法で $O(K^3 \log N)$。

Step 4: 計算量

  • Aho-Corasick 構築: $O(\sum |P| \times 26)$
  • 行列累乗: $O(K^3 \log N)$ ここで $K \le 201$
  • 全体: 約 $201^3 \times 60 \approx 4.8 \times 10^8$(PyPy 推奨)

よくあるミス

ミス原因正しい書き方
fail リンク伝播漏れoutput の fail 伝播をしていないBFS で if output[fail[u]]: output[u] = True
ルートの goto 初期化-1 のまま残すルート (node=0) の未登録 goto は自分自身 (0) に戻す
行列乗算の MOD 忘れ大きな積で誤答各加算後に % MOD を適用
$N=0$ のケース空文字列の扱いmat_pow の実装で自然に処理(単位行列)

次のステップ

  • 発展問題: 禁止パターンを「正規表現」に拡張 → NFA → DFA 変換 → 行列累乗
  • 類題: AtCoder ABC DP 問題「禁止文字列を含まない数え上げ」
  • さらに: Suffix Automaton を使った $O(|P|)$ クエリへの昇華

自己評価

理解度:

自分の回答:

気づき・メモ: