Suffix Automatonでもやもやした部分をなんか書いただけ
Suffix Automaton
Suffix Automatonを学ぼうとしてたらよくわからない部分があったので適当に書きます
https://cp-algorithms.com/string/suffix-automaton.html
を見たのでだいたいそこを読めばいいと思います
これ書きながら読み直したら全部ちゃんと書いてました(ア)
既にわかっているひとは何が言いたいかわかるかも、まあわかんないよね、ぐらいの気持ちで書いています
僕は自分の知識を共有して周りを強くしようという意志が殆どなく、そもそも誰かに読んでもらう気持ちで書いてないので(ア)
ですます調とかゴッチャゴチャなのも何も気にしていないからです
一度このデータ消えたので適当になりました
消えるまでは小学生が読んでもわかるとてもわかりやすい文章でできた記事だったのに...
Suffix Automatonってなに?
文字列のsuffixを受理するほにゃららオートマトン、受理状態を気にしなければ全部分文字列に対応する状態がある
それぞれの状態(頂点)からは次の文字に応じた状態への遷移があって、なかった場合は今見てる文字列の先頭を削るsuffix linkをたどる
とりあえず、上のURLでも導入されてるendpos
を導入します
として、一応 みたいなのも
例
S = "abcabcab"
endpos("abcabcab") = endpos("bcabcab") = endpos("cabcab") = {7}
endpos("abcab") = endpos("bcab") = endpos("cab") = {4, 7}
endpos("ab") = endpos("b") = {1, 4, 7}endpos("abcabc") = endpos("bcabc") = endpos("cabc") = {5}
endpos("abc") = endpos("bc") = endpos("c") = {2, 5}
これらの例で、どういうものかは分かると思います(適当)
また、定義とか、この例を見ると がわかります
ある文字列T'が現れる時、そのsuffixは必ずそこに現れるのでまず右が成り立ちます
逆に、出現位置の終端が等しいとすると後ろの方の文字が等しいことがわかり、包含関係にあるときサイズが小さい方が長いこともわかります(長い文字列のほうが短い文字列より出てきにくいからね)
どちらかがどちらかのsuffixであるとき、endposは必ず包含関係になることは少し考えると多分わかります
ここで、Suffix Automatonの状態はendposが等しい部分文字列の集合になっています
suffix linkは自分のsuffixで最も長いものにつけますが、先頭一文字削ったやつとかは同じ状態に入っていることもあるので
異なる状態に入る中で最も長いものにつけます
ちなみに、同じ状態に入っている文字列集合は一番長い文字列 T
と一番短い文字列を T'
見つけた時、Suf(T)
の中で T'
よりも長い文字列全てになります。
例を見るとわかると思います
Suffix Automaton の構築
構築部分で、いくつかなんかよくわからないね(考えてないので)みたいなところがありました
まあCorrectnessってところをちゃんと読みましょうなんですけど
一応次に構築方法の手順を書きます(URLのやつをわかりにくくしただけ)
まず各状態は文字ごとの遷移先(next[c]
)、suffix link(link
)、長さ(len
)を持ちます
構築方法としては、文字列の先頭から1文字ずつ追加していく感じなので現時点での文字列全体の状態(last
)も持ちます
オートマトンなので、最初は初期状態(頂点0)だけを用意しておき、初期状態のlinkは-1にしておくといいらしいです
cur
として状態を追加し、cur.len = last.len + 1
とします。また、追加する文字をc
としておきます(残りの決める必要があるものはlink
)p = last
として、次の処理を繰り返しますp == -1
であるとき、またはp.next[c]
が存在するなら繰り返しを抜けますp.next[c] = cur
としますp = p.link
とします
- ここで、
p == -1
であればcur.link = 0
として終了します q = p.next[c]
とします- もし
p.len + 1 == q.len
であればcur.link = q
として終了します clone
としてq
をコピーした後、clone.len = p.len+1
、cur.link = q.link = clone
としますp
からはじめて、p.next[c] == q
である間はp.next[c] = clone
としてlink
をたどることを繰り返します- 最後に
last = cur
として終了です
ここで、次のいくつかがよくわからなくなりました(頭がないので)
- なんで
p.next[c] = cur
としながらlinkをさかのぼっていっていいの? - なんで
p.len + 1 == q.len
だったら放置して終了して大丈夫なの? - なんで
clone
が必要なの? - なんで
clone
にnext
を張り替えるのはp.next[c] == q
である間だけでいいの?それともある場所以降は全部q
なの?
とりあえずこれらを僕が解消しようと試みるだけのアです
一応、S = "abcbcc"
に対してSuffix Automatonを構築する図を貼っておきます。
suffix linkを図示するの忘れてたけど見にくくなってしまうからということで...
そもそもそこまで説明に使わないし..
オートマトンの成長観察日記です。
頂点の数字はそこに属する文字列の中で最長のものの長さを示しています。
なんでp.next[c] = cur
とながらlinkをさかのぼっていっていいの?
現時点の文字列がS
で、そこにc
を付け足してS+c
のSuffix Automatonを作ろうとしていると考えます
すると、Suf(S)
は初期状態になるまでlink
をたどって状態に属する部分文字列を集めると出来上がる。(Suffix Linkをたどると最長のSuffixの状態に移動するので)
なのでそれらの状態からのc
による遷移をcur
に送るとcur
で全てのsuffixを受け持つことになる
全てのsuffixがcur
にあるので、suffix linkは頂点0に当然向かう
ここで、文字列S
によるSuffix Automatonは正しく構築されていることを仮定するので2つの状態に同じ文字列が属していることはない
ちなみに、後で議論するp.len + 1
とq.len
の関係について、p.len + 1 < q.len
となる遷移はここでのみ追加される
なんでp.len + 1 == q.len
だったら放置して終了して大丈夫なの?
ここでも同様にS+c
の考え方をしてみます
p.len + 1 == q.len
であるということはq
が文字を追加するときにできた状態か、clone
の状態である可能性があります
したがって、p
のsuffixにc
を追加した文字列の状態は全てq
を追加した際に解決されているのでこれ以上さかのぼる必要はありません
また、S+c
のsuffixのうちp
より長いものはcur
に属しているのでcur
からのsuffix linkはq
で大丈夫です
なんでclone
が必要なの?
clone
を追加するときはp.len + 1 < q.len
であるが、これはq
に属する文字列のうちp.len + 1
以下のものだけがほしいのにそれより長いものも取ってしまっていることを意味している
ここで、p.len + 1 < |T| <= q.len
だけq
に残し、clone
に(qに属するやつで最小の長さ) <= |T| <= p.len + 1
となるものが属するようにしたい
ひとまず、q
とclone
の関係だけ考えるとclone
はq
のsuffixなので、suffix linkはq
からclone
に向けてつなげる
clone
のsuffix linkについてはq
からもともとつながっていた状態がclone
のより短いものではもっとも長いのでclone
からつなげる
結論としては、q
だけだとp
にc
を追加するときにはq
よりもたくさん登場してる状態になっているからclone
で分ける必要がある
最初追加したときには、p
からのc
による遷移はq
のsuffixに完全に属しているのでのでendpos
がいい感じになっている
例えば上の観察日記の3日目と4日目を比べると、3日目で主鎖の2と書かれているところには"ab"と"b"
が属していてどちらもendpos = {1}
である
ただ、4日目になると"b"
のendpos = {1, 3}
となり"ab"
よりも多く登場するので分けなきゃいけない気持ちになる
そういうことです、多分(微妙になんかモヤモヤがあるけど多分無視できるレベルなので無視)
なんでclone
にnext
を張り替えるのはp.next[c] == q
である間だけでいいの?それともある場所以降は全部q
なの?
ある場所以降は全部q
なの? → そんなことはないです、多分
途中からsuffixが一致するようなものを考えるとなんか全部一緒にならないものもあると思うので(曖昧)
ただ、そうでなくても問題ないことを示せばいいので、お気持ちを書きます
p.next[c] == q
であるものが集まることでq
に属する文字列ができているので、これを集めてp.len + 1
以下の長さのものがclone
に属し、p.len + 1
よりも大きいものはq
に属したままになることがほしい状態です
ここで、q
に属するものより短い文字列は別の状態に属していて、それらはp.len + 1
よりも小さいので今の所同時に現れているので変更する必要がありません
これで疑問は解決していますが、ほしい状態にclone
とq
がなっているかを一応解決しておきます
p
からp.link
をたどって得られるsuffixはp.len
よりも短いものだけなのでclone
にはp.len + 1
以下でもともとq
に属していたものしか属しません
他の遷移はいじっていないのでq
には残りのp.len + 1
よりも長いものが属したままになっています
よって大丈夫そうであることがわかります
結論
ということでSuffix Automatonの構築でモヤモヤした部分のモヤモヤを少し解消した気持ちになりました
一応これぐらいで最低限は十分かという気持ちに少しなっています
実際にどう使うか知りたいですね、僕はまだわかりません(?)
Suffix ArrayのO(N)構築に比べ実装が圧倒的に軽いのでそこは強そうだけど、使うときに頭が必要そうで怖い
Suffix Arrayは適当にLCPとかこねてればある程度は大丈夫なイメージがあるので...