naoya_t@hatenablog

いわゆるチラシノウラであります

Suffix Array(接尾辞配列)とLCP(最長共通接頭辞)arrayを線形時間で構築する方法を調べてみた 〜SA-ISとKasai's algorithm〜

suffix arrayは知ってます。(当然)
(知ったきっかけは何だったかな…高林さんのsaryかな)
以前、英辞書検索用のCLIツールを作る時などにも使いました。

でも今までずっと、文字列のポインタを1つずつずらした(文字列の長さ分の)char**配列を用意してqsortでstrcmp的なものを渡すような、愚直な(O(N^2\log N)の)書き方しかしたことがありませんでした。

あまりそれで困るような場面に遭遇したことがなかったということでもありますが、
競プロで使うとなるとそういうわけにはいきません。

制約がN=|S|=10^5とかだともう無理ですよね。

LCP(最長共通プレフィックス)array もそうです。
suffix arrayを作った後に、隣り合う項目同士の最長共通プレフィックスを求めておくやつです。
隣同士の最長共通プレフィックスのリストがあると、あとはRMQとか使えば任意の2つのサフィックスの共通プレフィックスが求められたりして便利(らしい)です。
(配列の長さ-1)組の文字列の比較なので普通に考えたらO(N^2)かかります…よね*1

なのに、N=10^5なのにいかにもsuffix arrayとLCP(最長共通プレフィックス)arrayを計算して下さいと言わんばかりの問題*2が来て、上位陣はsuffix arrayで通しているというのです。

そこで

  • O(N)でLCP arrayが構築できる方法

について調べてみました。

上級者の皆様には「そんな事も知らなかったのか」「蟻本(上級編)に書いてあるぞ」「蟻本読め」「蟻本」と思われることでしょうが青コーダーレベルだと今までそれで困る場面がなかったということでお許しください。

Suffix Array

2009年に出てきたSA-ISというのが現在最強らしいです。

蟻本にも名前は出てきます。(p.337)

なお、接尾辞配列を構成するアルゴリズムは他にも多数存在しています。例えば、SA-ISと呼ばれるアルゴリズムなど、線形時間で非常に高速に構成するアルゴリズム等も存在していますが、プログラミングコンテストではほとんどの場合で上のアルゴリズム*3で十分です。

SA-IS

論文にCで書かれたコードが載ってます(そのまま動きます)。

仕組みについては日本語で書かれた解説記事があります。
めちゃ賢いアルゴリズムです。

これを使うとSPOJのSARRAYで満点が取れるらしいです。(※まだ試してない)

実装は元論文に載っているCで書かれたコードがそのまま動きます*5
秋葉さんの記事でおすすめのYuta Moriさんによる実装などもあります。

LCP array

笠井先生が考案した Kasai's algorithm というのがあって、O(N)で求めることができるのです。
なんということでしょう!
(※蟻本のpp.339-340にもあります。いかに蟻本を斜め読みしかしていないかが分かります。)

Kasai's algorithm

LCPが1以上の2つのサフィックスがあるときに、それぞれの2文字目以降のサフィックス同士のLCPは元の2つのサフィックスのLCPより1つ小さい

という事実を使って計算量を節約することができるというアイデアです。
で、それを最大限にやりたいので長い順に(=元の文字列の先頭から)見ていくことで、O(N)でLCP arrayが構築できてしまいます。

まとめ

以上によって suffix array と LCP array を線形時間で構築する問題を解くことができました。

いかがでしたか?

*1:蟻本上級編を読めというお叱りはあとで受けます。

*2:毎週日曜開催の某コンテストで最近そんな問題が出ました。

*3:Manber & Myersのアルゴリズム。O(N^2logN)のnaïveなやつ

*4:どうでもいいけど赤太字コメント超うける

*5:sにはsuffix arrayを構築したい文字列を、SAには結果を返すint配列のアドレス(サイズは後述のn)、nにはsentinelを考慮して文字列長+1を、Kにはアルファベットの文字数(unsigned charなら256)を、csにはsizeof(unsigned char)を渡します。