Hit&Blow を Ruby で

qiita.comおもしろそうなので、自分でも書いてみた。
 

ルール

コンピュータの決めた、各桁に重複のない3桁の数を当てるゲーム。
当たらなかった場合、

  • 入力と桁の合っている数字の数を、ヒット
  • 入力と桁は合っていないが重複はしている数字の数を、ブロウ

として教えてくる。
 

実行例

各桁のそれぞれちがう、3桁の数を入力して下さい(例:012): 135
ヒット:1, ブロウ:0
各桁のそれぞれちがう、3桁の数を入力して下さい(例:012): 315
ヒット:0, ブロウ:1
各桁のそれぞれちがう、3桁の数を入力して下さい(例:012): 351
ヒット:0, ブロウ:1
各桁のそれぞれちがう、3桁の数を入力して下さい(例:012): 124
ヒット:1, ブロウ:0
各桁のそれぞれちがう、3桁の数を入力して下さい(例:012): 106
ヒット:1, ブロウ:1
各桁のそれぞれちがう、3桁の数を入力して下さい(例:012): 160
ヒット:2, ブロウ:0
各桁のそれぞれちがう、3桁の数を入力して下さい(例:012): 170
ヒット:2, ブロウ:0
各桁のそれぞれちがう、3桁の数を入力して下さい(例:012): 180
ヒット:2, ブロウ:0
各桁のそれぞれちがう、3桁の数を入力して下さい(例:012): 190
正解です! 9回で当てました!

 

コード

hit_and_blow.rb

module Hit_and_Blow
  module_function
  
  N = 3
  
  def input
    expl = (0...N).to_a.join
    loop do
      print "各桁のそれぞれちがう、#{N}桁の数を入力して下さい(例:#{expl}): "
      s = gets.chomp.chars
      if s.size != N || s.uniq.size != N || s.any?(/\D/)
        puts "不正な入力です。"
      else
        return s
      end
    end
  end
  
  def judge(target, number)
    overlap = (target & number).size
    hit = target.zip(number).count { |a, b| a == b }
    [hit, overlap - hit]
  end
  
  def play
    target = [*"0".."9"].sample(N)
    (1..).each do |n|
      number = input
      
      if target == number
        puts "正解です! #{n}回で当てました!"
        break
      else
        hit, blow = judge(target, number)
        puts "ヒット:#{hit}, ブロウ:#{blow}"
      end
    end
  end
end

Hit_and_Blow.play

N = 3だと簡単ですが、N = 4にすると結構手ごわいです。

Enumerator#rest みたいなのが欲しい

Ruby の Enumerator で、列挙はひとつずつしかできない。まとめていくつか列挙できるメソッドが欲しい。

こんな感じ。

class Enumerator
  def rest(n=nil)
    ary = []
    case n
    in nil
      loop { ary << self.next }
    in Integer => a
      a.times { ary << self.next }
    end
    ary
  end
end


a = [*1..10].to_enum

a.next    #=>1
a.next    #=>2
a.rest(4) #=>[3, 4, 5, 6]
a.next    #=>7
a.rest    #=>[8, 9, 10]

ダメですかねえ…。

Ruby の slice が便利になった

Python

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10][1:10:3]    #=>[2, 5, 8]

って便利ですよね。Ruby でこれをやろうとすると、ちょっと工夫が必要でした。

例えばこんな感じに。

1.step(9, 3).map { [*1..10][_1] }    #=>[2, 5, 8]

 
しかし、これはいまではこんな風に書けるようです。

s = (1...10).step(3)
s.class    #=>Enumerator::ArithmeticSequence
[*1..10].slice(s)    #=>[2, 5, 8]

 
これの応用で、さらに短く書けます。

[*1..10].slice((1...10) % 3)   #=>[2, 5, 8]
[*1..10][(1...10) % 3]   #=>[2, 5, 8]

便利ですね。

Ruby でコラッツの問題

ja.wikipedia.org
「コラッツの問題」というのは、自然数 n を取り、

  • n が偶数なら2で割る
  • n が奇数なら、3倍して1を足す

という操作を繰り返したところ、どうなるかという問題。じつはこれは難問で、いまだに解決されていない。いまのところ、初期値が268あたりまで、1に帰結することが知られているそうである。

Ruby で計算してみる。初期値が10万までの範囲での、1に至るステップ数の最大値を求めるコードを、再帰で実装する。

