問題
文字列 $S$(長さ $N$)が与えられる。$Q$ 個のクエリが与えられ、各クエリ $(i, j)$ に対して、接尾辞 $S[i:]$ と $S[j:]$ の最長共通接頭辞の長さ(LCE, Longest Common Extension)を求めよ。
入力形式
N Q
S
i_1 j_1
...
i_Q j_Q
制約
$1 \le N \le 200000$
$1 \le Q \le 200000$
$S$ は英小文字のみ
$0 \le i_k, j_k < N$(0-indexed)
入出力例
入力例1
6 3
banana
1 3
0 2
1 1
出力例1
3
0
5
(1,3): "anana" vs "ana" → "ana"共通(3) / (0,2): "b" vs "n"不一致(0) / (1,1): 同じ接尾辞(N-1=5)
概念図: SA+LCP+RMQによるLCE
ヒント(段階的開示)
ヒント1: 方向性
愚直に先頭から比較すると1クエリO(N)、Q個で O(NQ)。明示的サフィックス木を構築しLCAを求めるのが理論的に直接的だが、Ukkonenのオンライン構築アルゴリズムは実装がかなり複雑。
ヒント2: アプローチ
実務でよく使う代替は「サフィックス配列(SA)+ LCP配列」の組み合わせ。明示的な木を作らずサフィックス木を暗黙的に表現できる。$S[i:]$と$S[j:]$のランクを$r_i,r_j$($r_i<r_j$)とすると、LCE$(i,j)$ = LCP配列の区間$[r_i+1,r_j]$の最小値になる。
ヒント3: 誘導(コード骨格)
def build_suffix_array(s):
n = len(s)
sa = list(range(n))
rank = [ord(c) for c in s]
k = 1
while True:
key = lambda i: (rank[i], rank[i+k] if i+k < n else -1)
sa.sort(key=key)
tmp = [0]*n
for idx in range(1, n):
tmp[sa[idx]] = tmp[sa[idx-1]] + (1 if key(sa[idx-1]) < key(sa[idx]) else 0)
rank = tmp
if rank[sa[-1]] == n - 1:
break
k <<= 1
return sa, rank
模範解答 (Python)
import sys
def solve():
data = sys.stdin.buffer.read().split()
idx = 0
N = int(data[idx]); idx += 1
Q = int(data[idx]); idx += 1
S = data[idx].decode(); idx += 1
n = N
# --- Step1: サフィックス配列の構築(ダブリング法) ---
sa = list(range(n))
rank = [ord(c) for c in S]
k = 1
while True:
def key(i):
return (rank[i], rank[i + k] if i + k < n else -1)
sa.sort(key=key)
tmp = [0] * n
for i in range(1, n):
tmp[sa[i]] = tmp[sa[i - 1]] + (1 if key(sa[i - 1]) < key(sa[i]) else 0)
rank = tmp
if rank[sa[-1]] == n - 1:
break
k <<= 1
# --- Step2: Kasaiのアルゴリズムで LCP 配列を構築 ---
lcp = [0] * n
h = 0
for i in range(n):
if rank[i] > 0:
j = sa[rank[i] - 1]
while i + h < n and j + h < n and S[i + h] == S[j + h]:
h += 1
lcp[rank[i]] = h
if h > 0:
h -= 1
else:
h = 0
# --- Step3: Sparse Table(RMQ) ---
arr = lcp[1:]
m = len(arr)
if m == 0:
st, LOG = [], [0]
else:
LOG = [0] * (m + 1)
for i in range(2, m + 1):
LOG[i] = LOG[i // 2] + 1
K = LOG[m] + 1
st = [arr[:]]
for kk in range(1, K):
prev = st[-1]
half = 1 << (kk - 1)
length = m - (1 << kk) + 1
if length <= 0:
break
st.append([min(prev[i], prev[i + half]) for i in range(length)])
def query_min(l, r):
length = r - l + 1
kk = LOG[length]
return min(st[kk][l], st[kk][r - (1 << kk) + 1])
out = []
for _ in range(Q):
i = int(data[idx]); idx += 1
j = int(data[idx]); idx += 1
if i == j:
out.append(str(n - i))
continue
ri, rj = rank[i], rank[j]
if ri > rj:
ri, rj = rj, ri
out.append(str(query_min(ri, rj - 1)))
print('\n'.join(out))
solve()
計算量: SA構築 O(N log^2 N)(Pythonの組み込みソートで実用上高速)、LCP構築 O(N)、Sparse Table前処理 O(N log N)・クエリ O(1)。全体 O(N log^2 N + Q)。"banana"の例で SA=[5,3,1,0,4,2], LCP=[_,1,3,0,0,2] を手計算し全クエリの出力が一致することを確認済み。
Step-by-Step 解説
1サフィックス配列とランク配列
ダブリング法は長さkのプレフィックス比較のランクを使い長さ2kの比較をO(1)で行う考え方をlog N回繰り返す。組み込みソートがC実装のため実用上十分高速。
ダブリング法は長さkのプレフィックス比較のランクを使い長さ2kの比較をO(1)で行う考え方をlog N回繰り返す。組み込みソートがC実装のため実用上十分高速。
2Kasaiのアルゴリズムの直感
「S[i:]の共通接頭辞長がhなら、S[i+1:]は少なくともh-1からスタートできる」という単調性を利用し、hを使い回すことで全体O(N)に抑える。
「S[i:]の共通接頭辞長がhなら、S[i+1:]は少なくともh-1からスタートできる」という単調性を利用し、hを使い回すことで全体O(N)に抑える。
3なぜLCP区間最小値がLCEになるか
離れた接尾辞同士のLCPは、間にある隣接LCPの最小値を超えられないという単調性がある。これはサフィックス木上でLCAを辿るのと数学的に同値で、暗黙的にサフィックス木を扱っていることになる。
離れた接尾辞同士のLCPは、間にある隣接LCPの最小値を超えられないという単調性がある。これはサフィックス木上でLCAを辿るのと数学的に同値で、暗黙的にサフィックス木を扱っていることになる。
4Sparse TableでO(1)クエリ
LCP配列は静的なのでSparse Table(O(N log N)前処理、O(1)クエリ)が最適。長さ2^kの2区間でオーバーラップさせて覆う古典テクニック。
LCP配列は静的なのでSparse Table(O(N log N)前処理、O(1)クエリ)が最適。長さ2^kの2区間でオーバーラップさせて覆う古典テクニック。
5正しさの検証
"banana"の手計算SA/LCPを使い、クエリ(1,3)→3, (0,2)→0, (1,1)→5がすべて一致することを確認した。
"banana"の手計算SA/LCPを使い、クエリ(1,3)→3, (0,2)→0, (1,1)→5がすべて一致することを確認した。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| LCP配列のインデックスを0-indexedのまま扱う | lcp[r]=LCP(sa[r-1],sa[r])の定義を忘れる | クエリ範囲を[ri+1,rj]として正しくオフセット変換する |
| i=jのクエリで通常のRMQ処理をする | ランクが等しい場合の区間が空になることを考慮していない | i=jは特別扱いし答えはN-iとする |
| Kasaiのhを毎回0にリセットする | 単調性の利用(h-=1で使い回す)を知らない | hを跨いで使い回さないとO(N^2)に退化する |
| ダブリング法でrank[i+k]の範囲外アクセス | 文字列末尾を超えたインデックスの処理を忘れる | i+k<nでなければ-1を番兵として使う |
次のステップ
- 発展: 本問はオフラインだが、Suffix Automatonとの比較で「オンラインに文字列を追加していく場合はどうなるか」を考えてみるとよい。
- 次回予告: ミンコフスキー和(凸多角形の辺マージによる面積計算)