4

I have a code for a random colour chooser but not a random NUMBER. I need it to choose a random number 15 times each between 5 and 20, but the twist is that the total of all 15 numbers has to be equal to 227

I have a code for a random colour generator, but I can't figure out how to generate numbers and how to get a total of 227

import turtle
import random
turtle.bgcolor("black")
t = turtle.Pen()

t.width(4)
colours = ["yellow", "orange", "red"]
for i in range(60):
    t.pencolor(colours[random.randint(0,2)])
    t.forward(100)
    t.back(100)
    t.left(6)
tevemadar
  • 12,389
  • 3
  • 21
  • 49
  • 1
    It's sort of a duplicate, but the unusual thing about this problem is that the expected value of 15 numbers between 5 and 20 is 187.5, well under 227. So this constraint seems pretty significant. More importantly, though, that is a great question and answer in general. – aschultz Aug 19 '19 at 12:55
  • @aschultz I agree that the constraints make this a different question, and have voted to reopen. – pjs Aug 21 '19 at 15:34

5 Answers5

1

Let's analyse the problem:

We will be generating 14 numbers and adding the 15th manually to get the correct sum.

But I would change @jfaccioni's approach of generating all numbers at once and restart the loop earlier: e.g. if we generate 5 5 times, we already know we can't get the correct sum! (5*5+20*(15-5) < 227)

def rng_list():
    while True:
        rng_list = []
        current_sum = 0
        for i in range(14):
            r = random.randint(5, 20)
            rng_list.append(r)
            current_sum+= r
            if not 5*(14-i) <= 227-current_sum <= 20*(14-i): #`i` goes from 0 to 14, so 14-i is how many (from 15) numbers are still not calculated
                break
        if len(rng_list) == 14:
            rng_list.append(227-current_sum)
            return rng_list
        print("Pass failed with list {}, sum {} - trying again.".format(rng_list, current_sum)) #added to debug/show how it works

I've added the print to monitor how it works when writing, you can change it to a counter how many times you needed to re-roll your list. But right now you can also see how failed attempts are of different length, as we monitor the current sum every time!

>>> rng_list()
Pass failed with list [12, 7, 18, 20, 12, 12, 15, 7, 15, 16, 7], sum 141 - trying again.
Pass failed with list [17, 6, 20, 5, 16, 18, 5, 19, 19, 7, 8], sum 140 - trying again.
Pass failed with list [10, 15, 18, 10, 8, 8, 12, 12, 13], sum 106 - trying again.
Pass failed with list [9, 12, 8, 5, 17, 20, 20, 6, 8], sum 105 - trying again.
Pass failed with list [9, 16, 9, 16, 6, 17, 20, 15, 9, 11, 15], sum 143 - trying again.
Pass failed with list [5, 14, 13, 12, 12, 13, 13, 9, 8], sum 99 - trying again.
Pass failed with list [11, 9, 5, 11, 11, 13, 18, 7], sum 85 - trying again.
Pass failed with list [10, 12, 19, 9, 14, 16, 11, 19, 5, 5], sum 120 - trying again.
Pass failed with list [11, 10, 8, 10, 10, 17, 17, 9, 19, 14], sum 125 - trying again.
Pass failed with list [13, 11, 7, 15, 14, 7, 5, 10], sum 82 - trying again.
Pass failed with list [11, 6, 7, 20, 6, 17, 18, 12, 8], sum 105 - trying again.
Pass failed with list [16, 17, 9, 18, 7, 8, 17, 14, 13, 13, 14], sum 146 - trying again.

[19, 6, 18, 9, 19, 20, 15, 14, 16, 15, 11, 18, 11, 20, 16]

>>> rng_list()
Pass failed with list [17, 6, 8, 9, 14, 17, 13, 8, 10], sum 102 - trying again.
Pass failed with list [7, 16, 12, 8, 20, 19, 18, 15, 5, 5], sum 125 - trying again.
Pass failed with list [5, 7, 6, 15, 12, 17, 6, 10], sum 78 - trying again.
Pass failed with list [17, 18, 8, 17, 18, 6, 10, 16, 18, 18, 6, 5], sum 157 - trying again.
Pass failed with list [10, 13, 9, 11, 11, 5, 18, 17, 13, 12], sum 119 - trying again.
Pass failed with list [20, 8, 8, 7, 14, 16, 17, 15, 15, 19, 13, 15, 17], sum 184 - trying again.
Pass failed with list [19, 12, 10, 15, 12, 13, 20, 14, 12, 6, 9], sum 142 - trying again.
Pass failed with list [9, 5, 13, 10, 15, 10, 13, 14, 7], sum 96 - trying again.
Pass failed with list [15, 12, 5, 19, 6, 5, 5, 17], sum 84 - trying again.
Pass failed with list [8, 5, 7, 11, 15, 16, 12, 18, 13], sum 105 - trying again.
Pass failed with list [15, 14, 10, 9, 8, 6, 10, 15, 18], sum 105 - trying again.
Pass failed with list [14, 17, 10, 13, 16, 8, 5, 6, 14], sum 103 - trying again.
Pass failed with list [10, 12, 19, 9, 5, 18, 12, 8, 9], sum 102 - trying again.
Pass failed with list [15, 10, 11, 19, 12, 12, 18, 15, 13, 8, 19, 11], sum 163 - trying again.
Pass failed with list [10, 20, 17, 11, 20, 11, 14, 13, 18, 5, 5], sum 144 - trying again.
Pass failed with list [20, 8, 11, 16, 18, 16, 15, 12, 9, 14, 15, 18, 13], sum 185 - trying again.
Pass failed with list [16, 7, 20, 11, 12, 16, 11, 9, 5, 13], sum 120 - trying again.
Pass failed with list [10, 12, 19, 9, 14, 15, 17, 19, 7, 11, 17, 17, 7], sum 174 - trying again.
Pass failed with list [5, 5, 6, 12, 10, 16, 10], sum 64 - trying again.
Pass failed with list [16, 18, 20, 14, 20, 19, 16, 7, 5, 12, 9, 11, 15], sum 182 - trying again.
Pass failed with list [14, 7, 13, 15, 16, 12, 20, 5, 5, 13], sum 120 - trying again.
Pass failed with list [17, 16, 9, 20, 13, 9, 9, 17, 19, 19, 7, 13, 18], sum 186 - trying again.
Pass failed with list [16, 11, 18, 17, 14, 16, 9, 10, 14, 19, 17, 6, 17], sum 184 - trying again.
Pass failed with list [12, 9, 9, 16, 10, 12, 18, 17, 16, 12, 18, 15], sum 164 - trying again.

