普通のゴルフは、ボールをカップに入れるまでのストローク数をいかに少なくするかを競います。code golf(コードゴルフ)は、目的の出力をするプログラムをいかにすくないストローク数で作成するかを競います。つまり、「ソースコードが短い方が勝ち」です。
そんなの面白いのか?と思われるかもしれませんが、やってみるとなかなか奥が深く、中毒性がありますよ。
このページでは、そんなcode golfのC言語での入門ページです。「C言語での」と書いた理由は、code golfは目的の出力が得られればよいわけですから、言語はなんでもよいのです。なので、このページでは「C言語でいかにコードを短縮するか」の入門をしていきます。
ちなみに、code golfの言語はなんでもよいと書きましたが、異なる言語間では勝負が成り立ちませんので、通常はC言語はC言語同士で勝負します(例えば、必ず main と書かねばならないC言語はperlやrubyなどに比べて不利)。柔道やレスリングの体重別階級のようなものですね。体重別に階級のあるスポーツと同じく、どんな言語を使ってもいいからとにかく短く!といった無差別級もあります。
注意! このページを作った人および、この後紹介するC言語でのcode golfコンテストが開かれているサイトでは、コンパイラに gcc を、CPUにx86互換のものを使っています。code golfはできる限り省略した書き方などをするため、非常にコンパイラ依存性が高いコードが多いです。ですから、VisualC++やBorlandC++Compilerではコンパイルできない、または実行結果が違うといったことがあるかもしれません。またCPU依存性が出る場合もあります。IntelのCPUのほとんど(Itanium, Itanium2以外)を32bitモードで使っている場合や、AMDのCPUを32bitモードで使っている場合はx86互換ですので問題ありません。PowerPCやOpteronを使っていたり、またCore2Duoを64bitモードで使っていたりすると動作が異なる可能性があります。
code golfはあまり「有用」ではないので、そのコンテストサイトは普通のプログラミングコンテストのサイトよりも残念ながら少ないです。
おそらく最大級だと思われるところは次のサイトです。
このサイトは各問題に対して無差別級のランキングと、言語別のランキングが表示されます。ただし、残念ながらC言語は扱っていません。
C言語を扱っていて、このページを作った人がよく遊んでいるのはこちらのサイトです。
このサイトはものすごくたくさんの言語を扱っています(2008年11月現在65言語)。サイトにも"The purpose of this server is not serious competition"(このサーバーは本気の競争を目的としたものではない)とあるように、さきほどのサイトほど縛りが厳格ではありません(例えば、ジョーク問題が多々あったり、正解すべき入力が全て分かっているのである特殊な場合だけできればよかったり、など)。しかし、中にはいろいろな要素がつまった良問もあり、やりがいがあります。
このページと同じくC言語でのcode golfを扱った本がすでにあります。Short Codingという本で、非常に評判がよいです(このページを作った人は貧乏なので買ってませんから、口コミ情報ですが)。
このページは「code gold入門」なので基礎的なところしか扱っていませんが、こちらの本は常人には考え付かないような、ものすごく高度なことをやっておられるそうです。ですから、このページを見てcode golfに興味が沸いた方はぜひとも読んでみるとよいと思います。このページを書いた人も近々購入しようと思っています。
それでは、実際にコード短縮の方法をやっていきます。
コード短縮の方法には、主に二つあります。
アルゴリズムの改善とは、例えば1からnまでの和を出せといわれたときに、
int i,s=0; for(i=1;i<=n;i++) s+=i; printf("%d",s);
とするのを、和の公式を使って
printf("%d",n*(n+1)/2);
とするようなものです(答えを出す「やり方」を変える)。
小手先の業とは、例えばさきほどのコードの for(...) の行を
for(i=n;i--;)
などとすることです。
この例でもそうですが、たいていの場合アルゴリズムを改善したほうが劇的にコードは短くなります。小手先の業では、もともとのコードの長さにもよりますが、せいぜい10%~40%程度しか減りません。しかし、小手先の業は「どんな問題にも普遍的に適用できる」という利点があります。アルゴリズムの改善はその問題ごとに考えていかねばなりません。なので、このページでは主に小手先の業を紹介し、アルゴリズムについては余裕があればケーススタディの形で取り扱いたいと思います。
これは基本中の基本です。いらないスペース、改行は全てなくしましょう。
ただし、このページでは読みやすさのためにコード例は全て改行とスペースを削らないで書きます。
C言語入門のときは「おまじない」といわれ、いつのまにか「ヘッダファイル(.h)をincludeすることでそのライブラリが使えるようになる」という理解にすりかわってしまいがちな #include ですが、実は「C言語ではプロトタイプ宣言は必要ない」ので、いりません(そもそも初期のCにはプロトタイプ宣言はありませんでした)。
じゃあなんのためにヘッダファイルをincludeしてるの?という話ですが、実はプロトタイプ宣言をしておくと、誤った型の引数で関数を呼び出した時などにチェックしてくれます。しかし、code golfではそんなことおかまいなしなのでincludeはしません。
ヘッダファイルをincludeする他の理由として、ヘッダファイルでdefineされている定数(例えばEOFやstdinなど)を使いたいこともあります。この場合EOFなどの決まった値はその値を直接書くことで解決します。stdinなどはなるべくその定数を明示しなくてもよい関数を利用するようにし、どうしても必要な場合にのみincludeします。(実はstdinといえどもファイルポインタ、突き詰めれば値なので、数字を直接書けば解決する(こともある)のですが、それはおいおいやっていきます。)
普段まじめにプログラムを書いている方は、main関数を
int main(){ ... }
あるいは
int main(void){ ... }
などと書いていると思います(返り値の型をintではなくてvoidとしてる方もおられるかもしれません)。
しかし、C言語の入門書の最初の方によく書いてあるように、int, voidを省略して
main(){ ... }
と書いても問題ありません。
さて、一つ前で関数の型が省略できると書きましたが、これは「型が省略されたグローバル識別子はint型だとみなされる」からです。ということは、変数も型を省略してオッケーということです。例えば、
main(){ int i,j; ... }
というコードは、
i,j; main(){ ... }
としてよいことになります。残念ながら型を省略できるのはグローバル変数のみですが、code golfでは読みやすさや安全性などは興味の対象ではありませんので、全ての変数をグローバルにしてしまえばよいです。
さらに、jをmainの仮引数にしてやると、こちらも型は省略できて、
i; main(j){ ... }
となって、iとjを結んでいた , の分が1文字へります。
ちなみに、「グローバル変数は0で初期化される」ので、こうすると後の0初期化用コードなども削れることがあります。ついでに、上の例の場合はjにはコマンドライン引数の数+1が入ります(int main(int argc, char *argv[]){ ... }としたときの argc と同じ)。
普通にプログラムを書く時、forループは
for(i=0;i<n;i++){ ... }
のように書くと思います。ここで、C言語の条件評価について思い出してみましょう。「0は偽、0以外は真」ですね。これを使って、
for(i=n;i;i--){ ... }
とすれば、1からnがnから0になったという違いはあるものの、ほぼ同じループをより短く表現できました。
しかし、iをグローバル変数にしていて0で初期化されている場合はこの必要はなく、 for(;i<n;i++) で充分ですね。上の例は例えば二重ループの内側のように毎回初期化が必要なときに用いると効果的です。
次もfor文についてです。ここではiが(グローバル変数にして)0で初期化されているとして、次のようなfor文を作ったとします。
for(;i<n;i++) printf("%d\n",i);
これを次のようにします。
for(;i<n;) printf("%d\n",i++);
これで1文字減り、出力もまったく同じです。このように、for文の一番最後のi++やi--は多くの場合省略できます。forに続く文中にiが登場しない場合でも、
for(;i<n;i++) printf("A\n");
を
for(;i++<n;) printf("A\n");
とすることで短縮できます。
複文とは、{ } で囲まれた文のことです。ここでもforを例にとります。例えば、次のようなコード
for(;i++<n;){ j--; printf("This is the %dth loop\n",i); }
があったとします。しかし、forの文法をよく思い出すと、
for(式;式;式) 文
なのです。しかし、通常は文1つでは収まらないので { } でくくって複数の文を1つまとめて(上の例のように)使っています。さて、つまり { } を省略するにはこの二つを1つの文にしてしまえばよいということです。この二つは式であり、「式はカンマでつないで連続して書ける」ので、
for(;i++<n;) j--,printf("This is the %dth loop\n",i);
とできます。
さて、式だ文だと難しそうなことを書きましたが、基本的に評価して結果が返るもの(変数に代入できるもの)は全て式、それ以外は文です。ちなみに、式⊂文ですので、式であれば同時に文でもあります。forの文法が
for(式;式;式) 文
なのに、文のところに式(j--,printf(...))を入れられるのはこのためです。(逆に、式のところに文は入れられず、if(...)は文(i=if(a==1);とかできない)なので、for(;if(a==1);){ ... } などとはできません。)
ややこしくなってしまいましたが、最初は「カンマでつないでみて、コンパイルエラーになったら諦める」くらいの気持ちでいいでしょう。
さて、さきほどの例ですが、実はもう1バイト減らせます。for(A;B;C) の C の部分を利用するのです。
for(;i++<n;printf("This is the %dth loop\n",i)) j--;
これでprintのセミコロンのぶんだけ減らすことができました。一般に、
for(a;b;) c,d;
を
for(a;b;d) c;
とすることで1バイト減らせます。ただし、dを入れるところが空欄でなかった場合、例えばeが入っていた場合には for(a;b;e,d) となって , が1文字増えるので結局同じ長さとなります。 このように、forはあいたところにいろいろ入れることができる(さらに、while()とfor(;;)で長さが同じ)なのでwhileよりも多用されます。
ifを使わずに条件分岐(のようなもの)を実現する方法は、二つあります。
?は普通の使い方と同様なので、簡単ですね。例えば
if(x>0) y=x; else y=-x;
を、
y=x>0?-x:x;
とします。通常のプログラミングなら見やすさのためにカッコをつけて (x>0?-x:x) とするところですが、つけなくてもプログラム的には問題ありません。
次に && または || を使う方法です。C言語のページにも書いてあるように、&&および||はそれ以上評価する必要がなくなるとそこで評価を中止します。このことを用いて、例えば
if(n>0) a+=n;
を、
n>0&&a+=n;
などとします。これは a+=n>0?n:0; より1文字短く、先ほどの ? とうまく使い分けてより短いコードを生成できます。
gccにはさまざまな拡張(標準のCにはない機能)があり、こちらのサイトに詳しくまとめられています。
数あるgcc拡張の中でも、特に有用なのは以下の二つです
です。前者は、例えばnが0以上に限られているとして
a=n?:-1; // nが非0ならa=n, そうでなければa=-1
などと使えそうです。
後者は
x=i>?-i;
のようにするとiの絶対値が標準ライブラリ関数のabs()を使うよりも1バイト短くもとまります。ただ、こちらはc++でしか使えないのが残念なところです。
続く
余裕があればやります