Как мне сгенерировать случайное число в несмежном диапазоне, используя Ruby? - PullRequest
3 голосов
/ 24 декабря 2010

Например, предположим, у меня есть следующие диапазоны

5031..5032 6248..6249

Как мне сгенерировать случайное число в этих двух диапазонах?

Ответы [ 6 ]

5 голосов
/ 24 декабря 2010

Простой способ:

(r1.to_a + r2.to_a).choice

Более быстрое и намного более эффективное с точки зрения памяти общее решение будет включать в себя вычисление общего размера диапазона, генерацию случайного числа и затем нормализацию числа в соответствии с диапазоном.

Обновление: ок, понял. Это решение работает с произвольным числом диапазонов и не генерирует гигантские массивы или перебирает сами диапазоны. (Он повторяется дважды по массиву Диапазоны , а не массивам элементов Range.)

def rangerand *r
  r.inject(rand(
      r.inject(0) do |accum, rng|
        accum + rng.last - rng.first + 1
      end
    )) do |accum, rng|
      rngsize = rng.last - rng.first + 1
      return rng.first + accum if accum < rngsize
      accum - rngsize
    end
  # raise 'this "cannot happen"'
end

puts rangerand 1..3, 999..1001, 10_200..10_205

Хех, я никогда раньше не использовал два #inject в одном и том же выражении.

4 голосов
/ 24 декабря 2010

Для произвольного числа диапазонов

  1. Подсчитать количество возможных значений как total.
  2. Создать случайное число rnd между 0 и total - 1.
  3. Итерация по списку диапазонов, вычитание размера диапазона от rnd до возможного (пока rnd не является неотрицательным).
  4. Если вы не можете вычесть больше, просто получите rnd -йчисло в текущем диапазоне.
3 голосов
/ 24 декабря 2010

Я написал этот код и, глядя сейчас на ответ Никиты, думаю, это более или менее его идея (я использую r.end / r.begin, потому что у диапазонов Ruby нет предварительно рассчитанного атрибута длины)

ranges = [1..10, 21..40, 81..120]

counts = ranges.map { |r| r.end - r.begin + (r.exclude_end? ? 0 : 1) }
random = rand(counts.inject(&:+))    
idx, remainder = counts.inject([0, random]) do |(idx, remainder), count|
  if remainder < count
    [idx, remainder] # break here if you care more for efficiency than for functional purity
  else
    [idx+1, remainder - count]
  end
end
p ranges[idx].begin + remainder

Конечно, если бы у меня были диапазоны разумной длины, я бы просто использовал другие решения, которые уже обсуждались.

1 голос
/ 25 декабря 2010

Вот что я придумал:

require 'benchmark'

class RangeRandom
  def initialize(ranges=[])

    # sanity checks need to go here for things like:
    #   backward/inverted ranges: (0..-1)
    #   range overlaps: (0..2 & 1..3)
    #     could combine ranges, or raise an exception
    #
    # Restrict to ranges that pass ".inclusive?"?

    # the end value is informative only
    @prepped_ranges = ranges.map{ |r| [ r.begin, r.end - r.begin, r.end ] }
  end

  def rand
    range = @prepped_ranges[ Kernel::rand( @prepped_ranges.size ) ]
    range[0] + Kernel::rand( range[1] )
  end
end

Некоторые тесты:

range_random = RangeRandom.new([0..10, 90..100])
5.times do
  puts range_random.rand
end
puts

# >> 94
# >> 97
# >> 92
# >> 92
# >> 8

n = 500_000
Benchmark.bm(7) do |x|
  x.report('rand1:') { n.times do ; r = RangeRandom.new([ 0..1             ]); r.rand; end }
  x.report('rand2:') { n.times do ; r = RangeRandom.new([ 0..1_000_000     ]); r.rand; end }
  x.report('rand3:') { n.times do ; r = RangeRandom.new([ 0..1_000_000_000 ]); r.rand; end }

  x.report('rand4:') { n.times do ; r = RangeRandom.new([ 0..1 ]); r.rand; end }
  x.report('rand5:') { n.times do ; r = RangeRandom.new([ 0..1, 2..3, 4..5, 6..7 ]); r.rand; end }

  x.report('rand6:') { r = RangeRandom.new([ 0..1             ]) ; n.times do ; r.rand; end }
  x.report('rand7:') { r = RangeRandom.new([ 0..1_000_000_000 ]) ; n.times do ; r.rand; end }
end

Общее время выполнения для 500 000 итераций:

# >>              user     system      total        real
# >> rand1:   2.220000   0.000000   2.220000 (  2.224894)
# >> rand2:   2.250000   0.010000   2.260000 (  2.254730)
# >> rand3:   2.250000   0.000000   2.250000 (  2.247406)

Время дляИнициализация нескольких диапазонов для 500 000 итераций:

# >> rand4:   2.220000   0.000000   2.220000 (  2.222983)
# >> rand5:   4.340000   0.000000   4.340000 (  4.337312)

Время для одной инициализации, затем маленький диапазон или большой диапазон для 500 000 итераций:

# >> rand6:   0.560000   0.000000   0.560000 (  0.559673)
# >> rand7:   0.580000   0.000000   0.580000 (  0.584331)

Время увеличивается при использовании большихдиапазоны, но это могут быть вариации из-за активности системы.Инициализация нескольких диапазонов в массиве имеет больший эффект, чего я и ожидал.

1 голос
/ 24 декабря 2010

Добавьте диапазоны в массив и сгладьте его, чтобы получить серию чисел, затем вызовите rand, чтобы найти определенный элемент этого массива.

range_1 = 1..10
range_2 = 2..30

range_array = [range_1.to_a, range_2.to_a].flatten
range_array[rand(range_array.length)]
0 голосов
/ 24 декабря 2010

Для широких диапазонов range.to_a не очень хорошее решение.Вы можете создавать излишне много больших массивов.Самое быстрое решение может выглядеть следующим образом:

ruby-1.9.2-head > from, to = 1, 50000000
 => [1, 50000000] 
ruby-1.9.2-head > from + rand(to-from+1) 
 => 20698596 
ruby-1.9.2-head > from + rand(to-from+1) 
 => 15143263 
ruby-1.9.2-head > from + rand(to-from+1) 
 => 18469491 

Аналогичное решение для диапазонов, однако, почти так же медленно, как range.to_a :

ruby-1.9.2-head > range1 = 1..5000000
 => 50..50000 
ruby-1.9.2-head > range1.begin + rand(range1.count)   # TAKES ABOUT 1 SECOND!
 => 1788170 
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...