高速な素数の判定(Ruby)

素数は約数を 2個(1 とそれ自身)もつ自然数ですね。ですから、自然数 n が素数かどうかを判定するには、n がそれ自身より小さい自然数(1 を除く)で割ることができるかどうかを確かめればよいわけです。このとき、割る数は を超える必要がないことは明らかです(それを超える数 x で n を割るとき、割れても商は x を超えることはありません。つまり、既に調べています)。この考え方をもとに、以下のような素数判定のメソッドを書くことができます。

def prime?(n)
  return false if n < 2
  return true  if n == 2 or n == 3
  return false if (n % 2).zero?
  3.step(Math.sqrt(n).to_i, 2) {|i| return false if (n % i).zero?}
  true
end

高速化の多少の工夫があって、2以外の偶数はあきらかに素数でないので、先に除外し、3以上の奇数で割っています。これを使って 1~100万の自然数に何個の素数があるか調べてみると*1、自分の環境で 3.1秒程度となり、標準添付ライブラリの Prime クラスの Prime.prime? メソッドを使う(7.0秒程度)より、200%以上高速になります(Ruby 2.3.3)。


さらに高速化することも可能です。割る数として、5以上の場合は 5, 7, 11, 13, 17, 19, 23, 25, 29, 31, ... のように、ステップ幅を 2 と 4 の交互に取っていくという方法です。これはかなりかしこいアルゴリズムで、コードはこんな具合になります。

def prime?(n)
  return false if n < 2
  return true  if n == 2 or n == 3
  return false if (n % 2).zero? or (n % 3).zero?
  i, step = 5, 2
  guard = Math.sqrt(n).to_i
  while i <= guard
    return false if (n % i).zero?
    i += step
    step = 6 - step
  end
  true
end

これで先ほどと同じ計算をしてみると、1.8秒程度とさらに 170% ほど高速化します。Ruby の単純なコードとしては、これで充分なのではないでしょうか。


以下の書籍を参考にしました。

C言語による最新アルゴリズム事典 (ソフトウェアテクノロジー)

C言語による最新アルゴリズム事典 (ソフトウェアテクノロジー)

 

追記

標準添付ライブラリの 'prime' を使う場合、Prime.prime?() を使うより何故か Integer#prime? を使った方が 600% 以上高速になります。なので、これを使った方がよいでしょう。(2019/7/2)

Integer#prime? の実装は以下です(Ruby 2.6.0)。めちゃかしこい。

def prime?
  return self >= 2 if self <= 3
  return true if self == 5
  return false unless 30.gcd(self) == 1
  (7..Integer.sqrt(self)).step(30) do |p|
    return false if
      self%(p)    == 0 || self%(p+4)  == 0 || self%(p+6)  == 0 || self%(p+10) == 0 ||
      self%(p+12) == 0 || self%(p+16) == 0 || self%(p+22) == 0 || self%(p+24) == 0
  end
  true
end


*1:コードはこんな具合です。

co = 0
for i in 1..100_0000
co += 1 if prime?(i)
end
puts co

なお、1~100万までの間の素数の個数を調べるためだけなら、じつは「エラトステネスの篩」を使った方が速いです(0.38秒程度)。得られた配列の大きさがその答えになっているので。(エラトステネスの篩は配列をまとめなおすところが無駄なので、さらなる高速化も可能です。)ただし、エラトステネスの篩はメモリを喰いますし、与えられたひとつの数が素数かどうかを判定するには向きません。
比較的省メモリで「エラトステネスの篩」と同じ結果を得るにはこのようなコードで可能ですが、エラトステネスの篩よりもだいぶ遅くなります。

ひとつのサイコロの配置の数

ひとつのサイコロを特定の方向から見たばあい、その「見かけ」は全部で何とおりあるのでしょうか。
20180428164919
20180428164915
 
Ruby で解いてみました。コード。

