問題
長さ $N$ の文字列 $S$(英小文字)と $Q$ 個のクエリが与えられる。各クエリは文字列 $P_i$(長さ $|P_i|$)で、$S$ 中に $P_i$ が出現する回数と、最初の出現位置(0-indexed, 左端)を最小のものを答えよ。
すべてのクエリ長の総和 $\sum |P_i| \le 2 \times 10^5$ であることが保証されている。
制約
| パラメータ | 範囲 | 備考 |
|---|---|---|
| $N$ | $1 \le N \le 2 \times 10^5$ | 文字列長 |
| $Q$ | $1 \le Q \le 10^5$ | クエリ数 |
| $\sum |P_i|$ | $\le 2 \times 10^5$ | 全クエリ長の総和 |
| 文字集合 | 英小文字 |
入出力例
入力例1
10 3
abcabcabc
ab
abc
abcd
出力例1
3 0
3 0
0 -1
概念図: Suffix Array と二分探索
ヒント
ヒント1(方向性)
Suffix Array + LCP Array を使ってパターン検索を二分探索で行うアプローチが Python で現実的。Suffix Array を構築し、各クエリを $O(|P| \log N)$ で処理する。
ヒント2(アプローチ)
- Suffix Array (SA) をダブリング法で $O(N \log^2 N)$ 構築
- パターン $P$ が出現するかどうか: SA 上で二分探索(lower/upper bound)
- 出現回数: upper_bound - lower_bound
- 最初の出現位置: 対応する
SA[lo:hi]の最小値
ヒント3(ほぼ答え)
def lower(sa, s, p):
m = len(p)
lo, hi = 0, len(sa)
while lo < hi:
mid = (lo + hi) // 2
if s[sa[mid]:sa[mid]+m] < p:
lo = mid + 1
else:
hi = mid
return lo
def upper(sa, s, p):
m = len(p)
lo, hi = 0, len(sa)
while lo < hi:
mid = (lo + hi) // 2
if s[sa[mid]:sa[mid]+m] <= p:
lo = mid + 1
else:
hi = mid
return lo
模範解答
import sys
input = sys.stdin.readline
def build_sa(s):
"""Suffix Array O(N log^2 N) ダブリング法"""
n = len(s)
if n == 0:
return []
sa = list(range(n))
rank = [ord(c) for c in s]
k = 1
while k < n:
r = rank
def key(i):
return (r[i], r[i + k] if i + k < n else -1)
sa.sort(key=key)
new_rank = [0] * n
new_rank[sa[0]] = 0
for j in range(1, n):
prev_key = (r[sa[j-1]], r[sa[j-1]+k] if sa[j-1]+k < n else -1)
curr_key = (r[sa[j]], r[sa[j]+k] if sa[j]+k < n else -1)
new_rank[sa[j]] = new_rank[sa[j-1]] + (1 if curr_key != prev_key else 0)
rank = new_rank
if rank[sa[-1]] == n - 1:
break
k <<= 1
return sa
def solve():
data = sys.stdin.read().split()
idx = 0
N, Q = int(data[idx]), int(data[idx+1]); idx += 2
S = data[idx]; idx += 1
sa = build_sa(S)
def lower(p):
m = len(p)
lo, hi = 0, len(sa)
while lo < hi:
mid = (lo + hi) // 2
if S[sa[mid]:sa[mid]+m] < p:
lo = mid + 1
else:
hi = mid
return lo
def upper(p):
m = len(p)
lo, hi = 0, len(sa)
while lo < hi:
mid = (lo + hi) // 2
if S[sa[mid]:sa[mid]+m] <= p:
lo = mid + 1
else:
hi = mid
return lo
out = []
for _ in range(Q):
P = data[idx]; idx += 1
l = lower(P)
u = upper(P)
count = u - l
if count == 0:
out.append("0 -1")
else:
first = min(sa[l:u])
out.append(f"{count} {first}")
print('\n'.join(out))
solve()
Step-by-Step 解説
Step 1: Suffix Array の構築
Suffix Array は文字列のすべての接尾辞を辞書順にソートした際のインデックス列。ダブリング法では長さ $k$ のランクを用いて長さ $2k$ のランクを計算し、$O(N \log N)$ のソートを $O(\log N)$ 回繰り返す。全体計算量は $O(N \log^2 N)$。
Step 2: パターン検索の二分探索
Suffix Array 上で S[sa[i]:sa[i]+|P|] と P を比較しながら二分探索。
lower(P): $S[\text{sa}[i]:]$ が $P$ 以上になる最初の位置upper(P): $S[\text{sa}[i]:]$ が $P$ より大きくなる最初の位置- 出現回数 = upper - lower
各クエリの計算量は $O(|P| \log N)$。
Step 3: 最初の出現位置
sa[l:u] の最小値を取る。計算量は出現区間の幅に比例するが、問題の制約内では十分高速。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
S[sa[i]:sa[i]+m] が範囲外 | m が大きい場合の懸念 | Python スライスは自動クランプされるので安全 |
| lower/upper の境界を間違える | < vs <= の判定ミス | lower は <、upper は <= で前進 |
| SA 構築で k=1 の初期 rank に誤り | ord() を忘れる | rank = [ord(c) for c in s] |
| 二分探索のループ終了条件 | lo == hi でループしない | while lo < hi を使う |
次のステップ
- 発展: SA-IS $O(N)$ 構築を実装する
- 発展: LCP Array(Kasai's algorithm)+ Sparse Table で LCE クエリを $O(1)$ に
- 発展: Suffix Automaton (SAM) で出現回数を $O(|P|)$ に短縮
自己評価
理解度:
自分の回答:
気づき・メモ: