コンテンツへスキップ

【Ruby基礎】AtCoder Beginner Contest 025 B – 双子とスイカ割り

■はじめに

Rubyの基礎的な問題をたくさん解くことで基本的な考え方やメソッドの使い方を定着させたい。
基本的にはAtCoderというプログラミングコンテスト(競技プログラミング)の過去問を使う。(AtCoderは難易度が分かれており、難易度の低いA問題かB問題を解いていく)

(5/23時点の方針)
メソッドの切り分け方や値の受け渡しを練習するために、コード長の短さについては気にせずに書くことにする。

(2022/10/17時点の方針)
しばらくはB問題を小さい番号の方からやっていく。たまにA問題もやるかも。

■問題

●出典

AtCoder Beginner Contest 025のB問題
https://atcoder.jp/contests/abc025/tasks/abc025_b

●問題文

直大くんと直子さんは双子の兄妹です。今日は家の廊下でスイカ割りの練習をすることになりました。

廊下は東西方向に無限に長く、途中の 1 箇所に直大くんの部屋の入り口があります。最初、直大くんの部屋の前に直大くんと直子さんがいます。

スイカ割りの練習では、以下の N 回の移動を順に実行します。

  • i 番目の移動 : 最初に直子さんが方角とメートル単位の距離 di を指定します。指定する方角は東か西で、di は正整数です。その後、直大くんが指定された方向を向いて、距離 di を目標に移動します。

直大くんは 1 回の移動において A メートルよりも少ない距離を移動することと、B メートルよりも多い距離を移動することが苦手です。そのため、目標移動距離が di メートルだったときの最終移動距離は以下のようになります。

  • di < A のとき、直大くんは向いている方向に A メートル進む。
  • A≦di ≦B のとき、直大くんは向いている方向に di メートル進む。
  • di > B のとき、直大くんは向いている方向に B メートル進む。

あなたの課題は、直大くんが N 回の移動を終えたときにどこにいるのかを求めることです。

●入力

入力は以下の形式で標準入力から与えられる。

N A B
s1 d1
s2 d2
:
sN dN
  • 1 行目には、3 つの整数 N(1≦N≦100) と A と B(1≦A≦B≦100) が空白区切りで書かれている。
  • 2 行目からの N 行には、移動の情報が書かれている。N 行のうちの i(1≦i≦N) 行目には、文字列 si と整数 di (1≦d i ≦100) が空白区切りで書かれている。文字列 si は East または West であり、直子さんが指定する方角がそれぞれ東、西であることを表す。

●出力

  • 直大くんの最終的な位置が直大くんの部屋の前よりも X(1≦X) メートル東になったとき、文字列 East と X をこの順に空白区切りで 1 行に出力せよ。
  • 直大くんの最終的な位置が直大くんの部屋の前よりも X(1≦X) メートル西になったとき、文字列 West と X をこの順に空白区切りで 1 行に出力せよ。
  • 直大くんの最終的な位置が直大くんの部屋の前と同じ場所になったとき、整数 0 を 1 行に出力せよ。

いずれの場合においても、出力の末尾に改行を入れること。

■回答

●愚直に書く

East方向をプラス、West方向をマイナスとすると足し算引き算で考えられるのでは。

n, a, b = gets.split.map(&:to_i)
d = n.times.map {|x| x = gets.chomp}

ans = d.map { |i|
  i = i.gsub(/East |West /, "East " => "", "West " => "-").to_i
  if i < 0 && i.abs < a
    i = -a
  elsif i.abs < a
    i = a
  else
    i
  end
  if i < 0 && i.abs > b
    i = -b
  elsif i.abs > b
    i = b
  else
    i
  end
  }.sum

if ans > 0
  puts("East #{ans}")
elsif ans < 0
  puts("West #{ans.abs}")
else
  puts ans
end

かなり不格好だけど通った…!

●リファクタリング/別アプローチ

ひとまず、ifの式が大量にあるので、三項演算子でまとめてみた。

n, a, b = gets.split.map(&:to_i)
d = n.times.map {|x| x = gets.chomp}

ans = d.map { |i|
    i = i.gsub(/East |West /, "East " => "", "West " => "-").to_i
    i < 0 && i.abs < a ? i = -a : i.abs < a ? i = a : i
    i < 0 && i.abs > b ? i = -b : i.abs > b ? i = b : i
  }.sum

puts ans > 0 ? "East #{ans}" : ans < 0 ? "West #{ans.abs}" : ans

通った!
とはいえ、これがわかりやすいかというと微妙な気もする。

もうちょっとこう、スマートにできないものか…。
計算のためにEastWestを一度整数にして、最後の表示用にまたEastWestに戻す、というのが、他に良い方法があるといいんだけど。

ここでギブアップ、他の方の回答を見ることにする。

●他の方の回答例

一覧ページでコード長だけを見た感想としては、上位の方も結構長いコードにはなっているようだった。

実際のコードを見た感想としては、今回は結構皆さんの書き方が色々な感じがする。

その中でこれはと思った回答があったので、それをもとに自分の回答を整理してみる。

# 参考にした回答
N,A,B = gets.split.map(&:to_i)

x = 0
$<.each{|ln|
    s,d = ln.split
    x += d.to_i.clamp(A..B) * (2-(s[0].ord&3))
}

print 'East ' if 0 < x
print 'West ' if x < 0
p x.abs

上記でまず目に入ったのが、最後の出力のところ。
自分は入れ子の三項演算子で「East 数字」「West 数字」「 数字」の3パターンをputsしていた、つまり「数字」を3回puts内に書いていたけど、上の書き方は「数字」を1回書くだけで済んでいる。

続いて、clampという見たことのないメソッド。これを調べると、今回やりたい処理にぴったりのメソッドだった…!!!びっくり。

上記をもとに自分の回答も修正。

n, a, b = gets.split.map(&:to_i)
d = n.times.map {|x| x = gets.chomp}

ans = d.map { |i|
  i = i.gsub(/East |West /, "East " => "", "West " => "-").to_i
  i > 0 ? i = i.abs.clamp(a..b) : i < 0 ? i = -i.abs.clamp(a..b) : i
  }.sum

print "East " if 0 < ans
print "West " if ans < 0
puts ans.abs

通った!
gsubのところももうちょっとシンプルにできそうな気がするけど、今回はここまでにしておこう。

●出てきたメソッド等

公式リファレンスを見る訓練。

あとはるりまじゃないけど下記↓

■振り返りなど

いよいよ難しくなってきた…。でも今回は自分で回答を導き出せて、その後他の回答を参考に少しリファクタリングもできて良かった。