るびまゴルフ 【第 7 回】

書いた人:浜地慎一郎
編集:うえだ

はじめに

この連載ではゴルフについて扱います。ゴルフと言っても本当のゴルフではなく、コードを短く書くことを競うコードゴルフです。ゴルフについて詳しくは以下をご参照下さい。

簡単な問題を出題して、次回でその解答を解説しつつまた出題、というサイクルで進めて行きます。解けた人はこのページの下部にあるコメント欄でブログのURLを書いていただければ存在がアピールできるかもしれません。コードを縮めても特にいいことはありませんが、ちょっとしたパズルとして楽しんでいただけたら良いなと思います。

前回の問題の解答と解説

前回の問題は Unix の seq コマンドのようなものを実装する、というものでした。入力は

3,5

というようにカンマ区切りで標準入力から与えられ、これに対して

3
4
5

というように出力すれば良いという問題でした。また、おまけとして入力が空白で区切られている場合、オリジナルの seq と同様に ARGV から渡される場合、についても考えていただきました。以下では

  • カンマ区切りのもの …… (1)
  • 空白区切りのもの  …… (2)
  • ARGV 渡しのもの  …… (3)

と表記することにします。

Range

今回の問題は、少し試してみると解答例として出題時に紹介した

a,b=gets.split(',').map{|v|v.to_i}
a.upto(b){|i|puts i}

のような upto や times を使う方法ではなかなか縮まないことに気付くのではないでしょうか。実際、コメントを頂いた皆さんはなんらかの形で Range クラスを使っておられました。

Range を使った最もわかりやすい形としては、uguisu_an さん

