How to generate permutations of a list without “reverse duplicates” in Python using generators

后端 未结 10 944
情歌与酒
情歌与酒 2020-12-03 12:09

This is related to question How to generate all permutations of a list in Python

How to generate all permutations that match following criteria: if

相关标签:
10条回答
  • 2020-12-03 12:28

    If you generate permutations in lexicographical order, then you don't need to store anything to work out whether the reverse of a given permutation has already been seen. You just have to lexicographically compare it to its reverse - if it's smaller then return it, if it's larger then skip it.

    There's probably a more efficient way to do it, but this is simple and has the properties you require (implementable as a generator, uses O(n) working memory).

    0 讨论(0)
  • 2020-12-03 12:28

    itertools.permutations does exactly what you want. you might make of use of reversed built-in as well

    0 讨论(0)
  • 2020-12-03 12:29

    this is an implementation of onebyone's suggestion

    from http://en.wikipedia.org/wiki/Permutation#Lexicographical_order_generation The following algorithm generates the next permutation lexicographically after a given permutation. It changes the given permutation in-place.

    1. Find the highest index i such that s[i] < s[i+1]. If no such index exists, the permutation is the last permutation.
    2. Find the highest index j > i such that s[j] > s[i]. Such a j must exist, since i+1 is such an index.
    3. Swap s[i] with s[j].
    4. Reverse all the order of all of the elements after index i

    the function:

    def perms(items):
        items.sort()
        yield items[:]
        m = [len(items)-2]  # step 1
        while m:
            i = m[-1]
            j = [ j for j in range(i+1,len(items)) if items[j]>items[i] ][-1] # step 2
            items[i], items[j] = items[j], items[i] # step 3
            items[i+1:] = list(reversed(items[i+1:])) # step 4
            if items<list(reversed(items)):
                yield items[:]
            m = [ i for i in range(len(items)-1) if items[i]<items[i+1] ]  # step 1
    

    checking our work:

    >>> foo=list(perms([1,3,2,4,5]))
    >>> True in [(list(reversed(i)) in foo) for i in foo]
    False
    
    0 讨论(0)
  • 2020-12-03 12:33

    I have a marvelous followup to SilentGhost's proposal - posting a separate answer since the margins of a comment would be too narrow to contain code :-)

    itertools.permutations is built in (since 2.6) and fast. We just need a filtering condition that for every (perm, perm[::-1]) would accept exactly one of them. Since the OP says items are always distinct, we can just compare any 2 elements:

    for p in itertools.permutations(range(3)):
        if p[0] <= p[-1]:
            print(p)
    

    which prints:

    (0, 1, 2)
    (0, 2, 1)
    (1, 0, 2)
    

    This works because reversing the permutation would always flip the relation between first and last element!

    For 4 or more elements, other element pairs that are symmetric around the middle (e.g. second from each side p[1] <= p[::-1][1]) would work too.
    (This answer previously claimed p[0] < p[1] would work but it doesn't — after p is reversed this picks different elements.)

    You can also do direct lexicographic comparison on whole permutation vs it's reverse:

    for p in itertools.permutations(range(3)):
        if p <= p[::-1]:
            print(p)
    

    I'm not sure if there is any more effecient way to filter. itertools.permutations guarantees lexicographic order, but the lexicographic position p and p[::-1] are related in a quite complex way. In particular, just stopping at the middle doesn't work.

    But I suspect (didn't check) that the built-in iterator with 2:1 filtering would outperform any custom implementation. And of course it wins on simplicity!

    0 讨论(0)
  • 2020-12-03 12:36

    This is a more concise and faster version of ChristopheD's accepted answer, which I liked a lot. Recursion is great. I made it enforce uniquenss of the incoming list by removing duplicates, however maybe it should just raise an exception instead.

    def fac(x): 
        return (1 if x==0 else x * fac(x-1))
    
    def permz(plist):
        plist = sorted(set(plist))
        plen = len(plist)
        limit = fac(plen) / 2
        counter = 0
        if plen==1:
            yield plist
        else:
            for perm in permz(plist[1:]):
                for i in xrange(plen):
                    if counter == limit:
                         raise StopIteration
                    counter += 1
                    yield perm[:i] + plist[0:1] + perm[i:]
    
    # ---- testing ----
    plists = [
        list('love'),
        range(5),
        [1,4,2,3,9],
        ['a',2,2.1],
        range(8)]               
    
    for plist in plists:
        perms = list(permz(plist))
        print plist, True in [(list(reversed(i)) in foo) for i in perms]
    
    0 讨论(0)
  • 2020-12-03 12:39

    Here is my implementation:

    a = [1,2,3,4]
    
    def p(l):
      if len(l) <= 1:
        yield l
      else:
        for i in range(len(l)):
          for q in p([l[j] for j in range(len(l)) if j != i]):
            yield [l[i]] + q
    
    out = (i for i in p(a) if i < i[::-1])
    

    P function is a regular permu function, yields all possibilities. The filter is done when iterates the result. Simply, it has two possible results, the smaller half of the all permus and the bigger half of the permus. In this example, the out contains the smaller half of the list.

    0 讨论(0)
提交回复
热议问题