简体   繁体   中英

Python: How to get all possible bin combinations from a set of data with a weight limit?

I have a list of numbers which all correspond to items of different weight:

weights = [50, 40, 30, 100, 150, 12, 150, 10, 5, 4]

I need to split the values into two bins with the caveat that the bin sum total cannot exceed 300.

eg The simplest one I can think of is:

bin1 = [150, 150] = 300
bin2 = [50, 40, 30, 100, 12, 10, 5, 4] = 251

I want to be able to get all the combinations of these weights that would satisfy this caveat, unsure how to go about this?

one way is brute-forcing it by binning all possible permutations of the list

there has to be a better (more clever) way of doing that - it's terribly slow. (but I'm supposed to be doing other things right now ;-))

from itertools import permutations

max_bin_size = 300
weights = [50, 40, 30, 100, 150, 12, 150, 10, 5, 4]

bin_combinations = set()

def to_bins(weights):
    bin = []
    for weight in weights:
        if weight > max_bin_size:
            raise ValueError("wtf?")
        if sum(bin) + weight > max_bin_size:
            yield tuple(sorted(bin))
            bin = [weight]
        else:
            bin.append(weight)
    yield tuple(sorted(bin))


for permutation in set(permutations(weights)):
    bin_combinations.add(tuple(sorted(to_bins(permutation))))

for combination in bin_combinations:
    print(combination)

# ((4, 10, 40, 100), (5, 12, 150), (30, 50, 150))
# ((4, 5, 10, 40, 50, 100), (12, 30), (150, 150))
# ((4, 10, 30, 150), (5, 50, 100), (12, 40, 150))
# ((4, 5, 30, 100), (10, 50, 150), (12, 40, 150))
# ((4, 12, 50, 100), (5, 10, 30, 40), (150, 150))
# ((4, 5, 150), (10, 30, 40, 150), (12, 50, 100))
# ...

Update:

a version with reuse of bins (assuming one can put the weights freely into any available bin):

from itertools import permutations

max_bin_size = 300
weights = [50, 40, 30, 100, 150, 12, 150, 10, 5, 4]

bin_combinations = set()


def to_bins(weights):
    bins = [[]]
    for weight in weights:
        if weight > max_bin_size:
            raise ValueError("wtf?")
        for bin in bins:
            if sum(bin) + weight > max_bin_size:
                continue
            bin.append(weight)
            break
        else:
            bins.append([weight])            
    return tuple(sorted(tuple(sorted(bin)) for bin in bins))


for permutation in set(permutations(weights)):
    bin_combinations.add(to_bins(permutation))

print(len(bin_combinations), "combinations")
for combination in bin_combinations:
    print(combination)

# 14 combinations
# ((4, 5, 10, 12, 30, 50, 150), (40, 100, 150))
# ((4, 5, 10, 30, 40, 50, 150), (12, 100, 150))
# ((4, 12, 30, 100, 150), (5, 10, 40, 50, 150))
# ((4, 100, 150), (5, 10, 12, 30, 40, 50, 150))
# ((4, 5, 12, 30, 50, 150), (10, 40, 100, 150))
# ((4, 5, 10, 12, 100, 150), (30, 40, 50, 150))
# ((4, 5, 12, 30, 40, 50, 150), (10, 100, 150))
# ((4, 5, 40, 100, 150), (10, 12, 30, 50, 150))
# ((4, 10, 12, 30, 40, 50, 150), (5, 100, 150))
# ((4, 5, 10, 30, 100, 150), (12, 40, 50, 150))
# ((4, 5, 10, 12, 30, 40, 150), (50, 100, 150))
# ((4, 10, 40, 50, 150), (5, 12, 30, 100, 150))
# ((4, 5, 10, 12, 40, 50, 150), (30, 100, 150))
# ((4, 5, 10, 12, 30, 40, 50, 100), (150, 150))

I have been looking for another approach to this problem. The solution I propose is to use masks to test the different possible combinations. A friend of mine also suggested a more complex solution but about 10 times faster, using pruning.

With both solutions, we find 33 solutions for the given example.

Simple solution

The solution I am proposing is the following (for an explanation of the code see further):

import numpy as np

def get_bins(weights, max_bin_size):
    my_bins = set()
    weights = np.array(sorted(weights))
    for i in range(1, 2 ** (len(weights) - 1)):
        mask = np.zeros(len(weights), dtype=bool)
        binary = np.fromiter((int(digit) for digit in bin(i)[2:]), dtype=bool)
        mask[len(weights) - len(binary):] = binary
        if sum(weights[mask]) <= max_bin_size and sum(weights[~mask]) <= max_bin_size:
            my_bins.add((tuple(weights[~mask]), tuple(weights[mask])))
    return my_bins

Results:

>>> max_bin_size = 300
>>> weights = [50, 40, 30, 100, 150, 12, 150, 10, 5, 4]
>>> bins = get_bins(weights, max_bin_size)
>>> print(len(bins))
33
>>> for b in bins:
>>>     print(b, sum(b[0]), sum(b[1]))
((4, 5, 10, 12, 30, 50, 150), (40, 100, 150)) 261 290
((4, 5, 10, 30, 40, 50, 150), (12, 100, 150)) 289 262
((4, 5, 12, 30, 50, 150), (10, 40, 100, 150)) 251 300
...

Optimized solution

from typing import List, Tuple, Optional
from itertools import chain, repeat, starmap


