Day 030-Q1 — Aho-Corasick法(複数パターン文字列マッチング)

2026-05-13 赤色 Master / Phase 8+ ★★★★★★★★★ 文字列マッチング

問題

$N$ 個のパターン文字列と $Q$ 個のテキストに対し、各テキスト中のパターン種類数と延べ出現回数を求めよ。

制約

$1 \le N, Q \le 1000$
$|P_i| \le 100$
$|T_j| \le 10^5$
パターン総長 $\le 10^5$

入出力例

入力例 1

3 2
ab
bc
abc
abcbc
xyzab

出力例 1

3 4
1 1

abcbc: ab(1), bc(2), abc(1) → 種類3, 延べ4。xyzab: ab(1) のみ。

ヒント (段階的開示)

ヒント1: 方向性
Aho-Corasick オートマトン: Trie + Failure Link + Dictionary Link。
ヒント2: アプローチ
パターンで Trie 構築 → BFS で fail 設定 → output を fail 経由で伝播 → テキスト走査でマッチ収集。
ヒント3: 誘導
fail[s] = s に対応する文字列の最長真の接尾辞でオートマトン上にあるノード。KMP の多パターン版。

模範解答 (Python)

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

class AhoCorasick:
    def __init__(self):
        self.goto = [{}]
        self.fail = [0]
        self.output = [[]]
    def add_pattern(self, pattern, idx):
        state = 0
        for c in pattern:
            if c not in self.goto[state]:
                self.goto[state][c] = len(self.goto)
                self.goto.append({})
                self.fail.append(0)
                self.output.append([])
            state = self.goto[state][c]
        self.output[state].append(idx)
    def build(self):
        q = deque()
        for c, s in self.goto[0].items():
            q.append(s)
        while q:
            r = q.popleft()
            for c, s in self.goto[r].items():
                q.append(s)
                state = self.fail[r]
                while state != 0 and c not in self.goto[state]:
                    state = self.fail[state]
                f = self.goto[state].get(c, 0)
                if f == s:
                    f = 0
                self.fail[s] = f
                self.output[s] = list(set(self.output[s]) | set(self.output[f]))
    def search(self, text):
        state = 0
        results = []
        for c in text:
            while state != 0 and c not in self.goto[state]:
                state = self.fail[state]
            state = self.goto[state].get(c, 0)
            for pat_idx in self.output[state]:
                results.append(pat_idx)
        return results

def main():
    N, Q = map(int, input().split())
    patterns = [input().strip() for _ in range(N)]
    texts = [input().strip() for _ in range(Q)]
    ac = AhoCorasick()
    for i, p in enumerate(patterns):
        ac.add_pattern(p, i)
    ac.build()
    for text in texts:
        hits = ac.search(text)
        kinds = len(set(hits))
        total = len(hits)
        print(kinds, total)

main()

Step-by-Step 解説

1Trie 構築
パターン文字列を挿入、終端ノードにインデックス記録。
2Failure Link を BFS で構築
KMP の失敗関数の多パターン拡張。根から順に計算。
3output 伝播
fail を辿ったマッチを事前マージで検索効率化。
4テキスト走査
1 文字ずつ遷移、失敗時は fail で巻き戻し。
5集計
set(hits) で種類数、len(hits) で延べ回数。

よくあるミス

ミス原因正しい書き方
fail[s] == sルート遷移なしif f == s: f = 0
output マージ忘れfail 先の出力反映なしoutput[s] |= output[fail[s]]
failure 遷移ループ状態巻き戻しが浅いwhile で繰り返し巻き戻し
パターン重複カウント同パターンの複数終端set() で種類数管理

次のステップ

  • 出現位置の列挙
  • 動的追加・削除(Incremental AC)

自己評価

自分の回答

気づき・メモ