General bars and stars

Question:

I’ve got a the following “bars and stars” algorithm, implemented in Python, which prints out all decomposition of a sum into 3 bins, for sums going from 0 to 5.
I’d like to generalise my code so it works with N bins (where N less than the max sum i.e 5 here).
The pattern is if you have 3 bins you need 2 nested loops, if you have N bins you need N-1 nested loops.

Can someone think of a generic way of writing this, possibly not using loops?

# bars and stars algorithm
N=5
for n in range(0,N):
    x=[1]*n
    for i in range(0,(len(x)+1)):
        for j in range(i,(len(x)+1)):
            print sum(x[0:i]), sum(x[i:j]), sum(x[j:len(x)])
Asked By: pontikos

||

Answers:

This can be solved recursively in the following approach:

#n bins, k stars,
def F(n,k):
  #n bins, k stars, list holds how many elements in current assignment
  def aux(n,k,list):
        if n == 0: #stop clause
            print list
        elif n==1: #making sure all stars are distributed
            list[0] = k
            aux(0,0,list)
        else: #"regular" recursion:
            for i in range(k+1):
                #the last bin has i stars, set them and recurse
                list[n-1] = i
                aux(n-1,k-i,list)
  aux(n,k,[0]*n)

The idea is to “guess” how many stars are in the last bin, assign them, and recurse to a smaller problem with less stars (as much that were assigned) and one less bin.


Note: It is easy to replace the line

print list

with any output format you desire when the number of stars in each bin is set.

Answered By: amit

Take it one step at a time.

First, remove the sum() calls. We don’t need them:

N=5
for n in range(0,N):
    x=[1]*n
    for i in range(0,(n+1)):  # len(x) == n
        for j in range(i,(n+1)):
            print i, j - i, n - j

Notice that x is an unused variable:

N=5
for n in range(0,N):
    for i in range(0,(n+1)):
        for j in range(i,(n+1)):
            print i, j - i, n - j

Time to generalize. The above algorithm is correct for N stars and three bars, so we just need to generalize the bars.

Do this recursively. For the base case, we have either zero bars or zero stars, which are both trivial. For the recursive case, run through all the possible positions of the leftmost bar and recurse in each case:

from __future__ import print_function

def bars_and_stars(bars=3, stars=5, _prefix=''):
    if stars == 0:
        print(_prefix + ', '.join('0'*(bars+1)))
        return
    if bars == 0:
        print(_prefix + str(stars))
        return
    for i in range(stars+1):
        bars_and_stars(bars-1, stars-i, '{}{}, '.format(_prefix, i))

For bonus points, we could change range() to xrange(), but that will just give you trouble when you port to Python 3.

Answered By: Kevin

Another recursive variant, using a generator function, i.e. instead of right away printing the results, it yields them one after another, to be printed by the caller.

The way to convert your loops into a recursive algorithm is as follows:

  • identify the “base case”: when there are no more bars, just print the stars
  • for any number of stars in the first segment, recursively determine the possible partitions of the rest, and combine them

You can also turn this into an algorithm to partition arbitrary sequences into chunks:

def partition(seq, n, min_size=0):
    if n == 0:
        yield [seq]
    else:
        for i in range(min_size, len(seq) - min_size * n + 1):
            for res in partition(seq[i:], n-1, min_size):
                yield [seq[:i]] + res

Example usage:

for res in partition("*****", 2):
    print "|".join(res)
Answered By: tobias_k

If this isn’t simply a learning exercise, then it’s not necessary for you to roll your own algorithm to generate the partitions: Python’s standard library already has most of what you need, in the form of the itertools.combinations function.

From Theorem 2 on the Wikipedia page you linked to, there are n+k-1 choose k-1 ways of partitioning n items into k bins, and the proof of that theorem gives an explicit correspondence between the combinations and the partitions. So all we need is (1) a way to generate those combinations, and (2) code to translate each combination to the corresponding partition. The itertools.combinations function already provides the first ingredient. For the second, each combination gives the positions of the dividers; the differences between successive divider positions (minus one) give the partition sizes. Here’s the code:

import itertools

def partitions(n, k):
    for c in itertools.combinations(range(n+k-1), k-1):
        yield [b-a-1 for a, b in zip((-1,)+c, c+(n+k-1,))]

# Example usage
for p in partitions(5, 3):
    print(p)

And here’s the output from running the above code.

[0, 0, 5]
[0, 1, 4]
[0, 2, 3]
[0, 3, 2]
[0, 4, 1]
[0, 5, 0]
[1, 0, 4]
[1, 1, 3]
[1, 2, 2]
[1, 3, 1]
[1, 4, 0]
[2, 0, 3]
[2, 1, 2]
[2, 2, 1]
[2, 3, 0]
[3, 0, 2]
[3, 1, 1]
[3, 2, 0]
[4, 0, 1]
[4, 1, 0]
[5, 0, 0]
Answered By: Mark Dickinson

I needed to solve the same problem and found this post, but I really wanted a non-recursive general-purpose algorithm that didn’t rely on itertools and couldn’t find one, so came up with this.

By default, the generator produces the sequence in either lexical order (as the earlier recursive example) but can also produce the reverse-order sequence by setting the “reversed” flag.