# Order weights in decreasing order, then compute a list of tuple (weight, count)
def counter(list_object: List):
    result = []
    list_object.sort(reverse=True)
    current = None
    count = 0
    for elt in list_object:
        if current == elt:
            count = count + 1
        elif current is not None:
            result.append((current, count))
            count = 1
            current = elt
        else:
            current = elt
            count = 1
    if current is not None:
        result.append((current, count))
    return result


class BinPacking:
    def __init__(self, max_bin_size: int, weights: List[int]):
        self.count_weights: List[Tuple[int, int]] = counter(weights)
        self.max_bin_size: int = max_bin_size
        self.bin1: List[Optional[Tuple[int, int]]] = [None] * len(self.count_weights)
        self.bin2: List[Optional[Tuple[int, int]]] = [None] * len(self.count_weights)
        self.solutions: List[Tuple[List[int], List[int]]] = []
        self.explore(0, 0, 0, 0, 0, True)

    # Add a new solution, transform lists of (weight, count) into flat list of weights
    def add_solution(self, bin1_index: int, bin2_index: int):
        list1 = list(chain.from_iterable(starmap(repeat, self.bin1[:bin1_index])))
        list2 = list(chain.from_iterable(starmap(repeat, self.bin2[:bin2_index])))
        self.solutions.append((list1, list2))

    def explore(self, weight_index: int, weight1: int, weight2: int, index1: int, index2: int, same: bool):
        if weight_index >= len(self.count_weights):
            self.add_solution(index1, index2)
            return

        weight, count = self.count_weights[weight_index]
        if same:
            minimum = (count - 1) // 2
        else:
            minimum = -1
        for i in range(count, minimum, -1):
            if (weight1 + i * weight) > max_bin_size or (weight2 + (count - i) * weight) > max_bin_size:
                continue
            if i > 0:
                self.bin1[index1] = (weight, i)
                i1 = index1 + 1
            else:
                i1 = index1
            if i < count:
                self.bin2[index2] = (weight, count - i)
                i2 = index2 + 1
            else:
                i2 = index2
            same2 = same and (i == (count - i))
            self.explore(weight_index + 1, weight1 + i * weight, weight2 + (count - i) * weight, i1, i2, same2)

Results:

>>> max_bin_size = 300
>>> weights = [50, 40, 30, 100, 150, 12, 150, 10, 5, 4]
>>> binpacking = BinPacking(max_bin_size, weights)
>>> print(len(binpacking.solutions))
33
>>> print("\n".join(str(b) for b in binpacking.solutions))
([150, 150], [100, 50, 40, 30, 12, 10, 5, 4])
([150, 100, 50], [150, 40, 30, 12, 10, 5, 4])
([150, 100, 40, 10], [150, 50, 30, 12, 5, 4])
...

Simple solution explanation

Before starting, it is interesting to note that we can systematically put the first value of the weights in the same bin, which reduces the problem to 2^(n-1) combinations.

To generate the mask my proposal is to iterate i from 1 to 2^(n-1) and convert this integer into a binary number represented as an array ( Integer to bitfield as a list ).

This gives the following code:

weights = np.array(weights)
for i in range(1, 2 ** (len(weights) - 1)):
    mask = np.zeros(len(weights), dtype=bool)
    binary = np.fromiter((int(digit) for digit in bin(i)[2:]), dtype=bool)
    mask[len(weights) - len(binary):] = binary

Now we add a condition to keep only the pairs of bins whose total weight is less than the desired maximum:

def get_bins(weights, max_bin_size):
    weights = np.array(weights)
    for i in range(1, 2 ** (len(weights) - 1)):
        mask = np.zeros(len(weights), dtype=bool)
        binary = np.fromiter((int(digit) for digit in bin(i)[2:]), dtype=bool)
        mask[len(weights) - len(binary):] = binary
        if sum(weights[mask]) <= max_bin_size and sum(weights[~mask]) <= max_bin_size:
            yield weights[~mask], weights[mask]

With this code we get 65 solutions, the cause: some redundant solutions appear if a weight value is present in several copies in the weights . In the example given, we have twice the value 150 , the permutations of these two values give us two solutions for each combination where the two weights 150 are in different bins. There is only 1 combination where they are in the same bin, and 32 where they are in different bins, which gives us 32*2 + 1 = 65 combinations.

To remove redundant combinations, a simple solution is to sort the list of weights and to use sets:

def get_bins(weights, max_bin_size):
    my_bins = set() # the set
    weights = np.array(sorted(weights)) # here we are sorting the weights
    for i in range(1, 2 ** (len(weights) - 1)):
        mask = np.zeros(len(weights), dtype=bool)
        binary = np.fromiter((int(digit) for digit in bin(i)[2:]), dtype=bool)
        mask[len(weights) - len(binary):] = binary
        if sum(weights[mask]) <= max_bin_size and sum(weights[~mask]) <= max_bin_size:
            my_bins.add((tuple(weights[~mask]), tuple(weights[mask])))
    return my_bins

Optimized solution explanation

The explanation of the code will come soon ... In the meantime, here are some details about optimizations:

The counter function is equivalent but faster than:

sorted(Counter(weights).items(), reverse=True)

In the same idea, the code used in add_solution is equivalent but faster than:

list1 = [elt for (x,y) in bin1[:bin1_index] for elt in [x]*y]

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM