Two lists – find all permutations – even multiple to one – Round 2

Question:

I asked this question previously, but things changed so new question is in order.


I have two lists and I need to find all permutations.

The kicker is that multiple "number" items (as per example below) can be assigned. No assignments are also allowed.

So this:

names = ['a', 'b']
numbers = [1, 2]

Would become this:

[
    "not only this"
    {'a': [1], 'b': [2]},
    {'a': [2], 'b': [1]},
    "but also"
    {'a': [], 'b': []},
    {'a': [1], 'b': []},
    {'a': [2], 'b': []},
    {'a': [1, 2], 'b': []},
    {'a': [2, 1], 'b': []},
    {'a': [], 'b': [1]},
    {'a': [], 'b': [2]},
    {'a': [], 'b': [1, 2]},
    {'a': [], 'b': [2, 1]},
]

The difference to my previous question is that in this case the list order DOES in face matter, so [1, 2] != [2, 1] – both cases need to be encompassed.



Also, the lists above are only for illustration purposes, since in the real scenario I will be using classes:

class sample_class():
    def __init__(self, parameter_1, parameter_2):
        self.parameter_1 = parameter_1
        self.parameter_2 = parameter_2
        
first_group_example_list = [sample_class(x, x+1) for x in range(2)]
second_group_example_list = [sample_class(x+5, x+5+1) for x in range(2)]

Now, since the number of permutations gets very high very quickly, speed is king. Not sure if using classes compared to list of ints has any effect on speed, but I suspect so, so if there are any good practices when it comes to that, please let me know.

Not sure if it has any effect on speed / memory, but since the lists are ordered, I was thinking about using element indexes when generating the permutations, instead of the classes themselves, and pull the actual class instances only in the subsequent processing steps.


EDIT: Let’s not complicate things with the classes and stick with names and numbers (as long the solution doesn’t care what type the two input lists are (either ints or strings)). I will figure out the rest, hopefully, as I go.


EDIT2: HERE is the solution that will calculate the number of "permutations".

import math

numbers = 6 # length of numbers list
names = 7 # length of names list

result = 0
for j in range(numbers + 1):
    result += math.factorial(j + names - 1) / (math.factorial(j) * math.factorial(numbers - j))

result *= math.factorial(numbers) / math.factorial(names - 1)

print(result) # number of "permutations"
Asked By: Seminko

||

Answers:

Here’s a fast solution. Note that it doesn’t do any searching – it simply rolls the solutions out:

import itertools

def perm_lists(d, names, ix):
    if ix == len(names):
        yield d.copy()
        return

    name = names[ix]
    for m in itertools.permutations(d[name]):
        d[name] = m
        yield from perm_lists(d, names, ix+1)

def f(p, names, numbers):
    d = {name : [] for name in names}
    for i, j in enumerate(p):
        if j >= 0:
            d[names[j]].append(numbers[i])

    yield from perm_lists(d, names, 0)

def val_gen(names, numbers):
    p_iter = itertools.product(range(-1, len(names)), repeat=len(numbers))

    for p in p_iter:
        yield from f(p, names, numbers)

This implements a generator, so you don’t have to pay the cost of the entire top-level list if all you want to do is iterate over the values, but you can of course convert it to a list if desired.

Also note that this uses tuples in place of the value lists. These are more memory-efficient than lists, and fall out naturally from the implementation, but it’s trivial to convert them to lists if needed.

Here’s an example:

names = ['a', 'b']
numbers = [1, 2]

for d in val_gen(names, numbers):
    print(d)

This produces:

{'a': (), 'b': ()}
{'a': (2,), 'b': ()}
{'a': (), 'b': (2,)}
{'a': (1,), 'b': ()}
{'a': (1, 2), 'b': ()}
{'a': (2, 1), 'b': ()}
{'a': (1,), 'b': (2,)}
{'a': (), 'b': (1,)}
{'a': (2,), 'b': (1,)}
{'a': (), 'b': (1, 2)}
{'a': (), 'b': (2, 1)}

If you want the value lists to be lists rather than tuples, just change:

d[name] = m

to:

d[name] = list(m)

in perm_lists. This will produce:

{'a': [], 'b': []}
{'a': [2], 'b': []}
{'a': [], 'b': [2]}
{'a': [1], 'b': []}
{'a': [1, 2], 'b': []}
{'a': [2, 1], 'b': []}
{'a': [1], 'b': [2]}
{'a': [], 'b': [1]}
{'a': [2], 'b': [1]}
{'a': [], 'b': [1, 2]}
{'a': [], 'b': [2, 1]}

If you want to create the full top-level list, rather than using the generator to iterate over it, you can use:

full_list = list(val_gen(names, numbers))

I suggest doing some timing experiments. I believe this will be fast when used on large amounts of data.

Answered By: Tom Karzes