3

I am trying to calculate all permutations with list = [0,1,2,3,4,5,6] adhering to some constraints.

  • Position 0 must be > 3
  • Position 3 + Position 5 < Position 4

With my current code I determining all permutations and iterations through each, applying the constraints.

import itertools

Used_permutations = []    
numbers = [0,1,2,3,4,5,6]
all_permutations = list(itertools.permutations(numbers)) #Determine all permutations

for permutation in all_permutations:
    if permutation[0] > 3 and permutation[3] + permutation[5] < permutation[4]: #Constraints applied to permutations
        Used_permutations.append(permutation)  
        ####################################################
        ###   Apply calculations to current permutation ###
        ####################################################

The problem with this code is that I'm wasting time finding all possible permutations just to filter them out again. Can someone assist with a way to apply the constraints while permutations are determined so not all N! are determined?

5
  • 1
    Also position 0 being > 3 means position 0 is 4 since 4 is the only candidate that satisfies that. Commented Jan 25, 2018 at 12:20
  • Assuming that by positions 3, 4 and 5 you mean 2, 3, and 4, there are only four permutations that satisfy your conditions. [4, 2, 0, 3, 1], [4, 2, 1, 3, 0], [4, 3, 0, 2, 1] and [4, 3, 1, 2, 0] Commented Jan 25, 2018 at 12:23
  • Forgot these two [4, 1, 0, 3, 2] and [4, 1, 2, 3, 0]. So 6 in total Commented Jan 25, 2018 at 12:33
  • Sorry, copied wrong list. Edit to correct list. Commented Jan 25, 2018 at 12:34
  • @Pierre it's too late now mate.. Commented Jan 25, 2018 at 12:39

1 Answer 1

8

Instead of first creating a list of all the permutations and then adding some of those elements to a second list and discarding the rest (about 90% in this case), you can use a list comprehension to filter the permutations yielded by itertools as they are created.

>>> numbers = [0,1,2,3,4,5,6]
>>> [p for p in itertools.permutations(numbers) if p[0] > 3 and p[3] + p[5] < p[4]]
[(4, 0, 1, 2, 6, 3, 5),
 (4, 0, 1, 3, 6, 2, 5),
 ... a few more ...
 (6, 5, 4, 1, 3, 0, 2),
 (6, 5, 4, 2, 3, 0, 1)]
>>> len(_)
516

If the checks become more complicated, you do not even have to use a list comprehension. You can do the same in a regular for loop with if conditions, the only difference being that you do not collect all the permutations in a list first, but iterate the generator directly:

all_permutations = itertools.permutations(numbers)  # no list(...)
for permutation in all_permutations:
    ...

Both approaches will still generate all the N! permutations, but most are immediately discarded and only the "right" ones are ever stored in the list.


If you do not even want to generate them, you will have to implement a custom recursive permutations algorithm and manually check whether e.g. for a given value for p[3] there can be any valid values for p[5] and so on. Something like this:

def manual_permutations(numbers, n=0, last=0, lastlast=0):
    if len(numbers) <= 1:
        yield tuple(numbers)
    else:
        for i, first in enumerate(numbers):
            # first constraint
            if n == 0 and first <= 3: continue
            # second constraint: 3 + 5 < 4 === 3 - 4 < -5 === 3 < 4 - 5
            # assuming numbers are ordered: rest[0] is min, rest[-1] is max
            rest = numbers[:i] + numbers[i+1:]
            if n == 3 and first >= rest[-1] - rest[0]: continue
            if n == 4 and last - first >= - rest[0]: continue
            if n == 5 and lastlast - last >= - first: continue
            # constraints okay, recurse
            for perm in manual_permutations(rest, n+1, first, last):
                yield (first,) + perm

This checks the two constraints while generating the permutations, so that if e.g. all the permutations starting with a number <= 3 are not generated at all. The second check is a bit more complicated, and could probably be improved further (if we add a counter to the beginning of the function, we see that there are ~1200 recursive calls). Anyway, using IPython's %timeit we see that the "manual" approach with checks on the fly is still about three times slower than using itertools, so even improving the checks will probably not make it faster than that.*) Also, your own original loop is, in fact, not that much slower, either.

>>> %timeit original_loop(numbers)
1000 loops, best of 3: 736 µs per loop

>>> %timeit list(itertools_perms(numbers))
1000 loops, best of 3: 672 µs per loop

>>> %timeit list(manual_permutations(numbers))
100 loops, best of 3: 2.11 ms per loop

Of course, depending on the size of the input list and the constraints, the manual approach can present more or less of a saving, but can also be (much) more or less difficult to implement or to adapt to changing constraints. Personally, I'd still go with using itertools.permutations and a few simple, readable filters.


*) Update: In my previous edit, the manual approach came out faster; this was because I forgot to actually consume the generators returned by the two functions.

Sign up to request clarification or add additional context in comments.

2 Comments

Can you elaborate on how i would implement a custom recursive permutation algorithm. I've been trying to add the the constraints directly into the recursion function but to no avail. I'm using a similar recursion function as Silveira Neto's solution in link.
@Pierre Please see my new edit. I made a mistake in my previous timing analysis and using itertools is actually faster.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.