私自身が表題の問題を解いた時のプログラムについて解説します。
問題の詳細は「取られたら取り返す!」(CodeIQ)を参照してください。
卓球では11点先取、バレーボールでは25点先取で1セットを取るようなルールがあります。
ただ、この得点よりも1点少ない得点以上の得点で同点となると「デュース」と呼ばれることがあり、その後は2点差を付けるまで続けられます。
(卓球やバドミントンにはデュースという言葉はありませんが、同様に進められます。)
A と B がn 点先取の試合を行ったとき、それぞれの点数が a 対 b になるまでの点数の推移を考えます。
例えば、n = 3, a = 4, b = 2 のとき、点数の推移は以下の6通りがあります。
(下記の記号は点数を取った側を表すものとします。)
(1) A -> A -> B -> B -> A -> A
(2) A -> B -> A -> B -> A -> A
(3) A -> B -> B -> A -> A -> A
(4) B -> A -> A -> B -> A -> A
(5) B -> A -> B -> A -> A -> A
(6) B -> B -> A -> A -> A -> A
このとき、以下のように点数が推移することはありません。
(途中で3点を先取してしまい、セットが終了するため)
A -> A -> B -> A -> B -> A
標準入力から n, a, b がスペース区切りで与えられるとき、点数の推移が何通りあるかを求め、標準出力に出力してください。
なお、n, a, b はいずれも0以上25以下の整数とします。
【入出力サンプル】
標準入力
3 4 2
標準出力
6
Rubyで解答しています。
#!/usr/bin/ruby
$Memo = {}
def solve(n,a,b,pa,pb)
return $Memo[[pa,pb]] if $Memo[[pa,pb]] != nil
if (a == pa) && (b == pb) then
return 1
else
if ((pa == n) && (pb <= n-2)) || ((pb == n) && (pa <= n-2)) then return 0
elsif ((pa > n) && (pa-pb >= 2)) || ((pb > n) && (pb-pa >= 2)) then return 0
elsif (pa > a) || (pb > b) then return 0
end
end
ret = 0
ret += solve(n,a,b,pa+1,pb)
ret += solve(n,a,b,pa,pb+1)
$Memo[[pa,pb]] = ret
return ret
end
# main
while line = gets
line.strip!
next if line.empty?
n,a,b = line.split.map{|v| v.to_i}
p solve(n,a,b,0,0)
end
再びメモ化再帰の問題です。
入力値を数値にしてsolve()に渡し、結果を印字します。
考え方に書いた通りの実装です。
引数に工夫があり、n,a,bは入力値をそのままずっと保持し続け、pa,pbがそれぞれABの得点で、0からカウントアップします。n,a,bをカウントダウンしながら再帰処理すれば良さそうに思えますが、それだとデュースの処理をうまく扱えないためこのようにしました。
$Memoは高速化のためのメモで[pa,pb]をキーとしてその得点からのパターン数を値にして計算量を減らしています。
メモ化再帰でOKというのはすぐに思いつきましたが、実装の説明でも書いた通りカウントダウンするとデュースの処理がうまく行かず少し手間取りました。