pythonconditional-statementspartitioningpartitionminimum-size

Find all unordered partitions of a list with a minimum number of elements


A list of elements is given. I want to have all the possibilities to divide this list into any number of partitions so that each partition has at least x elements. The order of the partitions in the list and the order of the elements in the partition do not matter. E.g.: List = [1,2,3,4] get_partitions(list,2) should return:

[[1,2,3,4],
 [[1,2],[3,4]],
 [[1,3],[2,4]],
 [[1,4],[2,3]]]

List = [1,2,3,4] get_partitions(list,1) should return:

[[1,2,3,4],
 [[1,2],[3,4]],
 [[1,3],[2,4]],
 [[1,4],[2,3]],
 [[1],[2],[3,4],
  ...]

I have started to implement this recursively in Python, but I create too many redundant cases. For runtime reasons, I would like to reduce these cases in advance and not delete them afterwards with frozensets, for example.

from itertools import combinations
import numpy as np
def get_partitions(liste,min,max=None):

    if max is None:
        # Default setting
        max = len(liste)
    if len(liste) == min :
        # Termination Criterium
        yield [liste]
    else:
        for r in range(np.min([len(liste),max]),min-1,-1):
            # max for avoiding cases like: [[1,2,3,4],[2,6]] and [[2,6],[1,2,3,4]]
            for perm in combinations(liste,r):
                rest = [i for i in liste if i not in perm]
                if len(rest) >= min:
                    for recurse in get_partitions(rest,min,r):
                        yield [list(perm)] + list(recurse)
                if len(rest) == 0:
                    # r == len(liste)
                    yield [list(perm)]

This leads to:

[[[1, 2, 3, 4]],
 [[1, 2], [3, 4]],
 [[1, 3], [2, 4]], 
 [[1, 4], [2, 3]],
 [[2, 3], [1, 4]],
 [[2, 4], [1, 3]],
 [[3, 4], [1, 2]]]

Thanks in advance for any help.


Trying to use @mozway 's answer and extending it to a recursive version lead me to:

def get_partitions(iterable, minl=2):
    s = set(iterable)

    for r in range(minl, len(s)//2+1):
        if len(s)//2 != r:
            for c in combinations(s, r):
                for recurse in get_partitions(list(s.difference(c)), minl):
                    yield [list(c),*recurse]
        else:
            for c in islice(combinations(s, r), comb(len(s),r)//2):
                for recurse in get_partitions(list(s.difference(c)), minl):
                    yield [list(c),*recurse]

    yield [list(s)]

For the example list = [1,2,3,4], x=1 it is reducing the number of possibilities from 47 (my initial try) to 19. Still, there are plenty of redundant cases.

[[[1], [2], [3], [4]], <----
 [[1], [2], [3, 4]],
 [[1], [2, 3, 4]],
 [[2], [1], [3], [4]], <----
 [[2], [1], [3, 4]],
 [[2], [1, 3, 4]],
 [[3], [1], [2], [4]], <----
 [[3], [1], [2, 4]],
 [[3], [1, 2, 4]],
 [[4], [1], [2], [3]], <----
 [[4], [1], [2, 3]],
 [[4], [1, 2, 3]],
 [[1, 2], [3], [4]],
 [[1, 2], [3, 4]],
 [[1, 3], [2], [4]],
 [[1, 3], [2, 4]],
 [[1, 4], [2], [3]],
 [[1, 4], [2, 3]],
 [[1, 2, 3, 4]]]

Solution

  • Here is one long-ish solution. No rejection is used in generating partitions, so in that sense this may be somewhat efficient. Still, there are lots of things to optimize.

    Example:

    list(get_partitions(range(3), 1))
    # [[[0, 1, 2]], [[0], [1, 2]], [[1], [0, 2]], [[2], [0, 1]], [[0], [1], [2]]]
    

    Here is an outline of how this works:

    This is a fun problem and comments on bugs and optimizations are very welcome.


    from itertools import combinations
    from collections import Counter
    
    # from this thread:
    # https://stackoverflow.com/questions/10035752/elegant-python-code-for-integer-partitioning
    def partitions(n, I=1):
        yield (n,)
        for i in range(I, n//2 + 1):
            for p in partitions(n-i, i):
                yield (i,) + p
    
    
    def split(lst, n):
      '''
      return all ways to split lst into two groups, 
      with n and len(lst) - n elements respectively
      '''
      assert len(lst) >= n
      # handle special case of len(lst) == 2 * n
      if len(lst) == 2 * n:
        for first, second in split(lst[1:], n-1):
          yield [lst[0], *first], second
      else:
        for comb in combinations(range(len(lst)), n):
          comb = set(comb)
          first = [x for i, x in enumerate(lst) if i in comb]
          second = [x for i, x in enumerate(lst) if i not in comb]
          yield first, second
    
    
    def get_partitions_same_size(lst, n, k):
      # print(lst, n, k)
      'return all ways to partition lst into n parts each of size k up to order'
      if len(lst) != n * k:
        print(lst, n, k)
      assert len(lst) == n * k
      if n == 0 and len(lst) == 0:
        yield []
      # case when group size is one
      elif k == 1:
        yield [[x] for x in lst]
      # otherwise, without loss, the first element of lst goes into the first group
      else:
        for first, rest in split(lst[1:], k-1):
          for rec_call in get_partitions_same_size(rest, n-1, k):
            yield [[lst[0], *first], *rec_call]
    
    
    def get_partitions_helper(lst, p_scheme):
      """
      return all ways to partition lst into groups according to a partition scheme p_scheme
    
      p_scheme describes an integer partition of len(lst)
      for example, if len(lst) == 5, then possible integer partitions are:
      [(5,), (1, 4), (1, 1, 3), (1, 1, 1, 2), (1, 1, 1, 1, 1), (1, 2, 2), (2, 3)]
      for each, we count the number of groups of a given size
      the corresponding partition schemes are:
      [[(5, 1)],
      [(1, 1), (4, 1)],
      [(1, 2), (3, 1)],
      [(1, 3), (2, 1)],
      [(1, 5)],
      [(1, 1), (2, 2)],
      [(2, 1), (3, 1)]]
      """
      if not lst and not p_scheme:
        yield []
        return 
      assert len(lst) == sum(a * b for a, b in p_scheme)
      group_size, group_count = p_scheme[0]
      num_elts = group_size * group_count
      for first, second in split(lst, num_elts):
        for firsts in get_partitions_same_size(first, group_count, group_size):
          for seconds in get_partitions_helper(second, p_scheme[1:]):
            yield [*firsts, *seconds]
    
    
    def get_partitions(lst, min_):
      """
      get all partitions of lst into groups s.t. each group has at least min_ elements
      up to order (of groups and elements within a group)
      """
      for partition in partitions(len(lst), min_):
        p_scheme = list(Counter(partition).items())
        yield from get_partitions_helper(lst, p_scheme)