5

Imagine you're trying to allocate some fixed resources (e.g. n=10) over some number of territories (e.g. t=5). I am trying to find out efficiently how to get all the combinations where the sum is n or below.

E.g. 10,0,0,0,0 is good, as well as 0,0,5,5,0 etc., while 3,3,3,3,3,3 is obviously wrong.

I got this far:

import itertools
t = 5
n = 10
r = [range(n+1)] * t
for x in itertools.product(*r): 
   if sum(x) <= n:          
       print x

This brute force approach is incredibly slow though; there must be a better way?

Timings (1000 iterations):

Default (itertools.product)           --- time: 40.90 s
falsetru recursion                    --- time:  3.63 s
Aaron Williams Algorithm (impl, Tony) --- time:  0.37 s
PascalVKooten
  • 20,643
  • 17
  • 103
  • 160
  • You want to find all sum combinations of a number? – GLHF Dec 21 '14 at 03:16
  • @qqvc Sounds about right? – PascalVKooten Dec 21 '14 at 03:17
  • Possible duplicate of [this one](http://stackoverflow.com/questions/14162798/generating-all-distinct-partitions-of-a-number)? The task here is to find all the [partitions of an integer](http://en.wikipedia.org/wiki/Partition_%28number_theory%29). – Tony Dec 21 '14 at 03:26
  • @Tony While some ideas might apply, the question there is find a value equal to, while this question is finding all up to and including. Furthermore, it is an answer given in another language. I'm feeling like there must be some standard module offering this? – PascalVKooten Dec 21 '14 at 03:40

4 Answers4

3

Possible approach follows. Definitely would use with caution (hardly tested at all, but the results on n=10 and t=5 look reasonable).

The approach involves no recursion. The algorithm to generate partitions of a number n (10 in your example) having m elements (5 in your example) comes from Knuth's 4th volume. Each partition is then zero-extended if necessary, and all the distinct permutations are generated using an algorithm from Aaron Williams which I have seen referred to elsewhere. Both algorithms had to be translated to Python, and that increases the chance that errors have crept in. The Williams algorithm wanted a linked list, which I had to fake with a 2D array to avoid writing a linked-list class.

There goes an afternoon!

Code (note your n is my maxn and your t is my p):

import itertools

def visit(a, m):
    """ Utility function to add partition to the list"""
    x.append(a[1:m+1])

def parts(a, n, m):
    """ Knuth Algorithm H, Combinatorial Algorithms, Pre-Fascicle 3B
        Finds all partitions of n having exactly m elements.
        An upper bound on running time is (3 x number of
        partitions found) + m.  Not recursive!      
    """
    while (1):
        visit(a, m)
        while a[2] < a[1]-1:
            a[1] -= 1
            a[2] += 1
            visit(a, m)
        j=3
        s = a[1]+a[2]-1
        while a[j] >= a[1]-1:
            s += a[j]
            j += 1
        if j > m:
            break
        x = a[j] + 1
        a[j] = x
        j -= 1
        while j>1:
            a[j] = x
            s -= x
            j -= 1
            a[1] = s

def distinct_perms(partition):
    """ Aaron Williams Algorithm 1, "Loopless Generation of Multiset
        Permutations by Prefix Shifts".  Finds all distinct permutations
        of a list with repeated items.  I don't follow the paper all that
        well, but it _possibly_ has a running time which is proportional
        to the number of permutations (with 3 shift operations for each  
        permutation on average).  Not recursive!
    """

    perms = []
    val = 0
    nxt = 1
    l1 = [[partition[i],i+1] for i in range(len(partition))]
    l1[-1][nxt] = None
    #print(l1)
    head = 0
    i = len(l1)-2
    afteri = i+1
    tmp = []
    tmp += [l1[head][val]]
    c = head
    while l1[c][nxt] != None:
        tmp += [l1[l1[c][nxt]][val]]
        c = l1[c][nxt]
    perms.extend([tmp])
    while (l1[afteri][nxt] != None) or (l1[afteri][val] < l1[head][val]):
        if (l1[afteri][nxt] != None) and (l1[i][val]>=l1[l1[afteri][nxt]][val]):
            beforek = afteri
        else:
            beforek = i
        k = l1[beforek][nxt]
        l1[beforek][nxt] = l1[k][nxt]
        l1[k][nxt] = head
        if l1[k][val] < l1[head][val]:
            i = k
        afteri = l1[i][nxt]
        head = k
        tmp = []
        tmp += [l1[head][val]]
        c = head
        while l1[c][nxt] != None:
            tmp += [l1[l1[c][nxt]][val]]
            c = l1[c][nxt]
        perms.extend([tmp])

    return perms

maxn = 10 # max integer to find partitions of
p = 5  # max number of items in each partition

# Find all partitions of length p or less adding up
# to maxn or less

# Special cases (Knuth's algorithm requires n and m >= 2)
x = [[i] for i in range(maxn+1)]
# Main cases: runs parts fn (maxn^2+maxn)/2 times
for i in range(2, maxn+1):
    for j in range(2, min(p+1, i+1)):
        m = j
        n = i
        a = [0, n-m+1] + [1] * (m-1) + [-1] + [0] * (n-m-1)
        parts(a, n, m)
y = []
# For each partition, add zeros if necessary and then find
# distinct permutations.  Runs distinct_perms function once
# for each partition.
for part in x:
    if len(part) < p:
        y += distinct_perms(part + [0] * (p - len(part)))
    else:
        y += distinct_perms(part)
print(y)
print(len(y))
Community
  • 1
  • 1
Tony
  • 1,645
  • 1
  • 10
  • 13
  • Zero is indeed permitted. Also, this regards all "territories" to be exactly the same (ordering does not matter) while in fact for my application it does. That's why I believe it to be a bit of a different problem? Other than that, it is very efficient indeed :-) – PascalVKooten Dec 21 '14 at 03:50
  • @jterrace is right. Given the unique results above, you can generate the distinct permutations of each. It will probably be faster to do that than to use any other approach. There are [other](http://stackoverflow.com/questions/6284396/permutations-with-unique-values) [posts](http://stackoverflow.com/questions/15592299/generating-unique-permutations-in-python) which show how to get distinct permutations. – Tony Dec 21 '14 at 03:53
  • This can yield `[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]`. – falsetru Dec 21 '14 at 04:00
  • @falsetru That's a valid result. – PascalVKooten Dec 21 '14 at 04:02
  • 1
    @PascalvKooten, What about `t=5`? (**over some number of territories (e.g. t=5)** in the questino) – falsetru Dec 21 '14 at 04:02
  • @falsetru My bad, I'm mixing up `t` and `n` myself. – PascalVKooten Dec 21 '14 at 04:05
  • @falsetru. Quite right. I hadn't sorted that out. Will shortly remove my response, and think about it again. – Tony Dec 21 '14 at 04:07
  • Edited in response to @falsetru comments and Pascal's desire to have ordering matter. – Tony Dec 21 '14 at 09:08
  • Nice effort, impressive result in the end! I'm posting timings. +1 – PascalVKooten Dec 21 '14 at 17:37
  • Aaron Williams algorithm is now available in Python at https://github.com/ekg/multipermute. – Bill Bell Aug 26 '15 at 18:51
3

Make your own recursive function which do not recurse with an element unless it's possible to make a sum <= 10.

def f(r, n, t, acc=[]):
    if t == 0:
        if n >= 0:
            yield acc
        return
    for x in r:
        if x > n:  # <---- do not recurse if sum is larger than `n`
            break
        for lst in f(r, n-x, t-1, acc + [x]):
            yield lst

t = 5
n = 10
for xs in f(range(n+1), n, 5):
    print xs
falsetru
  • 357,413
  • 63
  • 732
  • 636
  • Your solution is really quite amazing as well. So concise, it was really easy to mod so that I can replace `[1, 2, 3, ..., n]` (`range(n+1)`) with some pre pruning like `[1, 2, n/4, n/2, n]`! – PascalVKooten Dec 22 '14 at 15:00
2

You can create all the permutations with itertools, and parse the results with numpy.

>>> import numpy as np
>>> from itertools import product

>>> t = 5
>>> n = 10
>>> r = range(n+1)

# Create the product numpy array
>>> prod = np.fromiter(product(r, repeat=t), np.dtype('u1,' * t))
>>> prod = prod.view('u1').reshape(-1, t)

# Extract only permutations that satisfy a condition
>>> prod[prod.sum(axis=1) < n]

Timeit:

>>> %%timeit 
    prod = np.fromiter(product(r, repeat=t), np.dtype('u1,' * t))
    prod = prod.view('u1').reshape(-1, t)
    prod[prod.sum(axis=1) < n]

10 loops, best of 3: 41.6 ms per loop

You could even speed up the product computation by populating combinations directly in numpy.

Community
  • 1
  • 1
Imanol Luengo
  • 15,366
  • 2
  • 49
  • 67
  • 1
    That way it is slower than the default method? `98ms` vs `40.9ms` per iteration. – PascalVKooten Dec 21 '14 at 18:58
  • 1
    In fact, on my machine it takes 2 minutes and 29 seconds to run 1000 iterations. – PascalVKooten Dec 21 '14 at 19:00
  • What do you mean by iteration? Doing that calculation 1000 times? If thats what you mean, you only need to to the first line once, and 1000 times the second line. – Imanol Luengo Dec 21 '14 at 19:11
  • Yea, it's not the clearest to use "iteration" here. I just meant when we run it a 1000 times (changing `s` to `ms` will give you a timing per single run). It takes 3.13s in total (so that means 3.13 ms for one iteration). However, creating and handling the combinations is also part of the timing. Imagine you change `t` or `n`, you'd have to create new combinations. While it might look useful, you're basically just creating extra overhead? – PascalVKooten Dec 22 '14 at 07:50
  • Ok, I got it wront then. I thought you meant the mean timing of 1000 iterations. Then numpy is not the best choice here, is rly fast to compute the condition over the array, but it requires the creation of the array with all the permutations (quite slow). I just updated to a sightly faster answer. However, other approaches are still faster as explained above. Sorry fot the missunderstanding ;P – Imanol Luengo Dec 22 '14 at 10:34
0

You could optimize the algorithm using Dynamic Programming.

Basically, have an array a, where a[i][j] means "Can I get a sum of j with the elements up to the j-th element (and using the jth element, assuming you have your elements in an array t (not the number you mentioned)).

Then you can fill the array doing

a[0][t[0]] = True
for i in range(1, len(t)):
    a[i][t[i]] = True
    for j in range(t[i]+1, n+1):
         for k in range(0, i):
             if a[k][j-t[i]]:
                 a[i][j] = True

Then, using this info, you could backtrack the solution :)

def backtrack(j = len(t)-1, goal = n):
    print j, goal
    all_solutions = []
    if j == -1:
       return []
    if goal == t[j]:
       all_solutions.append([j])
    for i in range(j-1, -1, -1):
       if a[i][goal-t[j]]:
          r = backtrack(i, goal - t[j])
          for l in r:
              print l
              l.append(j)
              all_solutions.append(l)
    all_solutions.extend(backtrack(j-1, goal))
    return all_solutions


 backtrack() # is the answer
spalac24
  • 1,076
  • 1
  • 7
  • 16