gets=~/,/;puts ($`.to_i..$'.to_i).to_a  # 38Bytes for (1)
gets=~/\s/;puts ($`.to_i..$'.to_i).to_a  # 39Bytes for (2)
a,b=$*;puts (a..b).to_a  # 23Bytes for (3)

edvakf さん

gets.chop=~/,/;puts *$`..$'  # 27Bytes for (1)
a,b=$*;puts *a..b  # 17Bytes for (3)

がありました。どちらも発想は近く、正規表現 /,/ でマッチさせて左側と右側をそれぞれ $` と $' で取ってきています。ただし入力に改行が入っているために文字列のまま '3'..'7' などとすることができず、 uguisu_an さんの方は to_i が二度入ってしまっているのが残念なところです。 (3) の方は $* が ARGV と同義であることがわかればすぐに理解できるかと思います。

edvakf さんはもう一つ、前置 * を使って .to_a を代用する工夫もしておられます。この演算は前回も紹介したのですが、今回は関数の引数を展開する意味で使われていて、厳密には .to_a とは違う動作になります。例えば puts *'3'..'5' であれば

puts ('3'..'5').to_a

ではなく

puts '3','4','5'

と同じ意味となっています。このケースではこの違いは特に関係ありませんが、後の例ではこの違いが効いてきます。

uguisu_an さんや edvakf さんの解答はゴルフとしてはまだ縮められるものの、日常的なワンライナーでも使えそうな感じで、実用性があって良いのではないかと思います。また、今回入力の末尾に改行があるかどうかを指定し忘れていたのですが、改行が無いとすると edvakf さんの (1) への解答は .chop を外すことができ、22Bytes とかなり短くなります。

Range.new を使う方法

grafi さんからは Range.new を用いた解答をいただいたのですが、これは完全に想定していなかった方法だったので面白いなぁと思いました。クラス名や new がゴルフで短いコードとして登場するのは極めて稀だからです。特に、ある程度慣れているゴルファーだとなかなか思いつけない方法なのではないかと思います。

さて、具体的には grafi さんのコードは

p *(eval"Range.new "+gets)  # 26Bytes for (1)
eval"p *Range.new(#{$*})"  # 25Bytes for (3)

というものでした*1。入力に入っているカンマをうまく使ってやり、例えば 3,5 というような入力に対して

p *(Range.new 3,5)

というようなコードが eval されるようになっています。

(2)、(3) については、 Range.new を使う場合は eval を使わない方が短くなるのではないかと思います。

puts *Range.new(*gets.split)  # 28Bytes for (2)
puts *Range.new(*$*)  # 20Bytes for (3)

こういう意外な解法が気持ちよくハマるシーンがたまにあるのもゴルフの面白いところではないかと思います。

eval を使いこなす ( sub 編)

Range.new の解答にも登場していましたが、今回のメインテーマは eval を使いこなすことでした。 eval は通常のプログラムであればコードを読みにくくしてしまったりしますが、ゴルフでは時として強力な武器となります。特にポイントとなるのは、 eval が .to_i を兼ねることができることと、複数の値の .to_i を兼ねたり、入力の文字をそのまま流用できたりすることです。流用できない文字がある場合は、いかに短く eval できる形に変換できるかがポイントとなります。

まず (1)、(2) の解答を見ていきます。これらは入力に含まれるカンマや空白の処理がポイントになります。みしょさんは String#[] を右辺値として使う少し変わったテクニックでこれらを処理されていました。

gets[/,/]='..';$><<[*eval($_)]*$/  # 33bytes for (1)
gets[/ /]='..';$><<[*eval($_)]*$/  # 33bytes for (2)

また、maraigue さんは String#sub を使っておられました。

puts eval(gets.sub",","..").map  # 31Bytes for (1)
puts eval(gets.sub" ","..").map  # 31Bytes for (2)

みしょさんはこの maraigue さんの解答を参考にさらに短いコードを発見されていました。

puts *eval(gets.sub",","..") # 28bytes for (1)
puts *eval(gets.sub" ","..") # 28bytes for (2)

さらにこれは eval してる関係で String ではなく Fixnum の Range ができていることと、この * は先程少し説明した通り、 .to_a としてではなく、関数の引数を展開する意味で使われているため、 puts のかわりに p が使えて

p *eval(gets.sub",","..") # 25bytes for (1)
p *eval(gets.sub" ","..") # 25bytes for (2)

とすることができます。 (1) に関してはこれが最短コードではないかと思っていました。

eval を使いこなす ( split と join 編)

idesakuさんは split を使ってカンマや空白を処理されていました。

eval"p *%d..%d"%gets.split(',')  # 31Bytes for (1)
eval"p *%d..%d"%gets.split  # 26Bytes for (2)
eval"p *%d..%d"%$*  # 18Bytes for (3)

この方法だと三つの問題をほぼ同型で解けていて、わかりやすいです。このように入力を split で分割してから String#% でコード中の置きたい位置に %d などで配置する方法は汎用性が高く、様々な問題で時々有効な手法だったりします。

ただ、今回はこのような汎用性は必要なく、 String#join と同義である String#* を使うと短くすることができます。 String#* はみしょさんが maraigue さんのコードを参考にした (3) への解答として使用されていました。

puts *eval($**'..')  # 19Bytes for (3)

これの puts を p に変えたものが (3) の最短だと思います。

p *eval($**'..')  # 16Bytes for (3)

この String#split と String#* を (2) に応用すると、 (2) は String#sub を使った 25Bytes より短くなり

p *eval(gets.split*'..')  # 24Bytes for (2)

という最短のコードができます。

……と、思っていた時代もありました

こんなところかなぁと思いつつ、私のゴルフ場にも同じ問題を出題した後にこの原稿を書いていたのですが、プロゴルファーの eban さんが (1) に対して 23Bytes という記録を出してしまいました。そのコードは

eval"A,B="+gets;p *A..B  # 23Bytes for (1)

というものでした。 (1) は入力にカンマがあるので今まではこれをうまく ".." に変換することを考えていたのですが、eban さんのコードはこのカンマをうまくそのまま生かすものでした。このように eval を使うコードでは入力を変換するよりもそのまま利用した方が良いケースが多いです。 Range.new の章で紹介した、 'Range.new '+gets を eval する手法もカンマをうまく生かしていると言えます。

なお A、B が大文字になっているのは a、b だと eval の作るスコープの外からアクセスできないため、定数にしてスコープを越えられるようにするためです。

今回のまとめ

今回は eval をメインテーマとして問題を考えてみました。 eval は普通のプログラミングで使用するとメンテナンスの難しいコードになってしまいがちですが、ゴルフでは強力な武器となります。今回ポイントとなったのは

  • eval は入力に対する .to_i を兼ねることができる
  • 入力にある文字はなるべくそのまま eval で読むようにする
  • 生かせない場合はなるべく短いコードで eval で読めるコードに変換する

というあたりかと思います。

eval を使うような問題の場合、自由度が高くなるために単純な問題でも最短がわかりにくくなりがちです。今回はさらに Range.new という意外なものが健闘したため、比較的簡単な問題を中心に出題しているるびまゴルフとしては珍しく、プロゴルファーも楽しめる問題となったようです。

今回の問題

一行の英単語が標準入力から与えられて、その末尾から 1Byte ずつ伸ばしていく形で文字数分の行を出力して下さい。具体的には

hoge

という入力に対して

e
ge
oge
hoge

と出力して下さい。入力は末尾に改行があるとしても無いとしても構いません。両方別々に解いていただいても良いですし、両方同時に解けるコードを考えていただくのも良いかと思います。入力の一行にはアルファベットや数値しか含まれないと仮定して良いことにします。例えば、入力末尾に改行が無いとすると

l=gets
l.size.times{|i|puts l[-i-1,i+1]}

が解答例となります。パーは 35Bytes とします。

Special Thanks

今回の問題は、テーマは考えてあったのですが具体的な問題を思いつかなかったため、にはさんに問題を考えてもらいました。ありがとうございます!

著者について

浜地慎一郎。ゴルフ場を経営しています。

解答コメント

解答をブログに書かれた方は、記事の URL と何か一言をコメントしていただけると嬉しいです。

※ コメントスパムが非常に多くなっているため、一定期間経過後にコメント受付を終了します。ご容赦下さい。

(comment plugin is disabled).




Last modified:2009/09/13 10:33:55
Keyword(s):
References:[Rubyist Magazine 0027 号] [0027 号 巻頭言] [るびまゴルフ 【最終回】] [各号目次] [prep-0027]

*1 (3) の解は ARGV[0] から 3,5 という文字列が渡ってくる想定のようです。