はじめに
ボウリングのスコア計算をするプログラムをRubyで書きました。
競技プログラミング初心者向けになっていると思います。
競プロ始めたての方(もしくはRuby始めたての方)には、楽しんで読んでもらえると思います。
[問題] ボウリングスコア計算
[入力]
- 一行の文字列で与えられる。
- 文字と文字の間にはスペースが含まれる。
- 文字は、
0 <= n <=10
のいずれかである。 - 数字は倒れたピン数を表す。
"3 7 10 8 2 8 1 8 1 6 3 8 2 8 2 9 1 9 1 9"
[スコア計算のルール]
- ピン数は10本であり、最終フレームは10フレーム目である。
- 1投目に全てのピンを倒すことをストライク、1フレーム内で全てのピンを倒すことをスペアと呼ぶ。
- 通常は、2投で1フレームである。
- 最終フレーム以外、ストライクの場合は1投でフレームを終え、次の2投先のスコアを、ストライクを取ったスコアに加算する。
- 最終フレーム以外、スペアの場合は、次の1投先のスコアをスペアを取ったスコアに加算する。
- 最終フレームは、2投目までに全ピンを倒すことができた場合のみ、3投する。
- 最終フレームは、ストライク、スペアの特別加算をしない。
詳しいルール説明は、こちらを参照。
[出力]
最終的なスコアを計算し、出力せよ。
160
解き方
ここからは解き方を説明します。
少々長くなるので、答えをさっと知りたい方は、「完成プログラム」と書かれているところまでスクロールしてください。
この問題を解く際、私は次のステップでコーディングする必要があると考えました。
- フレーム毎にスコアを分ける
- ストライク、スペアの場合の特別加算について考える
- 全フレームのスコアの合計値を出す
一つずつ説明します。
フレーム毎にスコアを分ける
まずは、入力値を分解し、フレーム毎にまとめます。
ここで注意しないといけないのは、1フレームは必ずしも2投からなるわけでは無いということです。
ストライクを取った場合は1投で1フレーム、最終フレームでは2投目までにストライクかスペアを出した場合は3投で1フレームになります。
そのため、「入力値をsplitして2個ずつまとめる」といったような単純な実装ではいけません。
ここがこの問題の最初の壁です。
フレームが切り替わるタイミングを考えてみます。
- 2投した場合
- ストライクを取った場合
フレームが切り替わるタイミングは、以上の2つのうちのどれかです。
フレームは10フレームが最後であるとわかっています。そのため、フレーム数をカウントする変数を用意し、「ストライクを取った場合」、「ストライクを取らずに2投した場合」でそれぞれフレーム数を一つずつ増やせば、最終フレームまで分割することができます。
一旦、ここまでの流れをプログラムで見てみましょう。
# 入力値をスペース区切りで分割し、数値型の配列にする
input_array = gets.split(" ").map(&:to_i)
# => [3, 7, 10, 8, 2, 8, 1, 8, 1, 6, 3, 8, 2, 8, 2, 9, 1, 9, 1, 9]
# フレーム数
frame = 0
# 最後のフレーム数
max_frame = 10
# フレーム内での投数
count = 0
input_array.length.times do |i|
# 最終フレームでは無い場合
if frame + 1 != max_frame
# ストライクの場合
if input_array[i] == 10 && count == 0
# ストライクの時は、1投で2回分投げたとみなす
count = 1
end
end
count += 1
if frame + 1 == max_frame
# 最終フレームの処理
else
# フレームの切り替え
if count == 2
count = 0
frame += 1
end
end
end
ストライク、スペアの特別加算
スコア計算のルールをもう一度おさらいします。
ストライクのときは、次の2投の倒したピンをストライクを出したフレームで加算します。
スペアのときは、次の1投の倒したピンを、スペアを出したフレームで加算します。
ストライクとスペアの区別は、フレーム内での1投目で10ピンを倒したか、2投目で倒したかでわかります。
先ほど、フレーム毎にスコアを区切るプログラムを書いたので、ストライクとスペアを区別できるようになったはずです。
次に、ストライクとスペアの特別計算をします。
これも難しく考える必要はなく、ストライクは次の2投分を加算し、スペアの場合は次の1投分を加算してあげると良いです。
ストライクとスペアを見分けることができれば、特別計算のロジックはそれほど苦労せずとも組めます。
ここで、注意しなければならないのは、最終フレームでは特別加算が行われないということです。
これも、フレーム数がわかっていれば簡単に実装できます。
この問題は、投球をフレーム毎に分けることができたら、ゴールに一気に近づけます。
あとは、フレーム毎にあるスコアの合計を求めれば、プログラムの完成です。
完成プログラム
# 入力値をスペース区切りで分割し、数値型の配列にする
input_array = gets.split(" ").map(&:to_i)
# => [3, 7, 10, 8, 2, 8, 1, 8, 1, 6, 3, 8, 2, 8, 2, 9, 1, 9, 1, 9]
# フレーム数
frame = 0
# 最後のフレーム数
max_frame = 10
# フレーム内での投数
count = 0
# フレーム毎のスコアをもつ配列
score = [0] * max_frame
input_array.length.times do |i|
# 最終フレームでは無い場合
if frame + 1 != max_frame
if input_array[i] == 10
if count == 0 # 2投目で10ピン倒した場合はスペアになることを考慮
# ストライクの時は、1投で2回分投げたとみなす
count = 1
score[frame] = input_array[i] + input_array[i+1] + input_array[i+2]
else # スペアの場合(1)
score[frame] = input_array[i] + input_array[i+1]
end
# スペアの場合 (2)
elsif input_array[i] + score[frame] == 10
score[frame] = score[frame] + input_array[i] + input_array[i+1]
else # ストライク、スペア以外の場合
score[frame] += input_array[i]
end
count += 1
# フレームの切り替え処理
if count == 2
count = 0
frame += 1
end
# 最終フレームの処理
else
score[frame] += input_array[i]
end
end
# 最終スコアを出力
puts score.sum
# => 160
最後に
毎度のことですが、正直私のプログラムはまだまだリファクタできるところがたくさんあると思います。
よろしければ、コメントで教えて頂ければ幸いです。
最後まで読んで頂きありがとうございました。