Packing a small number of packages into a fixed number of bins - python

I have a list of package sizes. There will be a maximum of around 5 different sizes and they may occur a few times (<50).
packages = [5,5,5,5,5,5,10,11]
I need to pack them into a fixed number of bins, for example 3.
number_of_bins = 3
The bins may vary in size (sum of the sizes of the packed packages) between 0 and, say, 2 (that is, the difference of the sum of the sizes of the packages in the bins must be equal or nearly equal). So having bins with [1,2] (=3) and [2] (=2) (difference is 1) is fine, having them with [10] (=10) and [5] (=5) (difference is 5) is not.
It is possible not to sort all packages into the bins, but I want the solution where a minimum number of packages remains unpacked.
So the best solution in this case (I think) would be
bins = [11,5],[10,5],[5,5,5]
remaining = [5]
There's probably a knapsack or bin-packing algorithm to do this, but I haven't found it. I'm fine with brute-forcing it, but I'm not sure what's an efficient way to do that.
Is there any efficient way of doing this easily? Did I just miss the relevant search term to find it?
Another example:
packages = [5,10,12]
number_of_bins = 2
leads to
bins = [12],[10]
remaining = [5]
because
bins = [12],[10,5]
has bin sizes of 12 and 15 which vary by more than 2.
Analog:
packages = [2,10,12]
number_of_bins = 3
leads to
bins = [2],[],[]
remaining = [12,10]

Here is a solution using pulp:
from pulp import *
packages = [18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 65, 65, 65]
number_of_bins = 3
bins = range(1, number_of_bins + 1)
items = range(0, len(packages))
x = LpVariable.dicts('x',[(i,b) for i in items for b in bins],0,1,LpBinary)
y = LpVariable('y', 0, 2, LpInteger)
prob=LpProblem("bin_packing",LpMinimize)
#maximize items placed in bins
prob.setObjective(LpAffineExpression([(x[i,b], -3) for i in items for b in bins] + [(y, 1)]))
#every item is placed in at most 1 bin
for i in items:
prob+= lpSum([x[i,b] for b in bins]) <= 1
for b in bins:
if b != 1: # bin 1 is the one with lowest sum
prob+= LpAffineExpression([(x[i,b], packages[i]) for i in items] + [(x[i,1], -packages[i]) for i in items]) >= 0
if b != number_of_bins: # last bin is the one with highest
prob+= LpAffineExpression([(x[i,number_of_bins], packages[i]) for i in items] + [(x[i,b], -packages[i]) for i in items]) >= 0
#highest sum - lowest sum <= 2 so every difference of bin sums must be under 2
prob += LpAffineExpression([(x[i,number_of_bins], packages[i]) for i in items] + [(x[i,1], -packages[i]) for i in items]) <= 2
prob += LpAffineExpression([(x[i,number_of_bins], packages[i]) for i in items] + [(x[i,1], -packages[i]) for i in items]) == y
prob.solve()
print(LpStatus[prob.status])
for b in bins:
print(b,':',', '.join([str(packages[i]) for i in items if value(x[i,b]) !=0 ]))
print('left out: ', ', '.join([str(packages[i]) for i in items if sum(value(x[i,b]) for b in bins) ==0 ]))