def collatz(n, co = 0)
  return co if n == 1
  n.even? ? collatz(n / 2, co + 1) : collatz(3 * n + 1, co + 1)
end

puts (1..10_0000).map(&method(:collatz)).max    #=>350

最大値は350ステップとわかる。実行時間は、僕の環境で0.94秒ほどでした。


350ステップかかるときの初期値を求めてみる。コードの後半をこう書き直す。

max = 0
max_idx = 1
(1..10_0000).each do |i|
  step = collatz(i)
  if step > max
    max = step
    max_idx = i
  end
end
p [max_idx, max]    #=>[77031, 350]

初期値が77031のとき350ステップかかることがわかる。
 

追記

メモ化できるよう、コードを修正してみる。

$memo = {}

def collatz(n)
  return $memo[n] if $memo[n]
  return 0 if n == 1
  sum = n.even? ? collatz(n / 2) : collatz(3 * n + 1)
  $memo[n] = sum + 1
end

puts (1..10_0000).map(&method(:collatz)).max    #=>350

これだと0.12秒で済む。100_0000回までやっても1.5秒ほど、ちなみに初期値が837799のとき、最大値524ステップを取る。もう一桁上げて、1000_0000回までやると、初期値8400511のとき、最大値685ステップ。

いまどきの Ruby なフィボナッチ数列

Enumerator.produceを使います。

fib = Enumerator.produce([1, 1]) { |a, b| [b, a + b] }.lazy.map(&:first)

#最初の10個
fib.first(10)
#=>[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

#数列の100以上3000未満の部分
fib.drop_while { _1 < 100 }.take_while { _1 < 3000 }.force
#=>[144, 233, 377, 610, 987, 1597, 2584]

#1000未満で素数であるもの
require "prime"
fib.select(&:prime?).take_while { _1 < 1000 }.force
#=>[2, 3, 5, 13, 89, 233]

 

追記(12/6)

無駄に便利にしてみました。

module Fib
  Seq = Enumerator.produce([1, 1]) { |a, b| [b, a + b] }.lazy.map(&:first)
  
  class << self
    def method_missing(name, *args, &bk)
      Seq.__send__(name, *args, &bk)
    end
    
    def [](arg)
      case arg
      in Integer => i
        drop(i).first
      in Range => r
        a = r.begin
        if r.end
          b = r.exclude_end? ? r.end - 1 : r.end
          drop(a).take(b - a + 1).force
        else
          drop(a)
        end
      end
    end
  end
end

#最初の10個
Fib.first(10)    #=>[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

#最初の5個をそれぞれ2乗した数列
Fib.map { _1 ** 2 }.first(5)    #=>[1, 1, 4, 9, 25]

#(0番始まりで)9番目の値
Fib[9]    #=>55

#(0番始まりで)3番目から9番目までの数列
Fib[3..9]    #=>[3, 5, 8, 13, 21, 34, 55]

#3番めから9-1番目までの数列
Fib[3...9]    #=>[3, 5, 8, 13, 21, 34]

#3番目からの無限数列を10個取ったもの
Fib[3..].first(10)    #=>[3, 5, 8, 13, 21, 34, 55, 89, 144, 233]

Ruby 的に自然な FizzBuzz

まことに FizzBuzz の種は盡きまじ。
 

fizzbuzz = Enumerator.new {|y|
  (1..).each do |i|
    f = (i % 3).zero?
    b = (i % 5).zero?
    y << case
         when f && b then "FizzBuzz"
         when f      then "Fizz"
         when b      then "Buzz"
         else             i.to_s
         end
  end
}

puts fizzbuzz.take(40)

Enumerator と Endless Range は使いたいよね。場合分けは case ~ when。

関数を微分して gnuplot で出力(Ruby)

20210116152153
 
コード。

require "numo/gnuplot"

dx = 0.0001

dif = ->(f, x) {
  (f.(x+dx)-f.(x))/dx
}.curry

f = ->(x) { x*x-2*x+1 }

dif_f = dif.(f)

xs = -5.step(5, 0.1).to_a
ys1 = xs.map(&f)
ys2 = xs.map(&dif_f)

Numo.gnuplot do
  set terminal: :x11
  unset :key
  set xrange: -5..5
  set yrange: -5..10
  set grid: true
  plot xs, ys1, {w: :lines, lw: 2},
       xs, ys2, {w: :lines, lw: 2}
end
gets    #終了待ち