Constructing all combinations from a list containing sublists of different length

Question:

I want to solve the following problem: Given a list containing sublists of different length, say L = [ [ 'a', 'b' ], [ 'c' ], [ 'd', 'e', 'f' ] ], construct all combinations involving exactly one element from each sublist only. So [ 'a', 'c', 'd' ] would be a correct combination, but not [ 'a', 'b', 'e' ]. There are as many combinations as the product of the lengths of the sublists evaluates to, here its six ones, in shorthand (order is not important): cda, cdb, cea, ceb, cfa, cfb.

The idea was to take one arbitrary sublist, but not a singleton, take the complement and create as many new sublists from the complement and one member of the chosen sublist as determined by the sublists length. Proceed now with taking another sublist of length greater than one and do this recursively until all sublists only contain singletons. This recursion has to end after all sought for combinations have been constructed. Here’s the code:

def break_bipartite_chain( var_list ):
    print('a')
    bbc = []
    def recursive_break( var_list ):
        print('b')
        bipartite_chain = sorted( var_list , key=len , reverse=True )
        LC = len( bipartite_chain )
        LCF = len( bipartite_chain[ 0 ] ) 
        complement_chain = []
        for i in range( 0, LCF ):
            complement_chain.append( bipartite_chain[ 1 : ] )
        if LCF == 1:
            print('c')
            bbc.append( bipartite_chain )
            return None
        else:
            print('d')
            for i in range( 0 , LCF ):
                complement_chain[i].append([ bipartite_chain[ 0 ][ i ] ])
        LCC = len( complement_chain )
        for i in range( 0 , LCC ):
            print('e')
            recursive_break( complement_chain[ i ] )
        return None
    bbc.append( recursive_break( var_list ) )
    return bbc[:-1]

Now, the code works and does what it should be (at least on the example I tried), but still I am not totally satisfied with it, and since I am a relative python newbie, I would suggest any comments, including the style, and otherwise focusing on the following questions:

  1. It only works because of the innermost ‘return’ in the if part. I don’t understand exactly what happens, when I remove that, but it gives an
    error code ‘IndexError: list index out of range’? I guess it is not really a good solution to leave the if part with this style, but I found some other people on this forum, who suggested a ‘return’ command to prematurely break from such a statement. Are there better solutions?

  2. The middlemost ‘return’ seems unecessary, but irrespective if I write it or not (it works in both cases), it writes a ‘None’ as a result, which I removed by slicing in the outermost return. Is there a way to suppress the None at all?

  3. Although it works, I am still not sure about the recursion operation. I traced the example given above step by step (that’s why the print commands are there) and it was logical. I could predict the order of solutions and they agreed. But recursion looks like magic often. Is there a cleaner solution?

  4. The first for loop is probably not necessary, I tried both (i) not to use this multiplication of the complement (and just use complement_chain.append later in the second for loop), or do it (ii) with bipartite_chain[1:]*LCF, both either not working at all (first case), or producing an odd behavior, in that the entry I want to append also appeared LCF times (second case).

Finally, I think there is probably a better solution? Or at least tiny things to improve?

Thanks already for any suggestions.

Asked By: Iridium

||

Answers:

You can use itertools.product for this

>>> list(itertools.product(*L))
[('a', 'c', 'd'), ('a', 'c', 'e'), ('a', 'c', 'f'), ('b', 'c', 'd'), ('b', 'c', 'e'), ('b', 'c', 'f')]

because what you are essentially trying to construct is the Cartesian product

Answered By: Cory Kramer

You can create a much simpler recursive function with a generator. Also, it appears that all possible orderings of the sublists of [['a', 'b'], ['c'], ['d', 'e', 'f']] is being considered in your solution, thus, an additional recursive method can be implemented to find all possible positions of l:

l = [['a', 'b'], ['c'], ['d', 'e', 'f']]
from collections import Counter
def placement(d, _c = []):
  if len(_c) == len(d):
    yield _c
  else:
    for i in d:
      _c1, _c2 = Counter(map(tuple, d)), Counter(map(tuple, _c))
      if _c2[tuple(i)] < _c1[tuple(i)]:
         yield from placement(d, _c+[i])

def combos(d, _c = []):
  if len(_c) == len(l):
    yield _c
  else:
    for i in d[0]:
      yield from combos(d[1:], _c+[i])

final_results = [''.join(i) for b in placement(l) for i in combos(b)]

Output:

['acd', 'ace', 'acf', 'bcd', 'bce', 'bcf', 'adc', 'aec', 'afc', 'bdc', 'bec', 'bfc', 'cad', 'cae', 'caf', 'cbd', 'cbe', 'cbf', 'cda', 'cdb', 'cea', 'ceb', 'cfa', 'cfb', 'dac', 'dbc', 'eac', 'ebc', 'fac', 'fbc', 'dca', 'dcb', 'eca', 'ecb', 'fca', 'fcb']
Answered By: Ajax1234
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.