Tricky one, really not sure about an optimal solution. Below is a solution that just iterates all possible groups and halts at the first solution. This should be a minimal-remainder solution since we first iterate through all solutions without any remainder.
It also iterates over solutions as everything in the first bin, which could be excluded for a faster result.
import numpy as np
def int_to_base_list(x, base, length):
""" create a list of length length that expresses a base-10 integer
e.g. binary: int2list(101, 2, 10) returns array([0, 0, 0, 1, 1, 0, 0, 1, 0, 1])
"""
placeholder = np.array([0] * length) # will contain the actual answer
for i in reversed(range(length)):
# standard base mathematics, see http://www.oxfordmathcenter.com/drupal7/node/18
placeholder[i] = x % base
x //= base
return placeholder
def get_groups(packages, max_diff_sum, number_of_bins):
""" Get number_of_bins packaging groups that differ no more than max_diff_sum
e.g.
[5, 5, 5, 5, 5, 5, 10, 11] with 2, 3 gives [5,5,5], [10,5], [11,5]
[5, 10, 12] with 2, 2 gives [10], [12]
[2, 6, 12] with 2, 3 gives [2], [], []
We approach the problem by iterating over group indices, so the first
example above has solution [0 0 0 1 2 3 1 2] with the highest number being
the 'remainder' group.
"""
length = len(packages)
for i in range((number_of_bins + 1)**length - 1): # All possible arrangements in groups
index = int_to_base_list(i, number_of_bins + 1, length) # Get the corresponding indices
sums_of_bins = [np.sum(packages[index==ii]) for ii in range(number_of_bins)]
if max(sums_of_bins) - min(sums_of_bins) <= max_diff_sum: # the actual requirement
# print(index)
break
groups = [packages[index==ii] for ii in range(number_of_bins)]
# remainder = packages[index==number_of_bins+1]
return groups
On your examples:
packages = np.array([5, 5, 5, 5, 5, 5, 10, 11])
max_diff_sum = 2
number_of_bins = 3
get_groups(packages, max_diff_sum, number_of_bins)
>> [array([5, 5, 5]), array([ 5, 10]), array([ 5, 11])]
And
packages = np.array([5,10,12])
max_diff_sum = 2
number_of_bins = 2
get_groups(packages, max_diff_sum, number_of_bins)
>> [array([10]), array([12])]

Related

Trying to understand how my python code works

i = [1, 2, 3, 5, 5, 7, 9, 12, 14, 14,]
list_length = len(i)
def numbers_found(x):
y = i.count(x)
return y
latest_num = i[list_length - 1]
for z in range(latest_num + 1):
print("Found", numbers_found(z), "of number", "\"" + str(z) + "\".")
I am trying to find how many of a certain number is available in the list, if I somehow minus by 1 to the maximum number in the list (assuming it is in ascending order) and add 1 again it works. Please help explain this to me.
Lets break it down, step by step.
# a list. indexes shown below
# 0 1 2 3 4 5 6 7 8 9
i = [1, 2, 3, 5, 5, 7, 9, 12, 14, 14]
# getting the length of the list (10)
# or the number of elements
list_length = len(i)
# a function returning the amounts of
# times a passed value is found in the list i
def numbers_found(x):
y = i.count(x)
return y
# see above that list_length is 10
# but we need one less that to retrieve the last element
# which will be 14
latest_num = i[list_length - 1]
# range given 1 argument iterates from 0
# to the number you pass it but not including it
# since latest_num is 14, it won't include it
# So range(15) would iterate like
# 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14
for z in range(latest_num + 1):
print("Found", numbers_found(z), "of number", "\"" + str(z) + "\".")
The reason range works like this, is it's common to see range(length_of_my_list) and expect it to return indexes for the full list.
In order for that to happen you need to only iterate to and not include the length. In your case (10).
What you are using it for is something else. You are trying to find the occurrences of all the numbers in the list. Since you are not using it for indexes, adding + 1 works since you WANT it to include 14.
Use
for z in range(len(i)):
print("Found", numbers_found(z), "of number", "\"" + str(z) + "\".")
Here len(i) is the size of i i.e. the number of elements in i, and range(n) = [0, 1, ..., n - 1] i.e all numbers from 0 to n-1, sorted asc.

How can I extract a centered window from a numpy array?

