コードゴルフ

Latest Author %20%20 /Date 2020-01-19 23:32:04 / Views 4539
8 (Favした一覧ページはユーザーページから)

コードゴルフとは

与えられた条件を満たすプログラムをなるべく少ない文字数で書く競技です。ショートコーディングと呼ばれる場合や、ゴルフと略される場合があります。
競プロの問題は、入力の形式・制約・出力の形式・実行時間制限などの条件がきちんと定まっているので、そのままゴルフの問題として流用することができます。
その上、競プロの問題は、解くのに必要な実装量がさほど多くない(ことが多い)ので、ゴルフと相性が良いです。

yukicoderにおけるコードゴルフ

yukicoderでは、出題済みの各問題(「スコア問題」および「その他の問題」は除く)について、最短ACコード(これをyukicoderでは「ショートコード」と呼んでいます)の提出者に対してゆるふわポイントが付与されます。
最短ACコードが複数ある場合は、それらの中で最も早いタイミングで提出されたコードのみをショートコードとし、その提出者に対してのみ、ゆるふわポイントが付与されます。

解説放送ではその時点でのショートコードが紹介されます。

問題毎のショートコードの一覧は以下のページで見ることができます(左のメニューの「ランキング」からこれらのページへ移動できます)。

その時点でのショートコードよりも短いコードを提出しACすれば、そのコードが新たなショートコードとなり、ゆるふわポイントを奪取することができます。
ショートコードの保持者が変わったことについて知りたいならば、以下のbotをフォローすると便利です。

コードを書く前に

実際に短いコードを書く前に知っておくべきことがいくつかあります。

強い言語を使う

言語別ゴルフの場合は使いたい言語を使えばよいのですが、yukicoderのショートコードのような多言語混合ゴルフでは「強い言語」を使う必要があります。
「強い言語」とは、主に以下の言語です。競プロerに人気のC++は、ゴルフでは弱いです。

  • Perl(大抵のことは短く書ける。実行速度が遅いので問題によってはTLEになることもある。)
  • Ruby(大抵のことは短く書ける。実行速度はPerlよりは速いが問題によってはTLEになることもある。)
  • C(実行速度が速い言語の中では短く書ける言語である。)
  • cLay(C++っぽい言語をC++に変換する言語で、Cより短く書ける。yukicoder以外のオンラインジャッジでは使えない。)
  • Bash(dcやsedを呼び出すと短く書ける。それら以外にも競プロの問題でゴルフする際に相性の良いものはある。)
  • Perl6(Perlとは別の言語である。実行速度が非常に遅いが、とても短く書ける方法が色々ある。)

ジャッジ環境においてACとみなされる条件を把握しておく

ジャッジ結果がACになるのであれば何でもあり(ACにならないならどんなに良さそうでもダメ)なので、ジャッジがどうなっているのかを知るのは重要です。
ジャッジ環境の言語のバージョンには注意しましょう。
yukicoderでは以下の点に注意するとよいでしょう。

  • ユーザーの出力の行頭と行末のスペースを取り除いた結果が想定出力と一致すればACとなる。
  • 「最後に改行してください」とあるが、改行しなくてもよい。
  • 本来の出力の後に余計なスペースおよび改行があってもよい。この仕様は修正されました。
  • 出力が正しくてもREになってはいけない。
  • 誤差を許容する問題では、指数表記で出力しても値として正しければよい。
  • スペシャルジャッジでは、問題文で指定されている出力形式よりも実際にACとなる出力形式が緩いことが多い(改行区切り・スペース区切りのどちらでもよい、など)。ジャッジコードを読んで実際のAC判定がどうなっているのかを確認するとよい。
  • WAになるような入力ケースがたまたまテストケースに含まれていなかったためにACとなった場合(いわゆる嘘解法)は後から落とされる可能性がある。

縮め方

以下では、実際にコードを縮める上でよくある書き方や変形方法などについて述べます。

変数/宣言

特別な理由がない限り、変数名は1文字にします。

Perl

$に続いて変数名を書くとスカラー変数になります。

$a              # aという名前の変数です。
${a}            # こうも書けます。
${'a'}          # ${式}でもよいです。
${print'!';'a'} # 余計なこともできます。
$b='a';${$b}    # $bという名前の変数です。$bの値が'a'なので$aです。
$b='a';$$b      # この場合は{}を省略できます。

変数を宣言せずに使えます。宣言しなかった場合、グローバル変数になります。スコープを制限したいときには宣言が必要です。

宣言したかどうかにかかわらず、変数の初期値はundefです。これは、数値として評価すれば0、文字列として評価すれば空文字列、真偽値として評価すれば偽です。

@に続いて変数名を書くと配列変数になります。配列も宣言せずに使えます。初期値は空リストです。