table = {E: [3, 2, 6, 1, 5, 4], N: [5, 1, 3, 4, 6, 2],
         S: [2, 6, 3, 4, 1, 5], W: [4, 2, 1, 6, 5, 3],
         R: [1, 4, 2, 5, 3, 6], L: [1, 3, 5, 2, 4, 6]}
table1 = {}
table.each_key {|k| table1[k] = table[k].map(&:pred)}
      
throw = ->(dice) {
  memo  = [dice]
  stack = [dice]
  while (dice1 = stack.shift)
    [:E, :N, :S, :W, :R, :L].each do |c|
      next_dice = Array.new(6, 0)
      dice1.each_with_index do |d, i|
        next_dice[table1[c.to_sym][i]] = d
      end
      next if memo.include?(next_dice)
      memo  << next_dice
      stack << next_dice
    end
  end
  p memo
  puts memo.size
}

throw.([1, 2, 3, 4, 5, 6])

転がり方は上の図の N, S, E, W の方向の他、真上から見た左回転(L)と右回転(R)があります。いったん出た「見かけ」は変数 memo に入れておき、重複しないようにしています。幅優先探索で求めています。
 
結果。

[[1, 2, 3, 4, 5, 6], [4, 2, 1, 6, 5, 3], [2, 6, 3, 4, 1, 5], [5, 1, 3, 4, 6, 2],
 [3, 2, 6, 1, 5, 4], [1, 3, 5, 2, 4, 6], [1, 4, 2, 5, 3, 6], [6, 2, 4, 3, 5, 1],
 [2, 3, 1, 6, 4, 5], [5, 4, 1, 6, 3, 2], [4, 1, 5, 2, 6, 3], [4, 6, 2, 5, 1, 3],
 [6, 5, 3, 4, 2, 1], [3, 6, 5, 2, 1, 4], [2, 4, 6, 1, 3, 5], [3, 1, 2, 5, 6, 4],
 [5, 3, 6, 1, 4, 2], [1, 5, 4, 3, 2, 6], [2, 1, 4, 3, 6, 5], [5, 6, 4, 3, 1, 2],
 [6, 4, 5, 2, 3, 1], [6, 3, 2, 5, 4, 1], [3, 5, 1, 6, 2, 4], [4, 5, 6, 1, 2, 3]]
24

24とおりですか。

これはプログラミングで解かなくてもわかりますね。例えば上面を考えてそれは 6とおりあり、真上から見ての回転でそれぞれにつき 4とおりですから、6×4 = 24 とおりか。

なお、上のコードは N, S, E, W, R, L のすべての転がし方を考えていますが、じつは例えば E と N の 2種類の転がし方だけで同じ結果を得ることができます。

Ruby で Proc とブロックの相互変換について

Ruby で Proc オブジェクトとブロックは似ていますが、まったく同じものではありません。むしろ、表裏一体の関係にあるというべきでしょう。その証拠に、「&」を使ってブロックから Proc へ、また Proc からブロックへとお互いに変換することができます。


まず、ブロックから Proc へ。

$ pry
[1] pry(main)> def a(&block)
[1] pry(main)*   block
[1] pry(main)* end  
=> :a
[2] pry(main)> b = a {puts "block"}
=> #<Proc:0x0055b7609c9d30@(pry):4>
[3] pry(main)> b.call
block
=> nil

ここではメソッド a は、与えられたブロックを Proc に変えてそのまま返します。つまり、ブロック {puts "block"} が Proc に変換されて変数 b に入っています。なので、b を call してやるとブロックの中身が実行されて文字列 "block" が標準出力されます。このように、ブロックをオブジェクトとして取り出すことができます。


逆をしてみます。Proc からブロックへの変換の例。

$ pry
[1] pry(main)> a = proc {|x| x.chomp.upcase}
=> #<Proc:0x0055a9370c41e0@(pry):1>
[2] pry(main)> ["a\n", "b\n", "c\n"].map(&a)
=> ["A", "B", "C"]

ここで、変数 a に入っている Proc オブジェクトは、&a で変換されて map のブロックとして使われています。つまり、map {|x| x.chomp.upcase} と同じことです。


以上、Proc とブロックの相互変換についてでした。

カードゲーム「ブラックジャック」を Ruby で実装する

Qiita を見ていたら「初心者はブラックジャックを実装してみるといいよ」とかいう記事があったので、おもしろそうだと Ruby で実装してみました。

とりあえず実行例を見て下さい。

$ ruby blackjack.rb
カードをシャッフルします

コイン: 10000 点   ベットは?(enter = 1000):
  ベットは 1000 点です
=== カードを配ります ====
ディーラーの一枚は ♠6 です
プレイヤーの二枚は ♣Q と ♢9 です
Hit or Stand? (H or S): s
勝負です:
ディーラーのもう一枚は ♡7 です
  ディーラーはもう一枚引きます
ディーラーの引いたのは ♡8 です
ディーラーの合計は 21、プレイヤーの合計は 19 です
ディーラーの勝ちです
** コインは 9000 点です

コイン: 9000 点   ベットは?(enter = 1000):4000
=== カードを配ります ====
ディーラーの一枚は ♢10 です
プレイヤーの二枚は ♣3 と ♡3 です
Hit or Stand? (H or S): h
  Hitしました
プレイヤーのひいたカードは ♢2 です
Hit or Stand? (H or S): h
  Hitしました
プレイヤーのひいたカードは ♠10 です
Hit or Stand? (H or S): s
勝負です:
ディーラーのもう一枚は ♡J です
ディーラーの合計は 20、プレイヤーの合計は 18 です
ディーラーの勝ちです
** コインは 5000 点です

コイン: 5000 点   ベットは?(enter = 1000):2000
=== カードを配ります ====
ディーラーの一枚は ♣5 です
プレイヤーの二枚は ♢3 と ♠8 です
Hit or Stand? (H or S): h
  Hitしました
プレイヤーのひいたカードは ♡K です
Hit or Stand? (H or S): s
勝負です:
ディーラーのもう一枚は ♣7 です
  ディーラーはもう一枚引きます
ディーラーの引いたのは ♠J です
ディーラーの合計は 22、プレイヤーの合計は 21 です
プレイヤーの勝ちです
** コインは 7000 点です

コイン: 7000 点   ベットは?(enter = 1000):5000
=== カードを配ります ====
ディーラーの一枚は ♡9 です
プレイヤーの二枚は ♣J と ♠4 です
Hit or Stand? (H or S): h
  Hitしました
プレイヤーのひいたカードは ♡10 です
プレイヤーはバーストです
ディーラーの勝ちです
** コインは 2000 点です

コイン: 2000 点   ベットは?(enter = 1000):1000
=== カードを配ります ====
ディーラーの一枚は ♣A です
プレイヤーの二枚は ♣4 と ♡4 です
Hit or Stand? (H or S): h
  Hitしました
プレイヤーのひいたカードは ♣8 です
Hit or Stand? (H or S): s
勝負です:
ディーラーのもう一枚は ♡A です
  ディーラーはもう一枚引きます
ディーラーの引いたのは ♣10 です
  ディーラーはもう一枚引きます
ディーラーの引いたのは ♣2 です
  ディーラーはもう一枚引きます
ディーラーの引いたのは ♢J です
ディーラーの合計は 24、プレイヤーの合計は 16 です
プレイヤーの勝ちです
** コインは 3000 点です

コイン: 3000 点   ベットは?(enter = 1000):2000
=== カードを配ります ====
ディーラーの一枚は ♡6 です
プレイヤーの二枚は ♠K と ♢4 です
Hit or Stand? (H or S): h
  Hitしました
プレイヤーのひいたカードは ♡Q です
プレイヤーはバーストです
ディーラーの勝ちです
** コインは 1000 点です

