私自身が表題の問題を解いた時のプログラムについて解説します。
問題の詳細は「取られたら取り返す!」(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というのはすぐに思いつきましたが、実装の説明でも書いた通りカウントダウンするとデュースの処理がうまく行かず少し手間取りました。