問題
小文字英字と末尾1文字の $(他のどの文字よりも辞書順で小さい区切り文字)からなる文字列 S(長さn)が与えられる。以下の手順でFM-index(簡略版)を構築し、クエリ文字列 P それぞれについて S 中の P の出現回数を、愚直な文字列探索を使わず バックワード検索 のみで求めよ。
構築手順
- 全ての巡回シフトを辞書順ソートした開始位置列を接尾辞配列
SAとする。 BWT[i] = S[(SA[i]-1) mod n]C[c]=S中でcより真に小さい文字の総数。Occ(c, i)=BWT[0:i](半開区間)中のcの出現回数。
バックワード検索(末尾の文字から処理)
lo, hi = 0, n
for ch in reversed(P):
lo = C[ch] + Occ(ch, lo)
hi = C[ch] + Occ(ch, hi)
if lo >= hi:
return 0
return hi - lo
入力形式
S
Q
P_1
:
P_Q
制約
$3 \le |S| \le 30$(
$含む)$1 \le Q \le 10$
各 $P_i$ は小文字英字のみ、$1 \le |P_i| \le 10$
$ は末尾に1文字だけ、他の文字より辞書順最小入出力例
入力例1
banana$
5
ana
na
a
ban
xyz出力例1
2
2
3
1
0BWT(banana$) = "annb$aa"。C = {$:0, a:1, b:4, n:5}。これらだけで愚直探索なしに出現回数を求める。
概念図:バックワード検索によるレンジの絞り込み(クエリ "ana")
ヒント(段階的開示)
ヒント1(方向性)
BWTは「Sの全巡回シフトをソートしたときの各行の最後の文字」。ソート済み巡回シフトの並びは接尾辞配列の並びと本質的に同じなので、BWT[i]は「辞書順でi番目の接尾辞の直前の文字」を意味する。この性質からパターンの末尾の文字から1文字ずつ絞り込む探索ができる。
ヒント2(アプローチ)
C[c]は「cから始まる接尾辞の範囲がSAのどこから始まるか」を表す。Occ(c,i)と組み合わせると、候補範囲[lo,hi)の中で直前の文字がcである接尾辞だけに絞り込んだ新しい範囲をC[c]+Occ(c,lo)からC[c]+Occ(c,hi)として計算できる。
ヒント3(誘導)
def build_fm(S):
n = len(S)
sa = sorted(range(n), key=lambda i: S[i:])
bwt = ''.join(S[(i - 1) % n] for i in sa)
chars = sorted(set(S))
C, total = {}, 0
for c in chars:
C[c] = total
total += bwt.count(c)
return bwt, C
def occ(bwt, c, i):
return bwt[:i].count(c)
模範解答 (Python)
import sys
def build_fm(S):
n = len(S)
sa = sorted(range(n), key=lambda i: S[i:])
bwt = ''.join(S[(i - 1) % n] for i in sa)
chars = sorted(set(S))
C = {}
total = 0
for c in chars:
C[c] = total
total += bwt.count(c)
return bwt, C
def occ(bwt, c, i):
return bwt[:i].count(c)
def backward_count(S, P, bwt, C):
n = len(S)
lo, hi = 0, n
for ch in reversed(P):
if ch not in C:
return 0
lo = C[ch] + occ(bwt, ch, lo)
hi = C[ch] + occ(bwt, ch, hi)
if lo >= hi:
return 0
return hi - lo
def main():
data = sys.stdin.read().split()
S = data[0]
Q = int(data[1])
bwt, C = build_fm(S)
out = []
for i in range(Q):
P = data[2 + i]
out.append(str(backward_count(S, P, bwt, C)))
print('\n'.join(out))
main()
Step-by-Step 解説
1BWTを構築する
全巡回シフトを辞書順ソートし、各行の最後の1文字を集める。
全巡回シフトを辞書順ソートし、各行の最後の1文字を集める。
$が最小文字なので通常の接尾辞配列と一致する。2C[c]を求める
C[c]は「Sの中でcより辞書順で小さい文字が何個あるか」。cから始まる接尾辞ブロックがSAのどこから始まるかに一致する。3Occ(c,i)の役割
候補範囲
候補範囲
[lo,hi)の中で直前の文字がcの接尾辞はOcc(c,hi)-Occ(c,lo)個あり、全てC[c]+Occ(c,lo)から始まる範囲に移動する(LFマッピング)。4末尾から先頭へ縮小
パターン全体を処理し終えた範囲の幅がそのまま出現回数になる。
パターン全体を処理し終えた範囲の幅がそのまま出現回数になる。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| パターンを先頭から処理してしまう | 通常の文字列探索の感覚で先頭から読む | バックワード検索は必ず末尾の文字から処理する |
C[c]を「c以下の文字数」で計算 | 境界条件の誤り | C[c]はcより真に小さい文字の総数にする |
lo>=hi後も処理を続ける | 早期終了の判定漏れ | 候補範囲が空になった時点で0を返す |
Occをbwt[:i+1]で数える | 半開区間の境界を誤解 | Occ(c,i)はbwt[0:i]と統一する |
| Cに無い文字の扱いを忘れる | Sに出現しない文字を含むPの扱い漏れ | Cに無い文字が来たら出現回数0で打ち切る |
次のステップ
- 発展: 本問では
Occ(c,i)を毎回$O(n)$で愚直に数えたが、実際のFM-indexでは各文字ごとのビットベクトルにRank/Select構造(Day145-Q3)を載せ$O(1)$に高速化する。 - 次回予告: BK-tree(Burkhard-Keller Tree・編集距離の三角不等式による距離空間探索木)