14

I am using python 3 and I am trying to find a way to get all the permutations of a list while enforcing some constraints.

For instance, I have a list L=[1, 2, 3, 4, 5, 6, 7]

I want to find all permutations. However, My constraints are:

  • 1 should always come before 2.
  • 3 should come before 4 which in turn should come before 5.
  • Finally, 6 should come before 7.

Of course, I can generate all permutations and ignore those which do not follow these constraints but this wouldn't be efficient I guess.

mathematical.coffee
  • 55,977
  • 11
  • 154
  • 194
Keeto
  • 4,074
  • 9
  • 35
  • 58

3 Answers3

17

This approach filters permutations using a simple filter.

import itertools

groups = [(1,2),(3,4,5),(6,7)]
groupdxs = [i for i, group in enumerate(groups) for j in range(len(group))]
old_combo = []
for dx_combo in itertools.permutations(groupdxs):
    if dx_combo <= old_combo: # as simple filter
        continue
    old_combo = dx_combo
    iters = [iter(group) for group in groups]
    print [next(iters[i]) for i in dx_combo]

What we are doing here is finding permutations of a multiset. (In this case the multiset is groupdxs.) Here's a paper that details an O(1) algorithm for this.

Steven Rumbalski
  • 44,786
  • 9
  • 89
  • 119
  • 1
    (You don't need the second argument to `permutations`. Also this is really neat!) – Katriel Mar 12 '12 at 01:01
  • 3
    isn't this (in a very elegant way) still generating all perms and then filtering them? (itertools.permutations is generating as many different orderings as it would if given [1,2,3,4,5,6,7]) – andrew cooke Mar 12 '12 at 01:05
  • @andrew cooke: i think so. `list(itertools.permutations((0,0))` -> `[(0, 0), (0, 0)]` i.e., `itertools.permutations()` generates `N!` permutations in any case. – jfs Mar 12 '12 at 01:19
  • @andrewcook: Yes, it's just filtering permutations, which isn't all that efficient. But it is an easier filter than testing the more complicated constraints directly. – Steven Rumbalski Mar 12 '12 at 01:39
  • @all: Added a link to my answer showing how to do this O(1). – Steven Rumbalski Mar 12 '12 at 02:28
  • 2
    There is extra `)`. if you use C++ `std::next_permutation()` then you don't need to filter e.g., [in Cython](https://gist.github.com/2019680) – jfs Mar 12 '12 at 04:01
2
def partial_permutations(*groups):
    groups = list(filter(None, groups)) # remove empties.
    # Since we iterate over 'groups' twice, we need to
    # make an explicit copy for 3.x for this approach to work.
    if not groups:
        yield []
        return
    for group in groups:
        for pp in partial_permutations(*(
             g[1:] if g == group else g
             for g in groups
        )):
            yield [group[0]] + pp
Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
  • can you give an example of how it should be run (i don't understand the code and `list(partial_permutations((1,2),(3,4,5),(6,7)))` gives `[]`). – andrew cooke Mar 12 '12 at 10:46
  • Run it exactly as you're attempting. It works fine for me in 2.7.2. The approach is simple: the permutations are generated recursively by trying the first number of each group, followed by the permutations of all the other numbers (keeping them grouped the same way). – Karl Knechtel Mar 12 '12 at 11:36
1

One way is to take one of the swapping algorithms, and when you are about to swap an element to its final position, check if it's in the right order. The code below does just this.

But first let me show its usage:

L = [1, 2, 3, 4, 5, 6, 7]
constraints = [[1, 2], [3, 4, 5], [6, 7]]

A = list(p[:] for p in constrained_permutations(L, constraints)) # copy the permutation if you want to keep it
print(len(A))
print(["".join(map(str, p)) for p in A[:50]])

and the output:

210
['1234567', '1234657', '1234675', '1236457', '1236475', '1236745', '1263457', '1263475', '1263745', '1267345', '1324567', '1324657', '1324675', '1326457', '1326475', '1326745', '1342567', '1342657', '1342675', '1345267', '1345627', '1345672', '1346527', '1346572', '1346257', '1346275', '1346725', '1346752', '1364527', '1364572', '1364257', '1364275', '1364725', '1364752', '1362457', '1362475', '1362745', '1367245', '1367425', '1367452', '1634527', '1634572', '1634257', '1634275', '1634725', '1634752', '1632457', '1632475', '1632745', '1637245']

But now the code:

def _permute(L, nexts, numbers, begin, end):
    if end == begin + 1:
        yield L
    else:
        for i in range(begin, end):
            c = L[i]
            if nexts[c][0] == numbers[c]:
                nexts[c][0] += 1
                L[begin], L[i] = L[i], L[begin]
                for p in _permute(L, nexts, numbers, begin + 1, end):
                    yield p
                L[begin], L[i] = L[i], L[begin]
                nexts[c][0] -= 1


def constrained_permutations(L, constraints):
    # warning: assumes that L has unique, hashable elements
    # constraints is a list of constraints, where each constraint is a list of elements which should appear in the permatation in that order
    # warning: constraints may not overlap!
    nexts = dict((a, [0]) for a in L)
    numbers = dict.fromkeys(L, 0) # number of each element in its constraint
    for constraint in constraints:
        for i, pos in enumerate(constraint):
            nexts[pos] = nexts[constraint[0]]
            numbers[pos] = i

    for p in _permute(L, nexts, numbers, 0, len(L)):
        yield p
Reinstate Monica
  • 4,568
  • 1
  • 24
  • 35