0

I am trying to create a function where:

  1. The output list is generated from random numbers from the input list
  2. The output list is a specified length and adds to a specified sum

ex. I specify that I want a list that is 4 in length and adds up to 10. random numbers are pulled from the input list until the criteria is satisfied.

I feel like I am approaching this problem all wrong trying to use recursion. Any help will be greatly appreciated!!!

EDIT: for more context on this problem.... Its going to be a random enemy generator.

The end goal input list will be coming from a column in a CSV called XP. (I plan to use pandas module). But this CSV will have a list of enemy names in the one column, XP in another, Health in another, etc. So the end goal is to be able to specify the total number of enemies and what the sum XP should be between those enemies and have the list generate with the appropriate information. For ex. 5 enemies with a total of 200 XP between them. The result is maybe -> Apprentice Wizard(50 xp), Apprentice Wizard(50 xp), Grung(50), Xvart(25 xp), Xvart(25 xp). The output list will actually need to include all of the row information for the selected items. And it is totally fine to have duplicated in the output as seen in this example. That will actually make more sense in the narrative of the game that this is for.

The csv --> https://docs.google.com/spreadsheets/d/1PjnN00bikJfY7mO3xt4nV5Ua1yOIsh8DycGqed6hWD8/edit?usp=sharing

import random
from random import *

lis = [1,2,3,4,5,6,7,8,9,10]

output = []

def query (total, numReturns, myList, counter):

    random_index = randrange(len(myList)-1)
    i = myList[random_index]
    h = myList[i]

    # if the problem hasn't been solved yet...
    if len(output) != numReturns and sum(output) != total:
        print(output)

        # if the length of the list is 0 (if we just started), then go ahead and add h to the output
        if len(output) == 0 and sum(output) + h != total:
            output.append(h)
            query (total, numReturns, myList, counter)


        #if the length of the output is greater than 0
        if len(output) > 0:

            # if the length plus 1 is less than or equal to the number numReturns
            if len(output) +1 <= numReturns:
                print(output)

                #if the sum of list plus h is greater than the total..then h is too big. We need to try another number
                if sum(output) + h > total:
                    # start counter

                    for i in myList:#  try all numbers in myList...
                        print(output)
                        print ("counter is ", counter, " and i is", i)
                        counter += 1
                        print(counter)

                        if sum(output) + i == total:
                            output.append(i)
                            counter = 0
                            break
                        if sum(output) + i != total:
                           pass
                        if counter == len(myList):
                            del(output[-1]) #delete last item in list
                            print(output)
                            counter = 0 # reset the counter
                    else:
                        pass

                #if the sum of list plus h is less than the total
                if sum(output) + h < total:
                    output.append(h) # add h to the list

        print(output)
        query (total, numReturns, myList, counter)


    if len(output) == numReturns and sum(output) == total:
        print(output, 'It worked')
    else:
        print ("it did not work")


query(10, 4, lis, 0)

B-L
  • 144
  • 1
  • 8
  • Can the numbers repeat? e.g. is `[2, 2, 3, 3]` a valid solution for `query(10, 4, lis, 0)`? – Nick Mar 14 '20 at 02:27
  • Hi Nick, yes the numbers can repeat. The [2, 2, 3, 3] is a perfect solution for what I am looking for. Thanks! – B-L Mar 14 '20 at 03:25
  • The answer you've accepted will not generate `[2, 2, 3, 3]` as a solution. – Nick Mar 14 '20 at 03:49
  • Thank you for catching that Nick, I did not realize. Having repeating numbers is actually preferable in this situation. – B-L Mar 14 '20 at 03:55
  • 1
    This appears to be an incarnation of the knapsack problem – Mad Physicist Mar 14 '20 at 05:27
  • Very cool, I will be looking into the knapsack problem today. Thanks for the help Mad Physicist! – B-L Mar 14 '20 at 15:54

2 Answers2

1

I guess that it would be better to get first all n-size combinations of given array which adds to specified number, and then randomly select one of them. Random selecting and checking if sum is equal to specified value, in pessimistic scenario, can last indefinitely.

from itertools import combinations as comb
from random import randint

x = [1,1,2,4,3,1,5,2,6]

def query(arr, total, size):
  combs = [c for c in list(comb(arr, size)) if sum(c)==total]
  return combs[randint(0, len(combs))]

