AtCoder Beginner Contest 135 F - Strings of Eternity

 

atcoder.jp

 

※ほぼ解説丸写しです

 

解説と解説リンクのZ algorithmの記事を読んで勉強したので備忘録

 

問題:文字列s、tが与えられる。sを任意の回数連結してできる文字列に出てくる連続する部分列で、tをk回連結した文字列と一致するものはあるか。(k>=0)。kの最大値を求めよ

 

 

 

読解:

問題文を崩さない程度に書き下したんですけど、わかりづらいですね

前半はsを無限に繰り返しことを指しています。

そして、その中でtの繰り返しが含まれるか調べることになります。

 

 

 

解説解法:

天下り式ではありますが、tと一致するsの開始地点を全て調べれば答えが求まります。

tと一致する開始地点というのは

sのi文字目~i+|t|-1文字目==tの1文字目~|t|文字目が一致することを指します

s[i:i+len(t)]==t

とも書けますね。ここでのsは、元のsを無限に繰り返しているとします。

 

 

 

この開始地点がわかれば、開始地点→終了地点+1を次の開始地点とする。と順に調べていき、最も長い場所を見つければよいです。ループがあれば無限にtを並べることができます。

 

 

はじめにsを十分に繰り返して並べ、文字列ssを作っておきましょう。どのくらいにすればよいでしょうか。

 

sの最後の文字から|t|文字調べるためlen(ss)>=len(s)+len(t)になれば十分です。

 

では次に、tと一致するssの連続する部分文字列を全て求める必要があります。

全探索ではO(len(s)*len(t))で間に合いません。

効率よく求めるアルゴリズムとしてKMP法とZ algorithmが挙げられていました。自分はZ algorithmを勉強したのでまとめておきます。

 

 

 

Z algorithm

 

www.geeksforgeeks.org

 

このリンクをほぼ写しただけになってます。リンク内のコードを読むのが一番わかりやすくて手っ取り早いです

 

 

Z algorithmとは?

 

ある開始地点iから、接頭辞と一致する長さを求めるアルゴリズムです。

 

文字列str=”abcaaabc”を考えると

Z[0]=str.size():先頭なので当然すべて一致します。Z algorithmでは考えません

Z[1]=0:1文字目bは、先頭のaと一致しないので0です

Z[3]=1:3文字目aは先頭のaと一致します。次の文字aは2文字目のbと一致しないのでZ[3]は1です

Z[5]=3:5~7文字目はabcと、先頭3文字と一致するので3になります

 

 

このように定義された大きさstr.size()の配列ZをO(str.size())で求めることができるのが、Z algorithmです

 

 

本問ではtとssを出現しない区切り文字を間に入れて連結します

t+'$'+ss

こうすることで、接頭辞tと一致する箇所をO(|t|+|ss|)で見つけることができます

Z[i]==|t|なら一致します。

$はtにもssにも出現しないので、Z[i]>|t|にはなりません

 

 

読んだ感じ、上の元の考え方より、下の繋げて一致する場所を探すほうが使いそうですね。知らんけど

 

 

アルゴリズム

1.0から順に調べる。いま区間 [ L,R ) が一致しているとして、Rを右側に増やしていく。はじめは[0,0)。iを調べ終わった後、L,Rを初期化せずにi+1に進みます

 

 

 

2.もし、iがRより小さい。iを既に調べているとき

k=i-Lとすると下に示した部分文字列が一致しています

 

L…i…R-1

0…k…R-L-1

     0...Z[k]

 

L~R-1は①で調べた通り、先頭と一致します

またk~は、長さZ[k]だけ先頭と一致します。

 

 

2-1.Z[k]<R-L-1-k+1=R-i

上の2行目と3行目を比べたとき、3行目のほうが短いことを指します

このときZ[k]+1~R-L-1は、①でkを調べたときに既に調べています。L~R-1が一致するので確かです。

 

したがって、Z[k]がZ[k]+1文字目で不一致だと言っているので、もう一度調べる必要がなく

Z[i]=Z[k]

が導けます

 

 

2-2.Z[k]>=R-i

このとき、Z[k]からR-L文字目とR-L-1-k文字目は一致することがわかります。(ただしR-LがZ[k]以下ならば)

しかし、R-L文字目と、R文字目は違うため、R文字目からは未知です。

なので①と同じ同様にしてRを伸ばして調べていきます

 

おそらくですが、Z[k]>R-iならば、Z[i]=R-iになりそうな気がします。

Z[k]==R-iのとき、調べる必要がありそうですね。

わけても計算量はほぼ変わらないのでわける意味がなさそうです。もしくは自分がわからないところでコーナーがありそう(多分後者)

 

 

 

 

3.i>Rのとき

これは、Z[i-1]=0だったりすると起こります。

L=R=iで初期化して①の操作をします。

 

 

 

全てのiで1~3をすればよいです。

以上がZ algorithmです。わかってしまえば実装量は多くないので楽でした

 

 

 

 

 

本問の実装

この問題にZ algorithmをどう適用したかを書いて終わろうと思います

 

・|ss|>=|s|+|t|となるsの繰り返しssを求める

・str=t+'$'+ssとその長さの配列Zを用意。Z algorithmを使う

 

このあと、有向辺(u,v)を作ります。

Z[(u-|t|-1)%|s|]=|t|となるuで、v=(u+|t|-|t|-1)%|s|です

ここでssのindexとZのindexが|t|+1個ズレていることに注意

 

sに落とし込むことでループを見つけやすくしています。

また、同じ辺から2つの別の頂点に辺ができることはありません。

 

 

 

最長パスを求める過程ですが、全探索っぽいですかね。名前つけられない

dist=[iを最後のtとするtの繰り返し回数]

初期値-1です。

visited=[false]*|s|

探索済みかどうかを記録します

 

for (i=0~s.size){ 

  探索済みならcontinue

  探索済みに変える  

  もし、出る辺がないならdist[i]=-1のまま 

  あるならdist[i]=0

  int now=i;

  while (辺があるなら){

    もし最初のiに戻れば無限

 

    距離を大きい方に更新できるなら更新して

    now=edge[now]と更新

    探索済みに変える

 

 更新できないならbreak;

  }

}

 

基本は距離更新したときだけ進むだけです。最悪ケースでも2|s|くらいのループで済んでると思います、たぶん

 

 

最後に、distの最大値を出力して終わりです。

 

 

 

 

 

感想:

Z algorithm面白かったです。なんかよく使いそうだけど、自分じゃ絶対思いつけないみたいなアルゴリズム良いですね

まあ名前ついてるアルゴリズムって大体そうだと思いますけど

 

まだ定着してないのでそらで書けるようにしときたいです。おつしゅ