コイン: 1000 点   ベットは?(enter = 1000):
  ベットは 1000 点です
=== カードを配ります ====
ディーラーの一枚は ♠7 です
プレイヤーの二枚は ♡2 と ♢5 です
Hit or Stand? (H or S): h
  Hitしました
プレイヤーのひいたカードは ♢6 です
Hit or Stand? (H or S): h
  Hitしました
プレイヤーのひいたカードは ♠2 です
Hit or Stand? (H or S): s
勝負です:
ディーラーのもう一枚は ♠Q です
ディーラーの合計は 17、プレイヤーの合計は 15 です
ディーラーの勝ちです
** コインは 0 点です

コインがなくなりました

 
こんなのですけれど、実際に遊んでみて、これはマジで結構アツいです! いやあ、ブラックジャック、おもしろいじゃないですか。

ルールはWikipediaどおりです。簡単にだけいうと、

  • 合計の点数が大きい方が勝ち。ただし、21 を超えると負け
  • J, Q, K は 10
  • A(エース)は 1 か 11 か、都合の良いほう
  • 最初はディーラーもプレーヤーも 2枚のカードが配られる。ディーラーの一枚はプレーヤーに見えている
  • プレーヤーは配られたカードを見て、もう一枚もらう(Hit)かもらわない(Stand)か、どちらかを選択できる。Hit は何度してもよいが、合計が 21 を超えた時点で負け(バースト)
  • ディーラーは合計が 17 以上になるまで引き続けないといけない

プレイヤーが「ナチュラル21」(最初に配られた二枚で 21)を出して勝った場合、ベットの2.5倍の点が払い戻されます。また、カードは最初に52枚すべてがシャッフルされ、何度かプレイしてデッキの枚数が15枚より少なくなるごとに、シャッフルし直します。なので、高度な戦略を練る場合はそれまで出たカードを覚えていて、それをもとに決めることも可能です。


コードは以下にあります。もし実行されるなら、コピペするよりも「Download ZIP」をポチリの方ががラクだと思います。
カードゲーム「ブラックジャック」の実装 · GitHub
長い長い関数でありまして、典型的な「叱られるクソコード」(笑)かもしれません。いやでも、ゲームは楽しいからよいことにしちゃいましょう。Ruby によるコーディングも楽しい!


Linux Mint 18.3, Ruby 2.3.3 と Windows 8.1, Ruby 2.2.2p95 (2015-04-13 revision 50295) [i386-mingw32] で動作確認しました。


※追記
まずないと思いますが、ゲームの途中でカードの山がなくなった場合に対応しました。また、ベットの入力でマイナス値を入れるとそこで終了するようにしました。勝ち逃げしたい方はどうぞ(笑)。

スート(スペードなどのカードの種類)の表示を機種依存文字から絵文字に替えました。

クロージャを使ったカウンター

新人プログラマに知ってもらいたいメソッドを読みやすく維持するいくつかの原則 - Qiita
を見ていたら、クロージャを使っておもしろい仕方でカウンターが作れることを知った。

元のコードは JavaScript のようであるが、Ruby だとこんな感じ。

$ pry
[1] pry(main)> counter = ->{
[1] pry(main)*   count = 0
[1] pry(main)*   ->{count += 1}
[1] pry(main)* }.call  
=> #<Proc:0x005594b3626450@(pry):3 (lambda)>
[2] pry(main)> counter.call
=> 1
[3] pry(main)> counter.call
=> 2
[4] pry(main)> counter.call
=> 3

おもしろいですよね。呼び出すごとに確かに 1 ずつ増えていくな。
ちなみに、変数 counter に入っているのは Proc オブジェクト ->{count += 1} です。


こんな記事もあります。ここでもクロージャを使ってカウントしていますね。
obelisk.hatenablog.com

「モンティ・ホール問題」をシミュレートする(Ruby)

