無駄の極み

千羽鶴って、貰って困るものランキングの上位ランカーだよな。 役に立たないし、捨てるにもなんだか躊躇いが感じられて。 って、貰ったことは無いんだけどさ。
寺や神社で定期的に古いお守りを処分するイベントがあるのは、処分するときの心理的な負担を軽減するためなのかもしれないな。 ま、寺社の側の主目的は、迷信グッズをまた新しく買わせることなんだろうけど。
+
先日の問題について考えてみる。
最長重複文字列(最も長く重複している部分文字列)を探すプログラムを作れ。 例えば
Ask not what your country can do for you, but what you can do for your country.
であれば 「 can do for you」 が最長重複文字列となる。
例文程度なら、何となく全体を眺めていれば見つかるだろう。 でも、こうした簡単な問題をいざプログラムで解決しようとすると、意外に面倒だったりするんだよな。
同じ文字列を探す。 文字通り、文字列は文字の列な訳だから、まずは単純に同じ文字を探すところから始まるだろう。 同じ文字を見つけて、もしあったら、そこから後ろに続く文字が同じ部分を取り出す。 さらに同じ文字があるか探して、もしあったらまた後ろに続く文字が同じ部分を取り出す。 先に取り出したのと長さを比べて、長い方を残す。 これを先頭の文字から繰り返して、最後に残ったのが答え。 あれ? 意外に簡単そうだな。
ということで、JavaScriptで上記をそのままコードにしてみた。
function search( s ) {
var answer = "";
for ( var i = 0; i < s.length - 1; i++ ) {
answer = getLonger( answer, subSearch( s.substring( i ) ) );
}
return answer;
}
function subSearch( s ) {
var answer = "";
for ( var i = 1; i < s.length - 1; i++ ) {
answer = getLonger( answer, getSame( s, getNext( s.substring( i ), s.charAt( 0 ) ) ) );
}
return answer;
}
function getNext( s, c ) {
var i = s.indexOf( c );
return i != -1 ? s.substring( i ) : "";
}
function getSame( s1, s2 ) {
for ( var i = 0; s1.charAt( i ) === s2.charAt( i ) && i < s2.length; i++ ) ;
return s1.substring( 0, i );
}
function getLonger( s1, s2 ) {
return s1.length >= s2.length ? s1 : s2;
}
本当に簡単だったな。 って、入門記事の例題なんだから簡単なのが当たり前か。 一応それぞれの関数の説明を残しておく。
- search( s )
- 文字列 s から最長重複文字列を取り出す。 関数名に「最長重複文字列」が見当たらないのは気にしない。
- subSearch( s )
- 文字列 s の先頭からと同じ並びの最長重複文字列を取り出す。
- getNext( s, c )
- 文字列 s から、文字 c で始まる文字列を取り出す。
- getSame( s1, s2 )
- 文字列 s1 と s2 の先頭から重複文字列を取り出す。 ここで s1 は s2 より長いものとする。
- getLonger( s1, s2 )
- 文字列 s1 と s2 で長い方を返す。
例題でテストすると、
search( "Ask not what your country can do for you, but what you can do for your country." );
ちゃんと 「 can do for you」 と答えが返ってくる。
返ってくるのだが、これは何だかモヤモヤするな。
モヤモヤするポイントの一つが、全部が同じ文字の場合。 例えば 「AAA〜A」 に適用した結果は、1文字少ない 「AA〜A」 になるのだが、これは最長重複文字列の定義に合っているのだろうか。 いや、合ってはいるんだろうが、何だか感情的に納得いかないというか…
モヤモヤポイントのもう一つが、各関数の中が for ばかりで、何か不細工っぽいところ。 扱う対象が文字列なので reduce や map といった便利な Array のメソッドが使えないからこうなっているのだが、いや、文字列を文字の配列に変換すれば使えるのだが、これはこれでまた何か違う気がするし。
せっかくなので、for を全部再帰で書き直してみた。
function search( s, answer ) {
if ( s.length <= answer.length ) {
return answer;
}
return search( s.substring( 1 ), getLonger( answer, subSearch( s, 1, answer ) ) );
}
function subSearch( s, i, answer ) {
var si = s.substring( i );
if ( si.length <= answer.length ) {
return answer;
}
return subSearch( s, i + 1, getLonger( answer, getSame( s, getNext( si, s.charAt( 0 ) ), 0 ) ) );
}
function getNext( s, c ) {
var i = s.indexOf( c );
return i != -1 ? s.substring( i ) : "";
}
function getSame( s1, s2, i ) {
if ( s1.charAt( i ) !== s2.charAt( i ) ) {
return s1.substring( 0, i );
}
return getSame( s1, s2, i + 1 );
}
function getLonger( s1, s2 ) {
return s1.length >= s2.length ? s1 : s2;
}
とりあえず for は消えたけど、今一つすっきりしないな。 見た目はむしろゴチャゴチャしてきた。 再帰の停止判定の効率を上げるためなんだけど、引数が増えたのもちょっと。 再帰版だと
search( "Ask not what your country can do for you, but what you can do for your country.", "" );
となるんだよね。 第2引数が微妙。 だからと言ってもう一段外側に関数を用意するのも、search の先頭で answer が undefined だったら空文字列にするとかも、どれも今一つ…
と思ったが、第2引数も使い道は有るな。 これがあることで 「ここに指定した文字列よりも長い最長重複文字列を取り出す」 という使い方が出来る。 これはこれで便利かもしれないな。 って、どれだけ便利だろうが、使う機会はほとんど無いんだけどさ。
来週、本屋で正解を確認しよう。 正解と言っても、雑誌の記事は関数型プログラミングの入門で言語は Haskel だったので、実装ではなく考え方が同じかどうかというレベルだけど。