Suppose I have the following numpy array:
>>> a = np.arange(0,21,1)
>>> a
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20])
Now suppose that I want to pick a window of length N, where 2 < N <= a.shape[0], such that the window is "centered" around one of the elements of the array a. For example, if I want to center a window of length N = 5 around the element 10 in array a, then this window would be:
>>> idx = 10 # index of the array element 10
>>> N = 5 # window size
>>> a[idx - N//2:idx + N//2 + 1]
array([ 8, 9, 10, 11, 12])
This method generalizes well for windows that are not near the edges of the array, but I can't make it work otherwise. For example, if I want to extract a window of length N = 7 around the element 2 in a, then what I get is:
>>> idx = 2
>>> N = 7
>>> a[idx - N//2:idx + N//2 + 1]
array([], dtype=int32)
However what I want is:
>>> a[0:7]
array([0, 1, 2, 3, 4, 5, 6])
How can I generalize this method for windows near the edges of a?
Try with:
idx = 2
start = min(idx - N//2, 0)
a[start:start + N]
Note that this is not centered at idx=2.
Based on Quang Hoang's answer, here is what worked:
import numpy as np
a = np.arange(0,21,1)
idx = 5 # desired element index
N = 7 # window length
if N % 2: # if window length is odd
step = N // 2
else: # if window length is even
step = int(N/2 - 1)
# make sure starting index is between 0 and a.shape[0] - N
start = min(max(idx-step,0),a.shape[0] - N)
window = a[start:start + N]

Generating a list of N pseudo-random integers, drawn from a specific range, with the list summing to a specific value

In Python:
How can I generate a list of N (e.g. 10) pseudo-random integers, each drawn from a specific range (e.g. between 3 and 9), with the list summing up to a specific value (e.g. 58)?
For this example the solution would look like this:
solution = [3, 7, 7, 9, 3, 6, 4, 8, 8, 3]
sum(solution)
58
That is, 10 numbers, each between 3 and 9, summing up to 58.
I've tried a solution approaches with np.random.dirichlet or np.random.multinomial as suggested in related questions here, but these do not allow for choosing the integers from a specific range.
import random
N = 10
s = 58
i, j = 3, 9
out = [i] * N
while sum(out) != s:
idx = random.randint(0, N-1)
if out[idx] < j:
out[idx] += 1
print(out)
Prints (for example):
[9, 7, 6, 4, 5, 8, 3, 5, 5, 6]
One idea I've just had is to initialise a list with n values that are the average value (so, as close to being all the same value as possible) and then randomly select a pair of values and increase one while decreasing the other while ensuring values are kept with the required range, so don't increase/decrease if the value will go out of bounds.
Repeat until success?
a = []
while sum(a) != 58:
a = random.choices(range(3, 10), k=10)
Takes about 17 attempts on average.

How to count the number of items in several bins using loop in python? details showed in picture

Question details showed in the picture Thanks for your help.
Write a function histogram(values, dividers) that takes as argument a sequence of values and a sequence of bin dividers, and returns the histogram as a sequence of a suitable type (say, an array) with the counts in each bin. The number of bins is the number of dividers + 1; the first bin has no lower limit and the last bin has no upper limit. As in (a), elements that are equal to one of the dividers are counted in the bin below.
For example, suppose the sequence of values is the numbers 1,..,10 and the bin dividers are array(2, 5, 7); the histogram should be array(2, 3, 2, 3).
Here is my code
def histogram(values, dividers):
count=0
for element in values:
index=0
i=0
count[i]=0
while index < len(dividers) - 2:
if element <= dividers[index]:
i=dividers[index]
count[i] += 1
index=len(dividers)
elif element > dividers[index] and element <= dividers[index+1]:
i=dividers[index]
count[i] += 1
index= len(dividers)
index += 1
return count[i]
from bisect import bisect_left
# Using Python builtin to find where value is in dividers
(this is O(log n) for each value)
def histogram(values, dividers):
count = [0]*(1+len(dividers))
for element in values:
i = bisect_left(dividers, element)
count[i] += 1
return count
values = list(range(1, 11)) # list from 1 through 10
bins = [2, 5, 7]
c = histogram(values, bins) # Result [2, 3, 2, 3]
Explanation of histogram
1. bisect_left finds the bin the index the value should be inserted
2. We update count array according to this index. Count array size is
(1+len(bins)), to allow for values > bins[-1]
A simple implementation would be to prepare a list of counters of size len(dividers)+1.
Go through all numbers provided:
if your current number is bigger then the largest bin-divider, increment the last bins counter
else go through all dividers until your number is no longer bigger as it, and increment that bin-counter by 1
This leads to:
def histogram(values, dividers):
bins = [0 for _ in range(len(dividers)+1)]
print (bins)
for num in values:
if num > dividers[-1]:
bins[-1] += 1
else:
k = 0
while num > dividers[k]:
k+=1
bins[k] += 1
return bins
print(histogram(range(20),[2,4,9]))
Output:
# counts
[3, 2, 5, 10]
Explanation
Dividers: [2,4,9]
Bins: [ 2 and less | 4 | 9 | 10 and more ]
Numbers: 0..19
0, 1, 2 -> not bigger then 9, smaller/equal 2
3, 4 -> not bigger then 9, smaller/equal 4
5, 6, 7, 8, 9 -> not bigger then 9, smaller/equal 9
10, 11, 12, 13, 14, 15, 16, 17, 18, 19 -> bigger 9
This is a naive implementation and there are faster ones using tree like data structures for more performance. Consider a divider of [5,6,7] and a list of [7,7,7,7,7,7] this would run 6 times (6*7) testing for bins 3 times (bigger then 5, bigger then 6, not bigger then 7) == 18 unrolled loops.
There are more efficient algos possible using better suited data structures.

Distribute elements based on percentages

Let's say that I want to distribute a number of items (n) into an array of fixed size (x).
The difficult part is that I have to distribute the items using a flexibility array.
Assuming that x = 4, n = 11 and flexibility = [20, 20, 30, 30] with len(flexibility) == x.
My question is:
How can I distribute the n elements in an array of length equal to x using the percentage defined in f?
What I want at the end is something like:
n = 11
x = 4
flexibility = [20, 20, 30, 30]
distributed = distribute_elements_in_slots(n, x, flexibility)
print(distributed)
# distributed = [2, 2, 3, 4]
In the case of equal flexibility values, the final result will depend on the rule that we decide to apply to use all the item. In the previous case, the final result will be good with [2, 2, 3, 4] and with [2, 2, 4, 3].
Edit: An example of the method that I want to have is as follows:
def distribute_elements_in_slots(n, x, flexibility=[25,25,25,25]):
element_in_slots = []
element_per_percentage = x / 100
for i in range(x):
element_in_slots.append(round(slots_per_point_percentage * flexibility[i])
Edit 2: One of the solutions that I found is the following:
def distribute_elements_in_slots(n, x, flexibility=[25,25,25,25]):
element_in_slots = [f * n / 100 for f in flexibility]
carry = 0
for i in range(len(element_in_slots)):
element = element_in_slots[i] + carry
element_in_slot[i] = floor(element)
carry = element- floor(element)
if np.sum(element_in_slots) < n:
# Here the carry is almost 1
max_index = element_in_slots.index(max(flexibiliyt))
appointments_per_slot[max_index] = appointments_per_slot[max_index] + 1
This will distribute almost evenly the slots based on the flexibility array.
what you need to do is split the number 11 according to certain percents given in the array so initially it becomes percentage * number(11). Then we get remainder and put assign it somewhere which in your case is the last element.
In [10]: [i*n/100 for i in f]
Out[10]: [2.2, 2.2, 3.3, 3.3]
In [11]: b=[i*n/100 for i in f]
In [12]: rem = sum(b) - sum(map(int,b))
In [13]: rem
Out[13]: 1.0
In [24]: b= list(map(int,b))
In [26]: b[-1] +=rem
In [27]: b
Out[27]: [2, 2, 3, 4.0]
Hope it helps. :)
As Albin Paul did, we need to allocate the whole-number amount for each slot's percentage. The leftovers need to be allocated, largest first.
def distribute_elements_in_slots(total, slots, pct):
# Compute proportional distribution by given percentages.
distr = [total * pct[i] / 100 for i in range(slots)]
# Truncate each position and store the difference in a new list.
solid = [int(elem) for elem in distr]
short = [distr[i] - solid[i] for i in range(slots)]
print(distr)
print(solid)
print(short)
# allocate leftovers
leftover = int(round(sum(short)))
print(leftover)
# For each unallocated item,
# find the neediest slot, and put an extra there.
for i in range(leftover):
shortest = short.index(max(short))
solid[shortest] += 1
short[shortest] = 0
print("Added 1 to slot", shortest)
return solid
n = 11
x = 4
flexibility = [20, 20, 30, 30]
distributed = distribute_elements_in_slots(n, x, flexibility)
print(distributed)
# distributed = [2, 2, 3, 4]
Output:
[2.2, 2.2, 3.3, 3.3]
[2, 2, 3, 3]
[0.2, 0.2, 0.3, 0.3]
1
Added 1 to slot 2
[2, 2, 4, 3]

Categories