How to create random number in disjoint range using Ruby?

For example, suppose I have the following ranges

5031..5032 6248..6249

How to create a random number in these two ranges?

+3
source share
6 answers

A simple way:

(r1.to_a + r2.to_a).choice

A faster and much more memory efficient general solution will include calculating the total size of the range, generating a random number, and then normalizing the number to the range that it falls into.

Update: Good, it worked. This solution works with an arbitrary number of ranges and does not generate giant arrays or does not iterate over the ranges themselves. (It is repeated twice over an array of ranges, not an array of Range elements.)

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 .

+5

  • total.
  • rnd 0 total - 1.
  • , rnd , ( , rnd ).
  • , rnd -th number .
+4

. , ( 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

, , .

+3

, , 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)]
+1

:

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

For wide ranges, range.to_a is not a good solution. You can create an unnecessary set of large arrays. The fastest solution might look like this:

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 

A similar solution for ranges, however, is almost as slow as 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 
0
source

Source: https://habr.com/ru/post/1781919/


All Articles