#example 4-item array with items from x, which adds to 10
print(query(x, 10, 4))


xana
  • 499
  • 3
  • 13
  • You don't actually want to generate the combinations. You just need to know the number (N), and have a deterministic way to map a number in [0, N) to a combination. Then all you need to do is to generate a single random number. – Mad Physicist Mar 14 '20 at 03:01
  • @MadPhysicist Any chance you could write that up as an answer? I'm curious as to how it would work. – Nick Mar 14 '20 at 03:30
  • This won't generate `[2,2,3,3]` as a solution. See the comments to the question. – Nick Mar 14 '20 at 03:33
  • 1
    I might have to solve the knapsack problem to do that... :) – Mad Physicist Mar 14 '20 at 05:26
  • You can also build generator with counter to generate n-size combinations, and put the random number which will indicate when to return, in order to not generating all combinations. If you want possibility to repeat numbers e.g [2,2,3,3] from [1,2,3,...,10] then you can use numpy repeat, to repeat all numbers in list n times, e.g for wanted list with size 4, input list modify to [1,1,1,1,2,2,2,2,....,10,10,10,10], then extract combinations. – xana Mar 14 '20 at 13:29
  • Pierogi, your solution is very clever. I really appreciate the help! I will be testing it out today. – B-L Mar 14 '20 at 15:55
0

If the numbers in your input list are consecutive numbers, then this is equivalent to the problem of choosing a uniform random output list of N integers in the range [min, max], where the output list is ordered randomly and min and max are the smallest and largest number in the input list. The Python code below shows how this can be solved. It has the following advantages:

  • It does not use rejection sampling.
  • It chooses uniformly at random from among all combinations that meet the requirements.

It's based on an algorithm by John McClane, which he posted as an answer to another question. I describe the algorithm in another answer.

import random # Or secrets

def _getSolTable(n, mn, mx, sum):
        t = [[0 for i in range(sum + 1)] for j in range(n + 1)]
        t[0][0] = 1
        for i in range(1, n + 1):
            for j in range(0, sum + 1):
                jm = max(j - (mx - mn), 0)
                v = 0
                for k in range(jm, j + 1):
                    v += t[i - 1][k]
                t[i][j] = v
        return t

def intsInRangeWithSum(numSamples, numPerSample, mn, mx, sum):
        """ Generates one or more combinations of
           'numPerSample' numbers each, where each
           combination's numbers sum to 'sum' and are listed
           in any order, and each
           number is in the interval '[mn, mx]'.
            The combinations are chosen uniformly at random.
               'mn', 'mx', and
           'sum' may not be negative.  Returns an empty
           list if 'numSamples' is zero.
            The algorithm is thanks to a _Stack Overflow_
          answer (`questions/61393463`) by John McClane.
          Raises an error if there is no solution for the given
          parameters.  """
        adjsum = sum - numPerSample * mn
        # Min, max, sum negative
        if mn < 0 or mx < 0 or sum < 0:
            raise ValueError
        # No solution
        if numPerSample * mx < sum:
            raise ValueError
        if numPerSample * mn > sum:
            raise ValueError
        if numSamples == 0:
            return []
        # One solution
        if numPerSample * mx == sum:
            return [[mx for i in range(numPerSample)] for i in range(numSamples)]
        if numPerSample * mn == sum:
            return [[mn for i in range(numPerSample)] for i in range(numSamples)]
        samples = [None for i in range(numSamples)]
        table = _getSolTable(numPerSample, mn, mx, adjsum)
        for sample in range(numSamples):
            s = adjsum
            ret = [0 for i in range(numPerSample)]
            for ib in range(numPerSample):
                i = numPerSample - 1 - ib
                # Or secrets.randbelow(table[i + 1][s])
                v = random.randint(0, table[i + 1][s] - 1)
                r = mn
                v -= table[i][s]
                while v >= 0:
                    s -= 1
                    r += 1
                    v -= table[i][s]
                ret[i] = r
            samples[sample] = ret
        return samples

Example:

weights=intsInRangeWithSum(
   # One sample
   1,
   # Count of numbers per sample
   4,
   # Range of the random numbers
   1, 5,
   # Sum of the numbers
   10)
# Divide by 100 to get weights that sum to 1
weights=[x/20.0 for x in weights[0]]
Peter O.
  • 32,158
  • 14
  • 82
  • 96