Day 083-Q1 — Suffix Automaton + 最長共通部分文字列クエリ(SA-DAWG・LCS $O(N+M)$)

2026-07-06 赤色 Master / Phase 8+ ★★★★★★★★★ Suffix Automaton・LCS・オンライン文字列照合

問題

文字列 $S$(長さ $N$)が与えられる。$Q$ 個のクエリが与えられ、各クエリでは文字列 $T_i$(長さ $M_i$)が与えられる。各クエリについて $S$ と $T_i$ の最長共通部分文字列(LCS)の長さを求めよ。

Suffix Automaton を $S$ に対して $O(N)$ で構築し、各 $T_i$ を SAM 上で走査することで $O(N + \sum M_i)$ で全クエリに答えよ。

制約

パラメータ範囲備考
$N$$\le 2 \times 10^5$文字列 $S$ の長さ
$Q$$\le 10^5$クエリ数
$\sum M_i$$\le 2 \times 10^5$クエリ文字列の合計長
文字種小文字英字26文字

入出力例

入力例1

7 3
abcabcd
3
abc
4
abcd
5
xyzab

出力例1

3
4
2

概念図: Suffix Automaton の構造と LCS クエリ

SAM("abcabcd") — ノード群と suffix link(破線) root len=0 a len=1 ab len=2 abc len=3 abca len=4 abcab len=5 a b c a b suffix link(破線): LCS クエリ: T = "xyzab" に対する SAM 走査 文字 x y z a b cur_len 0 0 0 1 2 ans 0 0 0 1 2 ✓ 動作 root で x なし→0 同様→0 root→a→ab マッチ 最終 LCS 長 = 2 (共通部分文字列: "ab")

ヒント

ヒント1(方向性)

Suffix Automaton(SAM)は文字列 $S$ の全部分文字列を受理する最小 DAWG(Directed Acyclic Word Graph)。ノード数 $O(N)$、辺数 $O(N)$。各ノードは endpos 等価クラスを表し、lenlink(suffix link)を持つ。

ヒント2(アプローチ)

$S$ の SAM を $O(N)$ で構築後、各クエリ文字列 $T$ について現在ノード cur・マッチ長 cur_len を管理しながら走査する。遷移できない場合は suffix link を辿って後退し、cur_len = sa_len[cur] に更新する。各ステップでの最大 cur_len が LCS 長。

ヒント3(ほぼ答え)
def lcs_query(T):
    cur, cur_len, ans = 0, 0, 0
    for c in T:
        while cur != 0 and c not in sa_trans[cur]:
            cur = sa_link[cur]
            cur_len = sa_len[cur]  # suffix link 先の len に制限
        if c in sa_trans[cur]:
            cur = sa_trans[cur][c]
            cur_len += 1
        ans = max(ans, cur_len)
    return ans

模範解答

import sys
input = sys.stdin.readline

def solve():
    N, Q = map(int, input().split())
    S = input().strip()

    MAXN = 2 * N + 5
    sa_len = [0] * MAXN
    sa_link = [-1] * MAXN
    sa_trans = [None] * MAXN
    for i in range(MAXN):
        sa_trans[i] = {}

    size = 1
    last = 0

    def extend(c):
        nonlocal size, last
        cur = size; size += 1
        sa_len[cur] = sa_len[last] + 1
        p = last
        while p != -1 and c not in sa_trans[p]:
            sa_trans[p][c] = cur
            p = sa_link[p]
        if p == -1:
            sa_link[cur] = 0
        else:
            q = sa_trans[p][c]
            if sa_len[p] + 1 == sa_len[q]:
                sa_link[cur] = q
            else:
                clone = size; size += 1
                sa_len[clone] = sa_len[p] + 1
                sa_link[clone] = sa_link[q]
                sa_trans[clone] = dict(sa_trans[q])
                while p != -1 and sa_trans[p].get(c) == q:
                    sa_trans[p][c] = clone
                    p = sa_link[p]
                sa_link[q] = clone
                sa_link[cur] = clone
        last = cur

    for c in S:
        extend(c)

    results = []
    for _ in range(Q):
        M = int(input().strip())
        T = input().strip()
        cur = 0
        cur_len = 0
        ans = 0
        for c in T:
            while cur != 0 and c not in sa_trans[cur]:
                cur = sa_link[cur]
                cur_len = sa_len[cur]
            if c in sa_trans[cur]:
                cur = sa_trans[cur][c]
                cur_len += 1
            ans = max(ans, cur_len)
        results.append(ans)

    print('\n'.join(map(str, results)))

solve()

Step-by-Step 解説

Step 1: SAM の構造

属性意味
sa_len[v]ノード $v$ が表す最長部分文字列の長さ
sa_link[v]suffix link(親等価クラスへのポインタ)
sa_trans[v]遷移辞書 {文字: 次ノード}

SAM のノード数は最大 $2N-1$、辺数は最大 $3N-4$。

Step 2: extend 操作($O(N)$ amortized)

新文字 $c$ を追加するたびに最大2ノードが生成される。cur: 新しい部分文字列を受理するノード。clone: 既存ノードの分割(suffix link 修正のため)。

clone = size; size += 1
sa_len[clone] = sa_len[p] + 1
sa_link[clone] = sa_link[q]
sa_trans[clone] = dict(sa_trans[q])  # 必ず浅いコピー

Step 3: LCS クエリ処理($O(\sum M_i)$)

クエリ文字列 $T$ を SAM 上で走査。遷移できない場合は suffix link を辿って後退し、cur_len = sa_len[cur] に更新する。各ステップでの cur_len の最大値が LCS 長。

よくあるミス

ミス原因正しい書き方
clone 作成時に trans を参照コピー辞書の参照共有で後続の extend が壊れるdict(sa_trans[q]) で浅いコピー
後退時に cur_len を更新しないsuffix link 先の len に長さが制限されるcur_len = sa_len[cur] と更新
root での無限ループroot の link = -1 を確認しないwhile cur != 0 and ...
MAXN 不足SAM ノード数は最大 $2N-1$MAXN = 2*N+5 以上に設定

次のステップ

  • 発展問題: 複数の文字列 $S_1, \ldots, S_K$ の全ての LCS を求めよ(Generalized SAM)
  • 参考: Blumer et al. (1985) "The smallest automaton recognizing the subwords of a text"

自己評価

理解度: / /

自分の回答:

気づき・メモ: