Reputation: 53
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.
e.g. 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?
Upvotes: 4
Views: 704
Reputation: 182
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.
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
...
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])
...
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
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]
Upvotes: 3
Reputation: 1322
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))
# ...
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))
Upvotes: 2