@a              # aという名前の配列です。
@{a}            # こうも書けます。
@{'a'}          # @{式}でもよいです。
@{print'!';'a'} # 余計なこともできます。
$b='a';@{$b}    # $bという名前の配列です。$bの値が'a'なので@aです。
$b='a';@$b      # この場合は{}を省略できます。

スカラー変数$aと配列変数@aは異なるものなので、1つのコード内で両方を使うことができます。

配列の1つの要素にアクセスするには$a[0]のように書きます。これは配列変数@aの0番目の要素であって、スカラー変数の$aとは無関係です。[]内はスカラーコンテキストで、整数値でないものは整数値に変換されます。

配列の複数の要素にアクセスするには@a[0,1,2]のように書きます。[]内はリストコンテキストで、整数値でないものは整数値に変換されます。

""で囲われた文字列では変数展開されます。配列の先頭要素を数値として使う際に、これを利用して縮めることができます。

@a=(3,1,4,1,5);print$a[0]+5
@a=(3,1,4,1,5);print"@a"+5

"@a"は変数展開されて"3 1 4 1 5"という文字列になります。この文字列は数値として評価すると3になります。

特殊変数

特殊変数と呼ばれるものが多数あり、それらを使いこなすのはゴルフでは重要です。ゴルフでよく使う特殊変数は下表の通りです。

ここに載っていない情報を詳細に知りたければperldoc.jpなどを参照してください。

