6

I want to exhaustively analyse subroutines for sorting small arrays and need a way to generate all uniquely ordered arrays of a particular length. In Python, that would be lists with non-negative integers as elements and preferably using smallest integers when possible. For example, N = 3:

[[0,0,0],
[0,0,1],
[0,1,0],
[0,1,1],
[0,1,2],
[0,2,1],
[1,0,0],
[1,0,1],
[1,0,2],
[1,1,0],
[1,2,0],
[2,0,1],
[2,1,0]]

[1,1,1] and [2,2,0] don't belong in the list above, because [0,0,0] and [1,1,0] respectively have the same relative order while using smaller integers.

Zyx
  • 336
  • 4
  • 18
  • 1
    I'd have hinted to `itertools.product` - but I don't understand the last sentence, why some of the normally used elements do not belong to your desired list... – SpghttCd Oct 22 '18 at 11:50
  • @SpghttCd From the perspective or sorting, where I can only compare whether an item is less than, greater than or equal to another item, the list [2,2,0] is equivalent to [1,1,0], so I don't need to test the same ordering twice. – Zyx Oct 22 '18 at 11:53
  • By the logic of your last statement, would `[0,0,1]` and `[1,1,0]` both belong? – Him Oct 22 '18 at 11:54
  • @Scott, yes, because they have a different order – Zyx Oct 22 '18 at 11:55
  • maybe you can start with `itertools.combinations_with_replacement(range(3),3)` and filter, but no doubt there would be better ways – Chris_Rands Oct 22 '18 at 11:57
  • Why [0,2,1] is in the list when [0,1,0] have the same relative order? – Dani Mesejo Oct 22 '18 at 12:00
  • @DanielMesejo In [0,2,1] the first item is less than second and third; the second item is greater than first and third; the third is greater than first but less than the second. In [0,1,0] the first item is less than second and equal to the third; the second is greater than first and third; the third is equal to the first and less than second. – Zyx Oct 22 '18 at 12:03

3 Answers3

2

This is a combination of (a) finding lists [k_1, ..., k_n] such that each k_i equals either k_(i-1) or k_(i-1)+1, and (b) finding the unique permutations of those lists.

The first can be done using a recursive function:

def combinations(n, k=0):
    if n > 1:
        yield from ([k] + res for i in (0, 1)
                              for res in combinations(n-1, k+i))
    else:
        yield [k]

For lists with n elements, there will be 2^(n-1) such combinations:

>>> list(combinations(2))
[[0, 0], [0, 1]]
>>> list(combinations(3))
[[0, 0, 0], [0, 0, 1], [0, 1, 1], [0, 1, 2]]
>>> list(combinations(4))
[[0, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 1], [0, 0, 1, 2], [0, 1, 1, 1], [0, 1, 1, 2], [0, 1, 2, 2], [0, 1, 2, 3]]

Combine that with itertools.permutations and filter out duplicates to get the final result:

import itertools
def all_combinations(n):
    return (x for combs in combinations(n)
              for x in set(itertools.permutations(combs)))

Example:

>>> list(all_combinations(3))
[(0, 0, 0), (0, 0, 1), (0, 1, 0), (1, 0, 0), (0, 1, 1), (1, 0, 1), (1, 1, 0), (0, 1, 2), (0, 2, 1), (1, 0, 2), (1, 2, 0), (2, 0, 1), (2, 1, 0)]
>>> sum(1 for _ in all_combinations(4))
75
>>> sum(1 for _ in all_combinations(5))
541

Note: Generating all n! permutations and then filtering the duplicates can be very wasteful even for slightly larger values of n. There are smarter ways of generating only unique permutations that can be used instead of itertools.permutations, see e.g. here or here.

tobias_k
  • 81,265
  • 12
  • 120
  • 179
1

You could iterate over the cartesian product of the range, for each element use the relative order as a key, and store the pair (relative order, tuple) in a dictionary, finally return sorted:

def uniquely_ordered_list(n=3):
    def key(o):
        relative = ['equal'] + ['less' if a < b else ('greater' if a > b else 'equal') for a, b in product(o, repeat=2)]
        return tuple(relative)

    found = {}
    for ordering in product(range(n), repeat=n):
        if key(ordering) not in found:
            found[key(ordering)] = ordering

    return sorted(found.values())

Output

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

UPDATE

As suggested by @tobias_k you could use the following function as key:

def key(o):
    sign = lambda x: x / abs(x) if x else x
    return tuple([0] + [sign(a - b) for a, b in product(o, repeat=2)])
Dani Mesejo
  • 61,499
  • 6
  • 49
  • 76
  • This is yielding different results than my approach, e.g. for n=4, mine gets 75 and yours 51. In particular, you do not seem to consider the combination `(0,1,2,3)` at all, and seem to consider `(0,2,2,1)` equivalent to `(0,1,1,0)`, even though they are different after sorting (and OP's problem is about test caes for sorting). (Not saying that this is necessarily wrong, just wanted to point out the difference.) – tobias_k Oct 22 '18 at 12:34
  • 1
    @tobias_k Fixed! Was a minor typo instead of range(3) it was range(n), with range(n) it generated 75 as yours. – Dani Mesejo Oct 22 '18 at 12:35
  • 1
    TBH, still trying to wrap my head around how exactly this works, but it seems to work. Of course, complexity is O(scary), but so is mine. Maybe you could simplify (?) `key` to `return tuple(sign(a - b) for a, b in product(o, repeat=2))` with `sign = lambda x: x / abs(x) if x else x`. – tobias_k Oct 22 '18 at 12:48
1

Here's another solution:

import numpy as np
from itertools import product, combinations

def rord(lst):
    ''' Maps the relative order of a list 'lst' to a unique string of 0, 1, and 2.

    Relative order is computed by converting the list 'sgns' of all 
    the values sgn(lst[i]-lst[j])+1, for i<j, i,j = 0,..., n-1,
    to a string.

    E.g. the lists [0, 0, 1], [0, 0, 2] and [1, 1, 2] have the same rord = '100'
    because lst[0] = lst[1], lst[0] < lst[1], lst[1] < lst[2] for all
    of them, so sgns = [1, 0, 0]
    '''
    sgns = np.sign([tup[0]-tup[1] for tup in combinations(lst, 2)]) + 1
    return ''.join(str(e) for e in sgns)  # return sgns.tostring() is faster


def uniq_rord_lst(n):
    '''Returns n-length sequences of integers 0,... n-1, with unique relative
    order. E.g. for n=2 returns [(0, 0), (0, 1), (1, 0)].
    '''
    seen_ro = set()
    result = []
    for comb in product(range(n), repeat=n):
        ro = rord(comb)
        if ro not in seen_ro:
            seen_ro.add(ro)
            result.append(comb)
    return result

Example:

>>> uniq_rord_lst(2)
[(0, 0), (0, 1), (1, 0)]

>>> uniq_rord_lst(3)
[(0, 0, 0),
 (0, 0, 1),
 (0, 1, 0),
 (0, 1, 1),
 (0, 1, 2),
 (0, 2, 1),
 (1, 0, 0),
 (1, 0, 1),
 (1, 0, 2),
 (1, 1, 0),
 (1, 2, 0),
 (2, 0, 1),
 (2, 1, 0)]

Update: a faster one

def uniq_rord_lst(n):
    seen_ro = set()
    result = []
    for comb in product(range(n), repeat=n):
        ro = tuple(sorted(comb).index(x) for x in comb)
        if ro not in seen_ro:
            seen_ro.add(ro)
            result.append(comb)           
    return result
Andreas K.
  • 9,282
  • 3
  • 40
  • 45