This is the randomest constructed way possible I can think of.
import random
ranges = [range(0, 10), range(10, 20), range(50, 70), range(0, 20)]
total = 100
# Initialize an output list with the first constraint for each element (x = min).
answer = [r.start for r in ranges]
# Find out the maximum each element in the list can take.
max_to_add = [r.stop - r.start - 1 for r in ranges]
# Some sanity checks.
if sum(answer) > total:
raise RuntimeError("The ranges have too high start values that collectively exceed the total")
if sum(answer) + sum(max_to_add) < total:
raise RuntimeError("The ranges can never make up the total")
# Keep trying till we satisfy the sum constraint.
while total != sum(answer):
# Get a random index in the list to add on.
i = random.randint(0, len(max_to_add) - 1)
# Get a random number that doesn't break our constraint (x < max).
random_to_add = random.randint(0, max_to_add[i])
# Make sure the random we choose doesn't make us go past the total sum.
random_to_add = min(random_to_add, total - sum(answer))
# Update the constraint for that element.
max_to_add[i] -= random_to_add
# Add the random number to the element.
answer[i] += random_to_add
print("Random list:", answer)
print("Total sum:", sum(answer))
You can see this code will struggle when the ranges tightly make up to the total number or a little bit more:
# Try raising total to 10000 for example and it will struggle more.
total = 100
# With these ranges we should get answer list of [1, 1, ...., 1] due to the constraints we have.
ranges = [range(0, 2)] * total
One possible solution for this is to store another list of indices that can be added to and use random.choice to pick from it (instead of random.randint(0, len(max_to_add) - 1))
Update:
This is the version for considering only the elements we can actually add to:
import random
ranges = [range(0, 10), range(10, 20), range(50, 70), range(0, 20)]
total = 100
# Initialize an output list with the first constraint for each element (x = min).
answer = [r.start for r in ranges]
# Find out the maximum each element in the list can take.
max_to_add = [r.stop - r.start - 1 for r in ranges]
still_can_add_to = list(range(len(max_to_add)))
# Some sanity checks.
if sum(answer) > total:
raise RuntimeError("The ranges have too high start values that collectively exceed the total")
if sum(answer) + sum(max_to_add) < total:
raise RuntimeError("The ranges can never make up the total")
# Keep trying till we satisfy the sum constraint.
while total != sum(answer):
# Get a random index in the list to add on.
i = random.choice(still_can_add_to)
# Get a random number that doesn't break our constraint (x < max).
random_to_add = random.randint(0, max_to_add[i])
# Make sure the random we choose doesn't make us go past the total sum.
random_to_add = min(random_to_add, total - sum(answer))
# Update the constraint for that element.
max_to_add[i] -= random_to_add
# Remove this element from the list of actively checked elements that we add to.
if max_to_add[i] == 0:
still_can_add_to.remove(i)
# Add the random number to the element.
answer[i] += random_to_add
print("Random list:", answer)
print("Total sum:", sum(answer))
This is an optimization that shines in cases when the ranges tightly make up to the total number:
total = 10000
# With these ranges we should get answer list of [1, 1, ...., 1] due to the constraints we have.
ranges = [range(0, 2)] * total
In the example above, the updated version takes about 3s whereas the old one takes about 13s.