特殊変数初期値説明
$_任意undef無名変数です。さまざまな場面で暗黙に使われます。
$.任意→int64undef標準入力から読み込んだ行数です。読み込む前は型が決まっていません。
入力が1行しかない数え上げの問題では初期値の1として使えます。
$%int640この変数に代入すると整数に変換されます。
$=int6460この変数に代入すると整数に変換されます。
$-uint630この変数に代入すると非負整数に変換されます。
負数を代入すると0になるので、0とのmaxをとるのに使えます。
$^Tint64またはint3216億ぐらい$^Tと書かずに$と書けます(の文字コードは20です)。
初期値はプログラム実行開始時刻(1970年1月1日からの経過時間)です。
この変数に代入すると整数に変換されます。初期値が大きいので最小値を求めるのに使えます。
$^Fint322$^Fと書かずに$と書けます(の文字コードは6です)。
この変数に代入すると整数に変換されます。初期値が2なのでたまに便利です。
$|uint10この変数に代入すると、一旦整数に変換された後、それが0でなければ1に変換されます。
--$|と書くと、0なら1(-1は0でないので1に変換される)、1なら0に変わるので、0と1を交互に使いたいときに使えます。
最初だけ違う処理をしたいときには$|++が使えます(2は0でないので1に変換される)。
$/文字列"\n"入力の行の終端を定義します。通常は「行」といえば"\n"が現れるまでの部分を指しますが、この変数の値を変えることで、例えば" "までを1行とすることもできます。
初期値が改行なので改行を出力するときにもよく使います。
$"文字列" "配列を""で囲んで文字列にするときに要素間を何でjoinするかを定義します。
初期値が空白なので、入力をsplitするときに<>=~$"の形で非常によく使います。
$,文字列undefprintで出力するときに要素間を何でjoinするかを定義します。
この変数の値を書き換えるとすれば大抵は$,=$"$,=$/だと思います。
$\文字列undefprintで出力するときに末尾に何を追加するかを定義します。
改行を出力しなくてもよい問題で、「答え」をこの変数に入れておく使い方をします。
$`read-only文字列undef正規表現にマッチした部分よりも前の文字列です。
$&read-only文字列undef正規表現にマッチした部分の文字列です。
$'read-only文字列undef正規表現にマッチした部分よりも後の文字列です。
空白区切りで2つの入力が与えられたときに、<>=~$"した後、右側の値を取り出すために使います。
$1,$2,$3,...read-only文字列undef正規表現のn番目の()にマッチした部分の文字列が$nです。
$#hogeint32-1正確には特殊変数ではないですが、変数のように扱うことができます。
@hogeの最終インデックス(要素数-1)です。負数を代入すると-1になります。
配列自動拡張機能を利用して最大値を求めるのに使えます。
pop@aの代わりに--$#aと書いた方が短くなる場合があります。
C
// (1)
main(){int a=0;...
a;main(){...

// (2)
int f(int a,int b){...
f(a,b){...

// (3)
char a[99],*b;
char*b,a[99];

(1)グローバル変数は0で初期化されます。int型のグローバル変数は宣言する際に型を省略できます。

(2)int型を返す関数の返り値の型は省略できます。引数が全てint型であるような関数の引数の型は省略できます。

(3)ポインタ変数とポインタでない変数を宣言する場合はポインタ変数を先に書いた方が短いです。

分岐

通常のプログラミングでは分岐といえばif文ないしif-else文を使うのが常識ですが、ゴルフでは?:,&&,||を使うのが常識です(言語によっては他の分岐方法もあります)。

A?B:Cは、Aが真のときBが、Aが偽のときCが実行されるので、if-else文の代わりになります。

A&&Bは、Aが真のときに限り、Bが実行されるので、if文の代わりになります。
A&&Bの真偽値を知りたいのではなく、Aの真偽値によってBが実行されるかどうかが決まる、ということです。
これは短絡評価と呼ばれる性質を利用したものです。

A||Bは、Aが偽のときに限り、Bが実行されるので、これもif文の代わりになります。

分岐する際には比較演算をすることが多いですが、そのときに、真偽を反転させるようにすると縮む場合があります。
例えば、if(a<=b)Ca<=b&&Cと書けて、真偽を反転することで、a>b||Cと書けます(同様に、a<=b?C:Da>b?D:Cと書けます)。

a<=b  →  a>b
a>=b  →  a<b
a==b  →  a-b  (Ruby以外の多くの言語では数値の真偽値は0なら偽、0でないなら真)

整数変数と定数の比較でも同様に長い演算子の代わりに短い演算子を使うことで縮む場合があります。

a<=5  →  a<6
a>=5  →  a>4

比較演算は分岐の条件として使われることが多いですが、比較によって得られた真偽値を整数値として使うこともあります(Ruby以外)。
例えばx+=a<=ba>b||++xよりも短いです。

ループ

C

Cのループ構文にはwhileforがありますが、forの方が短い(または同じ)です。次の3つのコードは基本的に同じです(A,B,C,Dは何らかの式とします)。

A;while(B){C;D;}
A;while(B)C,D;
for(A;B;D)C;

whileの繰り返す部分が単文のとき、ブロック{}は省略できます。これはforifでも同様です。;,に変えることで、通常は複数の文に分けて書くような部分を単文にすることができます。

forA,C,Dには何を書いてもよいので、whileで書けるものは必ずforでも書けます。Dにインクリメントやデクリメント以外の式を書くこともできます。Bの部分で,を使うこともあります。

型変換

Perl

Perlでは演算子によって項の解釈が定まることがほとんどなので、明示的に型変換する必要がある場面は他の言語に比べれば少ないものの、必要な場合もあります。また、Perlにはスカラーコンテキストとリストコンテキストという概念があり、これの変換についてもここで扱います。

次のコードを見てください。

# (1)
print<>+0^<>+0
print<>+0^<>

# (2)
<>;print 0+grep$_>3,<>
<>;print~~grep$_>3,<>

# (3)
@a=map<>.'',1..<>
@a=map~~<>,1..<>

# (4)
print int<>/2
print<>/2|0
print<>>>1

# (5)
$x=<>+0
$x+=<>
$%=<>

(1)は、入力の1行目の値と2行目の値をxorした結果を表示するコードです。Perlの^には「非負整数同士での演算」「文字列同士での演算」の2種類があり、少なくとも一方が文字列(またはundef)でない場合は非負整数同士での演算になるので、0を足すことで数値に変換できます。少なくとも一方が文字列でなくなればよいので、1行目を型変換した場合、2行目に対しては明示的に型変換する必要はありません。

(2)は、入力の2行目以降に与えられた値のうち、3より大きいものの個数を表示するコードです。printの引数はリストコンテキストで評価されるので、print grepとしてしまうと、条件に当てはまる要素のリストを表示してしまいます。要素数を知るにはスカラーコンテキストに変換すればよいので、0を足すのでもよいですが、ビット反転~を2回使ったほうが短いです。

(3)は、入力の1行目に要素数Nが与えられて、そこからN行にわたって与えられる文字列を@aに代入するコードです。mapの第1引数はリストコンテキストで評価されるので、map<>,1..<>としてしまうと、<>が残りの入力の文字列のリストになってしまいます。スカラーコンテキストに変換すればよいので、空文字列を連結するのでもよいですが、文字列のビット反転~を2回使ったほうが短いです。

(4)は、入力の1行目の値を2で割った結果(切り捨て)を表示するコードです。Perlの割り算は整数同士の演算であっても結果が実数になる場合があります。実数を整数に変換するにはintを使います。非負実数を非負整数に変換する場合は0との論理和|をとるほうが短いです。2の整数乗で割って切り捨てをする場合は右シフト>>を使ったほうが短い場合があります。

(5)は、入力の1行目の値を代入するコードです。未定義の変数に対して、文字列を数値に変換して代入したい場合は、+=を使うと短く書けます。Perlには特殊変数というものがあります。一部の特殊変数は型が決まっていて、代入すると強制的に型変換されます。