3

I want to find all possible combinations of n numbers such that the sum is = 100 in Python

A sample of 2 numbers:

x=[]
for i, j in itertools.product(range(0,101), range(0,101)):
    if i+j==100:
       x.append([i,j])

Any alternative and clever way to do this with a variable number of iteration variables and get the outcome in the form of this: n=5:

[[10,10,10,30,40], [100,0,0,0,0], [1,1,2,3,97] .......]
lrh09
  • 452
  • 3
  • 11
  • Are you looking for increased performance, or just alternative methods? – jpp Feb 10 '18 at 10:28
  • @jp_data_analysis yeap I am looking for generating these fast and store it in a txt file and everytime I need to refer to them as the indices of calculation, I will be able to pull these things out. The number of variables could go up to few hundred, to sum up to something like 10,000 appreciate if there is any help. – lrh09 Feb 10 '18 at 14:49

3 Answers3

3

itertools.product takes a repeat argument, which you can use to repeat the range(1, 101) iterator repeat number of times. This way you don't need to specify the iterator multiple times or generate the desired number of arguments. For example, for 5 times:

[i for i in itertools.product(range(1, 101), repeat=5) if sum(i) == 100]
HoldOffHunger
  • 10,963
  • 6
  • 53
  • 100
heemayl
  • 32,535
  • 3
  • 52
  • 57
3

A Pure Python Solution (i.e. without itertools.product)

The main difficulty here is executing a variable number of for-loops inside a function. The way we can get around this easily is using recursion which involves a function calling itself.

If we use recursion, then inside any instance of the function, only one for-loop is actually iterated through. So to apply this to the problem at hand, we want our function to take two parameters: the target for what number we are trying to sum to, and n - the number of positive integers we have available to use.

Each function will then return (given a target and n numbers), all the combinations that will make that target - in the form of a two-dimensional list.

The only special case that we must consider is the "leaf nodes" of our recursive tree (the cases where we have a certain target, but n == 1, so we only have one number to make the target with). This is easy to handle, we just need to remember that we should always return all combinations that make the target so in this case, there is only one "combination" which is the target.

Then (if n > 1) the rest is self explanatory, we are simply looping through every number less than target and adding to a list of combinations (cs) with the results of calling the function again.

However, before we concatenate these combos onto our list, we need to use a comprehension to add i (the next number) to the start of every combination.

And that's it! Hopefully you can see how the above translates into the following code:

def combos(target, n):
    if n == 1:
        return [[target]]
    cs = []
    for i in range(0, target+1):
        cs += [[i]+c for c in combos(target-i, n-1)]
    return cs

and a test (with target as 10 and n as 3 to make it clearer) shows it works:

>>> combos(10, 3)
[[0, 0, 10], [0, 1, 9], [0, 2, 8], [0, 3, 7], [0, 4, 6], [0, 5, 5], [0, 6, 4], [0, 7, 3], [0, 8, 2], [0, 9, 1], [0, 10, 0], [1, 0, 9], [1, 1, 8], [1, 2, 7], [1, 3, 6], [1, 4, 5], [1, 5, 4], [1, 6, 3], [1, 7, 2], [1, 8, 1], [1, 9, 0], [2, 0, 8], [2, 1, 7], [2, 2, 6], [2, 3, 5], [2, 4, 4], [2, 5, 3], [2, 6, 2], [2, 7, 1], [2, 8, 0], [3, 0, 7], [3, 1, 6], [3, 2, 5], [3, 3, 4], [3, 4, 3], [3, 5, 2], [3, 6, 1], [3, 7, 0], [4, 0, 6], [4, 1, 5], [4, 2, 4], [4, 3, 3], [4, 4, 2], [4, 5, 1], [4, 6, 0], [5, 0, 5], [5, 1, 4], [5, 2, 3], [5, 3, 2], [5, 4, 1], [5, 5, 0], [6, 0, 4], [6, 1, 3], [6, 2, 2], [6, 3, 1], [6, 4, 0], [7, 0, 3], [7, 1, 2], [7, 2, 1], [7, 3, 0], [8, 0, 2], [8, 1, 1], [8, 2, 0], [9, 0, 1], [9, 1, 0], [10, 0, 0]]

Improving performance

If we consider the case where we are trying to make 10 with 4 numbers. At one point, the function will be called with a target of 6 after say 1 and 3. The algorithm will as we have already explained and return the combinations using 2 numbers to make 6. However, if we now consider another case further down the line when the function is asked to give the combinations that make 6 (same as before) having been called with say 2 and 2. Notice how even though we will get the right answer (through our recursion and the for-loop), we will return the same combinations as before - when we were called with 1 and 3. Furthermore, this scenario will happen extremely often: the function will be called from different situations but be asked to give the same combinations that have already been previously calculated at a different time.

This gives way to a great optimisation technique called memoization which essentially just means storing the results of our function as a key: value pair in a dictionary (mem) before returning.

We then just check at the start of every function call if we have ever been called before with the same parameters (by seeing if the key is in the dictionary) and if it is, then we can just return the result we got last time.

This speeds up the algorithm dramatically.

mem = {}
def combos(target, n):
    k = (target, n)
    if k in mem:
        return mem[k]
    if n == 1:
        return [[target]]
    cs = []
    for i in range(0, target+1):
        cs += [[i]+c for c in combos(target-i, n-1)]
    mem[k] = cs
    return cs
Joe Iddon
  • 18,600
  • 5
  • 29
  • 49
  • the algorithm is so pure that it takes very long time to finish a run of large number of variables. Is there any better alternative? But your cotent is very useful here. Thanks – lrh09 Feb 10 '18 at 14:50
  • How large is large? I think you are underestimating just how long a list with `5` elements would be! I don't think that anything will work much better than this, sorry :/ – Joe Iddon Feb 10 '18 at 14:52
  • 100 variables that sum to 10,000? I was trying 50 variable, having a looooong time to compile and not done yet :/ – lrh09 Feb 10 '18 at 15:03
  • That's ridiculous!!! Without optimisation (the `itertools` approach) you would be getting onto about `100!` itertations: that's a **gigantic** amount - `~ 10 ^ 157` to put that into comparison, there is estimated `10 ^ 80` **atoms in the observable universe**! No way will any computer ever be able to do what you want! – Joe Iddon Feb 10 '18 at 15:16
  • yeap, totally agree. Hence wanting to know if there is any optimizations on these to make the computing time manageable – lrh09 Feb 10 '18 at 15:34
  • can you also add another edit using the second approach to only give x1 – lrh09 Feb 11 '18 at 09:26
2

This is a trivial generalisation / partial optimisation of your algorithm.

Edit: @heemayl's alternative which uses repeat is preferable to this solution.

import itertools

n = 3
x = []

x = [list(i) for i in itertools.product(*(range(0,101) \
             for _ in range(n))) if sum(i) == 100]
jpp
  • 134,728
  • 29
  • 196
  • 240