0

I need to fill a Ruby array with N random numbers. However, I need to ensure that they are X numbers apart from each other.

For instance, I need to fill an array with 5 numbers, randomly generated between 1 and 100, but ensuring that no number is closer than 3.

So this is valid:

[1, 4, 7, 22, 84]  # the differences between the numbers are >= 3

This is not valid:

[1, 2, 50, 70, 90] # "1" and "2" are too close together

Any ideas?

sawa
  • 165,429
  • 45
  • 277
  • 381
jmccartie
  • 4,956
  • 8
  • 50
  • 71
  • 1
    Does it have to be an increasing sequence? Do only consecutive numbers matter? Is `[1, 4, 1, 4, 1]` okay (provided it was generated randomly? – sawa Oct 14 '15 at 02:38
  • What is a random sequence in this case? I can think of two ways of drawing a random set of M numbers between 1 and N such that no two numbers are within D of each other. The first is to enumerate all such sets of numbers and then select one at random. (Depending on N, M and D, there could be quite a few sets.) The second way would be to randomly draw N numbers and then check to see if any pair is within D of each other. If not, you have a random set; if so, discard the set and draw another random set of N, continuing until a set passes the test. (cont...) – Cary Swoveland Oct 14 '15 at 08:56
  • (...cont.) Again, that could take quite awhile. (It all depends on the values of the three parameters.) Both of those methods result in a random set being drawn. If, however, both of those approaches are computationally infeasible, and you resort to some *ad hoc* procedure, you will probably not draw a set randomly and will have no idea how skewed your sample might be. – Cary Swoveland Oct 14 '15 at 09:04

4 Answers4

3

Here's one idea: every time a random number is generated, remove every number that's close to it from the candidates:

def gen_rand_arr(arr, diff)
  sample = arr.sample
  arr.delete_if {|x| (x - sample).abs < diff }
  sample
end

Then use it like:

arr = (1..100).to_a
result = []
1.upto(5) { result << gen_rand_arr(arr, 3) }
p result.sort
Yu Hao
  • 119,891
  • 44
  • 235
  • 294
  • It should be `< diff`, not `<= diff`. Or you could use `keep_if` and `>= diff`. – Stefan Oct 14 '15 at 08:27
  • Note that your candidates array has to be large enough in order to guarantee 5 results, in this case at least 1..21 (if I'm not mistaken). – Stefan Oct 14 '15 at 08:31
  • @Stefan You are right, the current code doesn't check for validity of input. It should be more defensive to be used in real, however I'll keep it simple here, just showing the idea. – Yu Hao Oct 14 '15 at 08:37
1
i = 1
while i < 100  do
   final = rand(i+3..i+20)
   (a ||= []).push(final)
   i +=20
end
p a

Some random examples.

[18, 39, 48, 65, 83]
[9, 27, 56, 66, 100]
[10, 29, 46, 68, 86]
[11, 34, 57, 64, 86]
[3, 31, 46, 70, 99]
[16, 36, 43, 75, 92]
Pedro Lobito
  • 94,083
  • 31
  • 258
  • 268
  • You are adding an additional constraint to the sequence. That is one way to make sense out of the OP's bad question. It is the OP's fault that the question was not clear, but I am not sure whether that was intended by the OP. – sawa Oct 14 '15 at 02:50
  • However, don't use global variables for such very temporal things. – sawa Oct 14 '15 at 02:51
  • 1
    This approach will fetch random numbers from the ranges 4..21, 24..41, 44..61, 64..81 and 84..101. Numbers from the gaps in-between are never returned. – Stefan Oct 14 '15 at 08:09
  • It's laughable to suggest that this would produce anything close to random sequences. Not only are there the gaps @Stefan mentioned, but forcing the selection of exactly one number in each interval is the antithesis of randomness. – Cary Swoveland Oct 15 '15 at 21:18
  • **"but forcing the selection of exactly one number in each interval"** Isn't exactly this that rand do, choose one number in each interval? I don't see your point, could you please elaborate ? – Pedro Lobito Oct 15 '15 at 21:22
  • 1
    Momentarily forgetting about the requirement that numbers cannot be too close to each other, suppose we just drew 5 numbers between 1 and 100 and wanted to compute the probability that each of the five intervals 1-20, 21-40, 41-60, 61-80 and 81-100 contained one number. To simplify the calculations I will assume sampling with replacement, which gives a close approximation to sampling with replacement (multinomial distribution). The first number can go in any of the five intervals, The second must go in one of the remaining 4 intervals, for which the probability is 4/5, (cont....) – Cary Swoveland Oct 15 '15 at 23:52
  • 1
    (...cont.) the third must go in one of the remaining 3 intervals, having a probability of 3/5, etc. Therefore, the probability of there being one randomly-drawn number in each interval equals 4!/5^4, which is about 0.038. – Cary Swoveland Oct 15 '15 at 23:54
  • If I'm not mistaken, there are [~37 million](https://www.wolframalpha.com/input/?i=87+choose+5) possibilities for these 5 numbers, but this algorithm only yields [~1.4 million](https://www.wolframalpha.com/input/?i=17%5E5) possibilities, that's just ~4%. – Stefan Oct 16 '15 at 07:30
  • My code isn't the algorithm for euro millions. The OP just need **"5 numbers, randomly generated between 1 and 100, but ensuring that no number is closer than 3."** The possibilities here doesn't matter, it's something very basic I guess you didn't understand the question properly. Nice explanation though. – Pedro Lobito Oct 16 '15 at 10:43
0

How about this random version one?

sample_result = []
arr = (1..100).to_a
diff = 15
result_len = 5
1.upto(result_len) {
    sample = arr.sample
    sample_result << sample
    (sample-diff..sample+diff).each do 
        |i| arr.delete(i)
    end
}
p sample_result.sort
sawa
  • 165,429
  • 45
  • 277
  • 381
luoluo
  • 5,353
  • 3
  • 30
  • 41
0

Here's another way to approach this.

The smallest numbers allowed are:

a = Array.new(5) { |i| i * 3 + 1 }
#=> [1, 4, 7, 10, 13]

We can increment these numbers (or this group of numbers) 100 - 13 = 87 times until we hit the end:

87.times { a[0..-1] = a[0..-1].map(&:next) }
a #=> [88, 91, 94, 97, 100]

These are the largest numbers allowed.

Instead of incrementing all elements, we can pick a random element each time and increment that one (and it's right neighbors):

def spread(size, count, step)
  arr = Array.new(count) { |i| i * step + 1 }
  (size - arr.last).times do
    i = rand(0..arr.size)
    arr[i..-1] = arr[i..-1].map(&:next)
  end
  arr
end

5.times do
  p spread(100, 5, 3)
end

Output:

[21, 42, 56, 73, 86]
[6, 21, 45, 61, 81]
[20, 33, 48, 63, 81]
[12, 38, 55, 75, 90]
[11, 29, 50, 71, 86]
[26, 44, 64, 79, 95]

Unfortunately we have to loop several times to generate the final values. This is not only slow, but also results in a non-uniform distribution:

Distribution 1

It would be better to determine 6 random offsets that sum to 87 and move the elements accordingly. Why 6? Because the offsets are the distances between our 5 numbers, i.e.:

       n1       n2                 n3     n4           n5            
|<-o1->|<--o2-->|<-------o3------->|<-o4->|<----o5---->|<----o6---->|
0                                                                  max

This helper method returns such offsets: (stolen from here)

def offsets(size, count)
  offsets = Array.new(count) { rand(0..size) }.sort
  [0, *offsets, size].each_cons(2).map { |a, b| b - a }
end

o = offsets(87, 5) #=> [3, 0, 15, 4, 64, 1]
o.inject(:+)       #=> 87

We can add the offsets to our initial number array:

def spread(size, count, step)
  arr = Array.new(count) { |i| i * step }
  offsets(size - arr.last, count).each_with_index do |offset, index|
    arr[index..-1] = arr[index..-1].map { |i| i + offset }
  end
  arr
end

5.times do
  p spread(99, 5, 3)
end

Output:

[1, 14, 48, 60, 94]
[12, 46, 54, 67, 72]
[8, 14, 35, 40, 45]
[27, 30, 51, 81, 94]
[63, 79, 86, 90, 96]

As expected, this results in a random distribution:

Distribution 2

That looks better. Note that these results are zero based.

We can even remove the initial array and calculate the final values from the offsets. Starting with the first offset, we just have to add each following offset plus 3:

def spread(size, count, step)
  offs = offsets(size - (count - 1) * step, count)
  (1...offs.size).each { |i| offs[i] += offs[i-1] + step }
  offs[0, count]
end
Community
  • 1
  • 1
Stefan
  • 109,145
  • 14
  • 143
  • 218