【圧縮文字列索引 連載part3】Burrows-Wheeler TransformとFM-index
はじめに
本記事は圧縮文字列索引についての連載第3弾である。
前半は、文字列圧縮のために提案された概念であるBurrows-Wheeler Transform(BWT)*1について、そしてそれを用いた文字列検索アルゴリズムについて説明する。
後半にはBWTを利用した文字列索引の一種であるFM-indexを紹介する。FM-indexの系譜をたどれば、最後には2022年現在トップクラスの性能を誇る索引であるr-index*2へと行き着く。その源流を理解しよう。
BWT
定義
まずBurrows-Wheeler Transform(BWT)を定義しよう。
文字列]に対するBWTとは、を並び替えたシンボル列]で、以下の条件を満たすものである。
定義からだとイメージが掴めないので、例のごとくに対してBWTを構築してみよう。
要は、接尾辞を辞書順にソートして(ここまでは接尾辞配列と同じ。part2参照)、それら接尾辞の直前に出現したシンボルを並べてできるシンボル列がBWTである。 なお、該当する辞書順の接尾辞が文字列そのものであった場合は、例外的にとする。
豆知識
ちなみに、なぜBWTを表す記号としてが使われるかというと、文字列の回転を辞書順に並べてできる行列の、最後の列("L"ast column)がBWTに一致するからである。 というより、当初はそもそもそういう定義で提案された。先述の接尾辞配列を用いた機械的な定義は、のちに整理されたものである。
当初はブロックソートという文字列圧縮手法のために提案されたBWTだが、現在ではその枠を超えた応用が考えられている。では次節以降で何に役立つのかを述べていこう。
BWTを使ってできること:後方探索
結論から述べると、BWTはパターンを検索するために使える。 それだけならば接尾辞配列でも可能だが、それに加えて接尾辞配列よりも圧縮しやすいことが、BWTの注目に値する特性である。直感的には、辞書順で隣接する接尾辞たちは似た文脈を持っており、それらの直前に出現するシンボルも同じものになりやすい。つまり、冗長な文字列のBWTでは、同じシンボルが何個も続きやすい傾向にある。
の計算に入ろう。part2にて言及した、パターンと接尾辞配列上の区間が対応するという性質を思い出してほしい。 この区間をうまいこと求めるのにBWTは使える。このとき、接尾辞配列の場合とは異なり、二分探索を必要とせず、元の文字列を持っている必要もない。その代わり、配列と操作を必要とする。これらが何かはすぐに説明する。
あるパターンと、接尾辞配列上の区間が対応していると仮定しよう。 このとき、の左に任意のシンボルを付け加えた新しいパターンに対応する区間を、
という式で計算することができるのだ。 この式における見慣れないものたちは
- よりも小さいシンボルの出現回数
- BWTの先頭個のシンボル]中のの個数
を意味する。パターンを1文字左に拡張する様子から、この計算に基づく区間の更新操作をleft-extensionと呼ぶことにする。
図を用いて、これが正しい理由を説明しよう。
に対応する接尾辞配列上の区間を求めたい。まず辞書式順序の性質より、シンボルから始まる接尾辞は、それよりも小さいシンボルから始まる接尾辞の後に並ぶ。この分のずれをで求める。 次に求める必要があるのは、シンボルから始まりつつもパターンより辞書順が小さい接尾辞の数である。ここで着目するのは、接尾辞との大小は、先頭の同一シンボルを除いたとの大小と一致する、という事実である。この事実より、「シンボルから始まりつつもパターンより辞書順が小さい接尾辞の数」は、「パターンよりも辞書順が小さい接尾辞で、直前のシンボルがであるものの数」と言い換えることが可能になる。パターンよりも辞書順が小さい接尾辞は接尾辞配列上でにあり、このうち直前のシンボルがであるものとはすなわち、BWTの値がであるものに他ならない。ゆえにが求める値になるわけだ。区間の右端についても同様で、「辞書順がパターン以下で、直前のシンボルがである接尾辞の数」を数えればよく、これらをまとめて先述の区間を得る。
なお、「やをどうやって計算するのか?」「サイズはどのぐらいになるのか?」という疑問は当然出るが、これは索引の実装に依存する問題なので、ここでは置いておいて後半のFM-indexで説明する。さしあたってはこういった操作がコンパクトかつ高速に計算できるという前提で議論を追ってほしい。
left-extensionができれば、あとは話が早い。
接尾辞配列ではパターンの先頭から1文字ずつ比較していくのと対照的に、 BWTを用いたパターン検索では、パターンの末尾から1文字ずつ左へマッチングしていくことが分かるだろう。 このような特性から、このアルゴリズムは後方探索(backward search)と呼ばれる。
後方探索の時間計算量
後方探索は二分探索を必要としない点で優れている。 left-extensionを計算する時間をとおいたとき、後方探索によるcountの時間計算量はとなる。は実装依存で変わってくるが、基本的にはよりも速いことが期待される。
との命名は「LF-mapping」に由来するものだ。以下の関数を考えてほしい。left-extensionと同じ計算を、シンボルと辞書順に対し適用するものだ。
これは接尾辞配列上の位置を、新たな位置にマッピングする関数になっている。新たな位置は、番目の接尾辞にその直前のシンボルを足した接尾辞を指し示している。これが、接尾辞の回転行列における最後の列と最初の列を対応付けるものであることからLF-mappingと呼ばれる。下図に例を示しておこう。
LF-mappingの持つ性質を式にすると以下のようになる。
接尾辞配列上の位置で見るとかなりざっくばらんとした並び替えだが、文字列位置でみると「一個左にずらす」操作をしている。このように、文字列位置と接尾辞配列上の位置(=辞書順)という二つの値は、全く異なる挙動をすることに注意されたい。
後方探索について説明が終わったところで、これを利用した初めての索引であるFM-indexを紹介していこう。
FM-index -後方探索を使った文字列索引-
概要
FM-index*3は、2005年に提案された、countとlocateを実現する圧縮文字列索引だ。名前は著者のイニシャルから取ったっぽい(そもそも元論文では名前を付けていないので他者による後付けだ)。
countはここまでで説明してきた後方探索により計算する。との計算方法については説明を後回しにしてきたが、まずは全ての値を配列に保存すればよい。 アルファベットサイズは一般的には定数(文字列長がいくら大きくなろうと変わらない)と考えられるので、これで十分にコンパクトだ。については、そのものは文字列と同じサイズになってしまうことに注意する必要がある。これをどうにか圧縮しつつ、その上でという操作を高速に行いたい。この二つの要求に対し、元論文ではBW_RLXというアルゴリズムで応えている。なお、現在では「ウェーブレット木」*4という簡潔データ構造を用いた方が性能が高いことが分かっているので、こちらを採用して性能を評価する。ウェーブレット木はあくまで道具として利用するので、アルゴリズムの詳細が気になった場合は別途調べられたい。既存の構造をコンポーネントとして新たな構造を作っていく連鎖は簡潔データ構造あるある。
BWT に対してウェーブレット木を構築すると、サイズビットで、へのアクセスおよびの計算が時間*5になる。ここでは文字列の次経験エントロピーだ。簡単に言うと、の冗長さ・圧縮しやすさを示す値で、が冗長であるほど小さくなる。経験エントロピーについても別途エントリを書く予定だ。
次にlocateなのだが、こちらにも工夫が要る。もし仮に、我々が接尾辞配列本体をそのまま持っていたとすれば、後方探索により接尾辞配列上の区間]を求めたのち、]を列挙すれば済む話だ。ただ、この話は「接尾辞配列が大きすぎる」というところから始まっている。そのまま持つわけにはいかないのだ。 そこで、接尾辞配列の値を全て保存するのではなく、一定間隔ごとにサンプリングして保存するという考えを採用する。サンプリング間隔をとおくと、個の値を保存することになる。
なぜサンプリングした値だけで成立するのかというと、LF-mappingが使えるからだ。LFは文字列位置を一個ずらす操作であることを思い出してほしい。LFを何度も計算していれば、いつかはサンプリングされた接尾辞配列の値に行き着く。つまり、]が求めたい値だったとき、に対して繰り返しLFを適用し、がサンプリングされた値だった時点で、その値にを足したが求めるの値になるわけだ。このようにしてを求める過程をの全てで行うので、パターンの出現回数をとしたとき、LFを合計で回行うことになる。
時間計算量
である。
よって、countには時間かかる。 一方、locateには時間かかる。
空間計算量
のサンプルも加えてビット必要になる。
おわりに
FM-indexがどうやってcountとlocateを実現しているか詳細に説明してきた。なお、この実装では二点注意するべきところがある。
- 経験エントロピーという値は頭打ちになる。つまり、巨大で冗長な文字列に対して構築するFM-indexは、およそに比例して大きいサイズになってしまう。もっと圧縮したい。
- locateの時間計算量を保証するためには、の一定間隔サンプルを保存する必要がある。これもに比例して大きくなってしまう。一定間隔でのサンプリングを使わないアイディアが欲しい。
次回は、どのようにこれらの問題を解決するかを説明していきたい。その議論は、最新の索引であるr-indexにつながっていく。
補足
ちなみに、FM-indexにちょっと構造を加えるだけで元文字列へのランダムアクセスも可能になる。もちろん、そのものは持っている必要はないので捨ててしまって構わない。
まず、逆接尾辞配列を考える。これは文字通り接尾辞配列の逆でを満たすものだ。意味合いとしては、「文字列位置を与えられると、そこから始まる接尾辞の辞書順を返す配列」となる。がの順列であることから、からへの関数として見ると全単射であるため、は必ず存在する。 このについても一定間隔のサンプルを保存する。
にアクセスしたいときは、まずサンプルのうちの直後にあるものを見つけてとする。それに回LFを適用した値が、位置から始まる接尾辞の辞書順となっている。したがってそこのBWTの値が求めたいだ。おしまい。
時間計算量は、LFを回適用することから時間である。なお、部分文字列に一気にアクセスするときには、いちいち全ての文字で回LFを計算する必要がないので時間となる。
空間計算量は、これまで述べてきたFM-indexに追加のサンプルビットを加えたものとなる。
*1:Burrows, Michael, and David J. Wheeler. "A block-sorting lossless data compression algorithm." (1994).
*2:Gagie, Travis, Gonzalo Navarro, and Nicola Prezza. "Fully functional suffix trees and optimal text searching in BWT-runs bounded space." Journal of the ACM (JACM) 67.1 (2020): 1-54.
*3:Ferragina, Paolo, and Giovanni Manzini. "Indexing compressed text." Journal of the ACM (JACM) 52.4 (2005): 552-581.
*4:Grossi, R. "Higher order entropy analysis of compressed suffix arrays." DIMACS Workshop on Data Compression in Networks and Applications, 2003. 2003.
*5:厳密に言うと、これはMultiary wavelet tree(多分岐ウェーブレット木)を使った場合の最新の成果のはず