def StarsAndBars(bins, stars, reversed=False):
    if bins < 1 or stars < 1:
        raise ValueError("Number of bins and objects must both be greater than or equal to 1.")

    if bins == 1:
        yield stars,
        return

    bars = [ ([0] * bins + [ stars ], 1) ]

    if reversed:           
        while len(bars)>0:
            b = bars.pop()
            if b[1] == bins:
                yield tuple(b[0][y] - b[0][y-1] for y in range(1, bins+1))
            else:
                bar = b[0][:b[1]]
                for x in range(b[0][b[1]], stars+1):
                    newBar = bar + [ x ] * (bins - b[1]) + [ stars ]
                    bars.append( (newBar, b[1]+1) )
        bars = [ ([0] * bins + [ stars ], 1) ]
    else:
        while len(bars)>0:
            newBars = []
            for b in bars:
                for x in range(b[0][-2], stars+1):
                    newBar = b[0][1:bins] +  [ x, stars ]      
                    if b[1] < bins-1 and x > 0:
                        newBars.append( (newBar, b[1]+1) )
                    yield tuple(newBar[y] - newBar[y-1] for y in range(1, bins+1))
            bars = newBars   
Answered By: TheBertster

This problem can also be solved somewhat less verbosely than the previous answers with a list comprehension:

from numpy import array as ar
from itertools import product

number_of_stars = M
number_of_bins = N

decompositions = ar([ar(i) for i in product(range(M+1), repeat=N) if sum(i)==M])

Here the itertools.product() produces a list containing the Cartesian product of the list range(M+1) with itself, where the product has been applied (repeats=)N times. The if statement removes the combinations where the number don’t add up to the number of stars, for example one of the combinations is of 0 with 0 with 0 or [0,0,0].
If we’re happy with a list of lists then we can simply remove the np.array()’s (just ar for brevity in the example). Here’s an example output for 3 stars in 3 bins:

array([[0, 0, 3],
       [0, 1, 2],
       [0, 2, 1],
       [0, 3, 0],
       [1, 0, 2],
       [1, 1, 1],
       [1, 2, 0],
       [2, 0, 1],
       [2, 1, 0],
       [3, 0, 0]])

I hope this answer helps!

Answered By: Eden Trainor

Since I found the code in most answers quite hard to follow i.e. asking myself how the shown algorithms relate to the actual problem of stars and bars let’s do this step by step:

First we define a function to insert a bar | into a string stars at a given position p:

def insert_bar(stars, p):
    head, tail = stars[:p], stars[p:]
    return head + '|' + tail

Usage:

insert_bar('***', 1) # returns '*|**'

To insert multiple bars at different positions e.g. (1,3) a simple way is to use reduce (from functools)

reduce(insert_bar, (1,3), '***') # returns '*|*|*'

If we branch the definition of insert_bar to handle both cases we get a nice and reusable function to insert any number of bars into a string of stars

def insert_bars(stars, p):
    if type(p) is int:
        head, tail = stars[:p], stars[p:]
        return head + '|' + tail
    else:
        return reduce(insert_bar, p, stars)

As @Mark Dickinson explaind in his answer itertools.combinations lets us produce the n+k-1 choose k-1 combinations of bar positions.

What is now left to do is to create a string of '*' of length n, insert the bars at the given positions, split the string at the bars and calculate the length of each resulting bin. The implementation below is thus literally a verbatim translation of the problem statement into code

def partitions(n, k):
    for positions in itertools.combinations(range(n+k-1), k-1):
       yield [len(bin) for bin in insert_bars(n*"*", positions).split('|')]
Answered By: Sascha

anyone looking for the specific case of k=2 can save ALOT of time by simply creating a range and stacking it with the reverse. Comparing versus accepted answer.

n = 500000

%timeit np.array([[i,j] for i,j in partitions(n,2)])

>>> 396 ms ± 13.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%%timeit
rng = np.arange(n+1)
np.vstack([rng, rng[::-1]]).T

>>> 2.91 ms ± 190 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

And they are indeed equivalent.

it2k = np.array([[i,j] for i,j in partitions(n,2)])
rng = np.arange(n+1)
np2k = np.vstack([rng, rng[::-1]]).T

(np2k == it2k).all()

>>> True
Answered By: Ryan Skene

Here is a nonrecursive algorithm that replicates the "bars and stars" nested loop approach. This assumes the bars all start on the right, and finish on the left (bins going from [x,0,0,…] to [0,0,..,x]). There will always be a zero in the first bin when a loop finishes, so you can follow the logic and match it to "bars and stars."

def combos(nbins, qty):
    bins = [0]*nbins
    bins[0] = qty #starting bin quantities
    while True:
        yield bins
        if bins[-1] == qty:
            return #last combo, we're done!
        #leftmost bar movement (inner loop)
        if bins[0] > 0:
            bins[0] -= 1
            bins[1] += 1
        else:
            #bump next bar in nested loops
            #i.e., find first nonzero entry, and split it
            nz = 1 
            while bins[nz] == 0: 
                nz +=1
            bins[0]=bins[nz]-1
            bins[nz+1] += 1
            bins[nz] = 0

Here is the result of 4 bins, quantity 3:

for m in combos(4, 3):
    print(m)

[3, 0, 0, 0]
[2, 1, 0, 0]
[1, 2, 0, 0]
[0, 3, 0, 0]
[2, 0, 1, 0]
[1, 1, 1, 0]
[0, 2, 1, 0]
[1, 0, 2, 0]
[0, 1, 2, 0]
[0, 0, 3, 0]
[2, 0, 0, 1]
[1, 1, 0, 1]
[0, 2, 0, 1]
[1, 0, 1, 1]
[0, 1, 1, 1]
[0, 0, 2, 1]
[1, 0, 0, 2]
[0, 1, 0, 2]
[0, 0, 1, 2]
[0, 0, 0, 3]
Answered By: delver
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.