rubyでモンティホール問題に挑戦 - Qiita
ここでいわゆる「モンティ・ホール問題」を Ruby でシミュレートしているのを目にして、自分なりにやってみたくなりました。

「モンティ・ホール問題」とはこういうものです。

三つの扉があり、その向こうのひとつにだけ当たりがあります。
 
挑戦者はまず三つの扉の中からひとつを選びます。
司会者(モンティ・ホール)は当たりの扉を知っています。そして二つあるハズレの扉のひとつを開けます。
挑戦者はそこで前に選んだ扉をもういちどそのまま選択してもよいし、閉まっている残りの二つのどちらかを選びなおしてもよいです。
 
さて、挑戦者はここで扉を替えた方がよいのか、そのままにした方がよいのか、それともいずれでも当たる確率は一緒なのか。
どうなのでしょうか?

 
先に答えを言っておくと、これは「扉を替えた方」が当たる確率は高くなるのです。扉を替えない場合、当たる確率は 1/3 で、扉を替えると 2/3 になります。
不思議ですか? ただ扉を開けただけなのに?


コードと結果。
monty_hall_problem.rb

N = 100_0000

#一回の試行
def trial(is_reselection)
  doors = [true, false, false]
  select_of_challenger = rand(3)
  select_of_monty = (select_of_challenger == 3) ? 2 : 3
  
  if is_reselection
    doors.delete_at(select_of_monty)
    reselect_of_challenger = select_of_challenger.zero? ? 1 : 0
    doors[reselect_of_challenger]
  else
    doors[select_of_challenger]
  end
end

#N回試行して確率の計算
def calc(is_reselection)
  co = 0
  N.times { co += 1 if trial(is_reselection) }
  co.to_f / N
end

#扉を替えない場合
puts calc(false)    #=>0.333056
#扉を替える場合
puts calc(true)     #=>0.666614

確かにそうなります。

説明が必要でしょう。まず、扉 0, 1, 2 があって、当たりは必ず扉 0 にあるとします。挑戦者は何も知らないので、この仮定で問題ありません。これが doors = [true, false, false] で表されます。
あと、モンティは最初に挑戦者の選んだ扉は開けられません。


あとはさほど問題はないと思います。

格子点の列挙(Ruby)

格子点の列挙 - みずぴー日記
ここの問題で遊んでみました。

問題を再掲しておきます。

二次元平面上の格子点(X,Y座標がともに整数の点)を、原点から近い順に列挙してください。

同じ距離の点はどういう順番でも構いませんが、可能であればX軸に一番近い第一象限の点から原点を中心として反時計回りの順に列挙してください。 列挙の方法は、1行に一つの点の、X,Y座標を出力することとします。

http://d.hatena.ne.jp/mzp/20071006/lattice

 
結果の例(括弧をつけた出力にしました)。

( 3, -2)
(-6,  1)
(-5,  4)
( 4, -5)
( 5, -7)
(-9, -2)
(-2, -9)
( 7,  7)
(-8,  8)
( 8, -9)

 
コード。

class Complex
  include Comparable
  
  def <=>(a)
    f = ->(θ) {(θ < 0) ? 2 * Math::PI + θ : θ}
    
    l1, l2 = abs2, a.abs2
    θ1, θ2 = f.(angle), f.(a.angle)
    
    if    l1 > l2 then  1
    elsif l1 < l2 then -1
    elsif θ1 > θ2 then  1
    elsif θ1 < θ2 then -1
    else 0
    end
  end
  
  def to_s
    sprintf "(% 2d, % 2d)", real, imaginary
  end
end

ar = 10.times.map {Complex(rand(-9..9), rand(-9..9))}
puts ar.sort

いわゆる「宇宙船演算子」(<=>というやつ)を Complex クラスに定義して解いてみました。並べ替えはふつうに Array#sort でやっています。

ちょっとトリッキーですかね。