[15, 14, 11, 14, 13, 13, 18, 19, 19, 13, 14, 6, 19, 16, 23]

>>> rng_list()

[13, 8, 15, 5, 17, 19, 14, 15, 17, 19, 20, 14, 17, 15, 19]

(Added empty lines in output for better readability.)

h4z3
  • 5,265
  • 1
  • 15
  • 29
1

Well, for such kind of problems multinomial distribution is the right answer - sum would be by definition equal to desired number. In Python it is pretty much one liner

q = np.random.multinomial(227, [1/15.]*15)                                                                          
np.sum(q)  

will print

227

Then you could apply rejection/acceptance technique:

if np.any(q < 5):
    # reject and start again

if np.any(q > 20):
    # reject and start again

# accept, do something with the sampled array

Or in function form

import numpy as np

def sample(nof_samples, min, max, sum):
    p = np.full(nof_samples, 1.0/np.float64(nof_samples)) # probabilities

    while True:
        q = np.random.multinomial(sum, p)
        if not np.any(q > max):
            if not np.any(q < min):
                return q

t = sample(15, 5, 20, 227)
print(t)

t = sample(15, 5, 20, 227)
print(t)

t = sample(15, 5, 20, 227)
print(t)
Severin Pappadeux
  • 18,636
  • 3
  • 38
  • 64
1

Riffing off of Severin Pappadeux's answer, you can eliminate the need for the lower bound rejection by subtracting 5 from the min and max, making them 0 and 15 for this particular problem. This reduces the target sum by nof_samples * min. Once an ensemble passes the reduced upper bound check, there's no need to check for a lower bound violation since that's now 0 and all the results are positive. We just need to translate those results by adding min back to all the values to restore them to the original range.

import numpy as np
import sys

def sample(nof_samples, min, max, sum):
    p = np.full(nof_samples, 1.0/np.float64(nof_samples)) # probabilities
    sum -= nof_samples * min
    max -= min
    if sum < 0 or sum > nof_samples * max:  # check that args have a feasible solutioon
        print('Inconceivable!')
        sys.exit()

    while True:
        q = np.random.multinomial(sum, p)
        if not np.any(q > max):
            return q + min

for _ in range(3):
    t = sample(15, 5, 20, 227)
    print(t)
    print(min(t), max(t), sum(t))  # confirm that all constraints have been met

Note that this version will also work with negative numbers as the sum and bounds:

print(sample(5, -10, 20, -1))

produces, e.g.,

[ 0  1 -1  4 -5]
pjs
  • 18,696
  • 4
  • 27
  • 56
0

If you don't care if the numbers are strictly random, you can generate 14 random numbers between 5 and 20, check if the sum of these numbers are close enough to your desired total of 227 (so that adding another number between 5 and 20 would reach 227) and add the corresponding "missing" number.

The following function does exactly that:

def rng_list(): 
    while True: 
        rng_list = [random.randint(5, 20) for _ in range(14)] 
        if 207 <= sum(rng_list) <= 222: 
            break 
    rng_list.append(227 - sum(rng_list))
    return rng_list

This function is not really optimized, since it may take quite a few tries to come up with a list of 14 random numbers whose sum falls in the right place, but it should be fast enough for your use case.

jfaccioni
  • 7,099
  • 1
  • 9
  • 25
0

You want 15 numbers that add up to 227. 227/15 = 15.133. So start with [15, 15, ..., 15, 16, 16] which adds up to 227 (15 x 15 = 225).

Now make random changes to pairs of numbers that a) leave the total unchanged and b) do not push a number outside the 5..20 limits.

One way would be to pick 2 numbers randomly, check how far the two numbers are from the limits and use that information to pick a random size change. Add the size change to one of the pair and subtract it from the other. Then pick another pair and repeat.

ETA: Don't pick both members of the pair randomly. Use something like a Fisher-Yates shuffle where one member of the pair runs sequentially through the list, thus ensuring that every member of the list is processed at least once on every pass through the list.

rossum
  • 15,344
  • 1
  • 24
  • 38