8

The input is an integer that specifies the amount to be ordered. There are predefined package sizes that have to be used to create that order.

e.g.

Packs
3 for $5
5 for $9
9 for $16

for an input order 13 the output should be:

2x5 + 1x3

So far I've the following approach:

remaining_order = 13
package_numbers = [9,5,3]
required_packages = []

while remaining_order > 0:
    found = False
    for pack_num in package_numbers:
        if pack_num <= remaining_order:
            required_packages.append(pack_num)
            remaining_order -= pack_num
            found = True
            break

    if not found:
        break

But this will lead to the wrong result:

1x9 + 1x3 remaining: 1

martineau
  • 119,623
  • 25
  • 170
  • 301
wasp256
  • 5,943
  • 12
  • 72
  • 119
  • 1
    As an aside, in Python, a `for` can have an `else`. – tripleee Jan 16 '19 at 07:13
  • we could read some books about ``integer linear programming'', or do some projects under LINGO software. – Togtokh Khorchin Jan 16 '19 at 08:20
  • 1
    Write down how __you__ would solve this problem yourself (how would you decide to use 2x5 + 1x3 instead of 1x9 + 1x3), then you'll have the correct algorithm.... – bruno desthuilliers Jan 18 '19 at 09:21
  • 1
    What should be the ouput for remaining_order = 9 ? 3x3 or 1x9? Just to be sure before I start writting a solution – BlueSheepToken Jan 18 '19 at 09:29
  • 1
    Can you specify boundaries? Will it be 3 package sizes max or may there be 20000? Biggest input? – DonQuiKong Jan 18 '19 at 09:30
  • 1
    @BlueSheepToken lowest price I think, so 9. I'd even think that combination of biggest inputs should suffice, prices don't normally favor smaller package_sizes, but op should answer that. – DonQuiKong Jan 18 '19 at 09:31
  • If anyone feels like putting this into an answer, the thing is called coin problem and I think e.g. this site has an answer: http://interactivepython.org/courselib/static/pythonds/Recursion/DynamicProgramming.html – DonQuiKong Jan 18 '19 at 09:34
  • Looks like the OP asks for implementation of `Fully polynomial time approximation scheme` from https://en.wikipedia.org/wiki/Knapsack_problem – Dima Tisnek Jan 18 '19 at 09:35
  • https://github.com/madcat1991/knapsack claims to have a solution, in 4 lines of code, using a library :) – Dima Tisnek Jan 18 '19 at 09:37
  • @DonQuiKong I agree, I made a solution taking only the biggest package possible, but for nine, it should be 3x3... I am writting several solutions, he will choose ! :) – BlueSheepToken Jan 18 '19 at 09:39
  • 1
    @BlueSheepToken oh damn, you're right, the prices aren't quite logical. That .. basically reduces the possible input packages to 3 and 5 though in this case. – DonQuiKong Jan 18 '19 at 09:41
  • 1
    Which also means that trivially any input greater than 22 (2*5 + 4*3) can be solved by going mod 15 because necessarily there is either 3*5 or 5*3 in the solution which both is 15. Rest .. lut. – DonQuiKong Jan 18 '19 at 09:45
  • @DonQuiKong Yeah I totally agree with you, but for the solutions I tried to be as generic as possible, the mathematics tricks should be used if the inputs do not change (which should be the case), I will precise it in the answer – BlueSheepToken Jan 18 '19 at 09:59
  • 1
    @BlueSheepToken if the number of possible packages is small and the number of possible inputs is big some trick like that will be needed. If both are big, some kind of greedy algorithm or heuristic might be needed. It really depends on what op wants to achieve and how fast. – DonQuiKong Jan 18 '19 at 10:01
  • @DonQuiKong I agree, we cannot find a polynomial solution for an NP completness problem... Even for 100 bounty ! – BlueSheepToken Jan 18 '19 at 10:05
  • 1
    Sorry about my late response, the valid solution should not depend on the price but the minimum number of packages needed! – wasp256 Jan 18 '19 at 12:34
  • Also this is just an example above, it can be any size of order and any number of packages available, so it should be a generic solution that finishes in a "decent" amount of time – wasp256 Jan 18 '19 at 12:35
  • It looks like a Linear Diophantine Equation: `3*x + 5*y + 9*z = 13'. According to this [answer](https://stackoverflow.com/a/46019800/8973620) you can solve it with SymPy or Sage. – Mykola Zotko Jan 18 '19 at 13:19
  • @wasp256 Did you check my hand written answer number 1? FIlling with the least packages each time. Else you can use a lib, which might be the best answer – BlueSheepToken Jan 18 '19 at 16:23
  • @wasp256: What do you mean under 'depend on the price but the minimum number of packages needed'? The Knapsack problem depends on both, trying to reach the maximum capacity simultaneously maximizing the price. Could you formulate the problem statement precisely? – Dmytro Prylipko Jan 18 '19 at 20:21

5 Answers5

5

So, you need to fill the order with the packages such that the total price is maximal? This is known as Knapsack problem. In that Wikipedia article you'll find several solutions written in Python.

To be more precise, you need a solution for the unbounded knapsack problem, in contrast to popular 0/1 knapsack problem (where each item can be packed only once). Here is working code from Rosetta:

from itertools import product


NAME, SIZE, VALUE = range(3)
items = (
    # NAME, SIZE, VALUE
    ('A', 3, 5),
    ('B', 5, 9),
    ('C', 9, 16))

capacity = 13


def knapsack_unbounded_enumeration(items, C):

    # find max of any one item
    max1 = [int(C / item[SIZE]) for item in items]
    itemsizes = [item[SIZE] for item in items]
    itemvalues = [item[VALUE] for item in items]

    # def totvalue(itemscount, =itemsizes, itemvalues=itemvalues, C=C):
    def totvalue(itemscount):
        # nonlocal itemsizes, itemvalues, C

        totsize = sum(n * size for n, size in zip(itemscount, itemsizes))
        totval = sum(n * val for n, val in zip(itemscount, itemvalues))

        return (totval, -totsize) if totsize <= C else (-1, 0)

    # Try all combinations of bounty items from 0 up to max1
    bagged = max(product(*[range(n + 1) for n in max1]), key=totvalue)
    numbagged = sum(bagged)
    value, size = totvalue(bagged)
    size = -size
    # convert to (iten, count) pairs) in name order
    bagged = ['%dx%d' % (n, items[i][SIZE]) for i, n in enumerate(bagged) if n]

    return value, size, numbagged, bagged


if __name__ == '__main__':
    value, size, numbagged, bagged = knapsack_unbounded_enumeration(items, capacity)
    print(value)
    print(bagged)

Output is:

23
['1x3', '2x5']

Keep in mind that this is a NP-hard problem, so it will blow as you enter some large values :)

Dmytro Prylipko
  • 4,762
  • 2
  • 25
  • 44
  • 1
    Am... link-only answers will be deleted in the low quality posts review page. – U13-Forward Jan 18 '19 at 09:27
  • 1
    @U9-Forward there's a meta discussion around somewhere, this is, barely, but still, an answer because it can be reproduced even if the link goes dead -> knapsack problem, wikipedia, done. – DonQuiKong Jan 18 '19 at 09:28
2

You can use itertools.product:

import itertools
remaining_order = 13
package_numbers = [9,5,3]
required_packages = []
a=min([x for i in range(1,remaining_order+1//min(package_numbers)) for x in itertools.product(package_numbers,repeat=i)],key=lambda x: abs(sum(x)-remaining_order))
remaining_order-=sum(a)
print(a)
print(remaining_order)

Output:

(5, 5, 3)
0

This simply does the below steps:

  1. Get value closest to 13, in the list with all the product values.

  2. Then simply make it modify the number of remaining_order.

If you want it output with 'x':

import itertools
from collections import Counter
remaining_order = 13
package_numbers = [9,5,3]
required_packages = []
a=min([x for i in range(1,remaining_order+1//min(package_numbers)) for x in itertools.product(package_numbers,repeat=i)],key=lambda x: abs(sum(x)-remaining_order))
remaining_order-=sum(a)
print(' '.join(['{0}x{1}'.format(v,k) for k,v in Counter(a).items()]))
print(remaining_order)

Output:

2x5 + 1x3
0
U13-Forward
  • 69,221
  • 14
  • 89
  • 114
1

For you problem, I tried two implementations depending on what you want, in both of the solutions I supposed you absolutely needed your remaining to be at 0. Otherwise the algorithm will return you -1. If you need them, tell me I can adapt my algorithm.

As the algorithm is implemented via dynamic programming, it handles good inputs, at least more than 130 packages !

In the first solution, I admitted we fill with the biggest package each time. I n the second solution, I try to minimize the price, but the number of packages should always be 0.

remaining_order = 13
package_numbers = sorted([9,5,3], reverse=True) # To make sure the biggest package is the first element
prices = {9: 16, 5: 9, 3: 5}
required_packages = []

# First solution, using the biggest package each time, and making the total order remaining at 0 each time
ans = [[] for _ in range(remaining_order + 1)]
ans[0] = [0, 0, 0]
for i in range(1, remaining_order + 1):
    for index, package_number in enumerate(package_numbers):
        if i-package_number > -1:
            tmp = ans[i-package_number]
            if tmp != -1:
                ans[i] = [tmp[x] if x != index else tmp[x] + 1 for x in range(len(tmp))]
                break
    else: # Using for else instead of a boolean value `found`
        ans[i] = -1 # -1 is the not found combinations

print(ans[13]) # [0, 2, 1]
print(ans[9]) # [1, 0, 0]

# Second solution, minimizing the price with order at 0

def price(x):
    return 16*x[0]+9*x[1]+5*x[2]

ans = [[] for _ in range(remaining_order + 1)]
ans[0] = ([0, 0, 0],0) # combination + price
for i in range(1, remaining_order + 1):
    # The not found packages will be (-1, float('inf'))
    minimal_price = float('inf')
    minimal_combinations = -1
    for index, package_number in enumerate(package_numbers):
        if i-package_number > -1:
            tmp = ans[i-package_number]
            if tmp != (-1, float('inf')):
                tmp_price = price(tmp[0]) + prices[package_number] 
                if tmp_price < minimal_price:
                    minimal_price = tmp_price
                    minimal_combinations = [tmp[0][x] if x != index else tmp[0][x] + 1 for x in range(len(tmp[0]))]
    ans[i] = (minimal_combinations, minimal_price)

print(ans[13]) # ([0, 2, 1], 23)
print(ans[9]) # ([0, 0, 3], 15) Because the price of three packages is lower than the price of a package of 9
BlueSheepToken
  • 5,751
  • 3
  • 17
  • 42
  • Mmmhh... this is right, I will remove the side note for the moment, will put it back later if I find the mathematical proof :p – BlueSheepToken Jan 18 '19 at 10:09
  • It should be 9*(5*3-1) + 5*(3*9-1) + 3*(9*5-1) because for example for 5 and 3 5*3 would be 15, but two times 5 and two times 3 is 16 which is a possible number but can't be solved with mod 15. – DonQuiKong Jan 18 '19 at 10:09
  • However, if any package is in there for self_value*product_of_rest, the mod takes it out. So any combination of a single package bigger than each package times product_of_all_other_package_values-1 can be ignored. – DonQuiKong Jan 18 '19 at 10:11
1

Since no declaration about the object function is found, I assume your goal is to maximize the package value within the pack's capability.

Explanation: time complexity is fixed. Optimal solution may not be filling the highest valued item as many as possible, you have to search all possible combinations. However, you can reuse the possible optimal solutions you have searched to save space. For example, [5,5,3] is derived from adding 3 to a previous [5,5] try so the intermediate result can be "cached". You may either use an array or you may use a set to store possible solutions. The code below runs the same performance as the rosetta code but I think it's clearer.

To further optimize, use a priority set for opts.

costs = [3,5,9]
value = [5,9,16]

volume = 130

# solutions
opts = set()
opts.add(tuple([0]))

# calc total value
cost_val = dict(zip(costs, value))

def total_value(opt):
    return sum([cost_val.get(cost,0) for cost in opt])

def possible_solutions():
    solutions = set()
    for opt in opts:
        for cost in costs:
            if cost + sum(opt) > volume:
                continue
            cnt = (volume - sum(opt)) // cost
            for _ in range(1, cnt + 1):
                sol = tuple(list(opt) + [cost] * _)
                solutions.add(sol)
    return solutions

def optimize_max_return(opts):
    if not opts:
        return tuple([])
    cur = list(opts)[0]
    for sol in opts:
        if total_value(sol) > total_value(cur):
            cur = sol
    return cur

while sum(optimize_max_return(opts)) <= volume - min(costs):
    opts = opts.union(possible_solutions())

print(optimize_max_return(opts))

If your requirement is "just fill the pack" it'll be even simpler using the volume for each item instead.

knh190
  • 2,744
  • 1
  • 16
  • 30
1

In case you need a solution for a small number of possible

package_numbers

but a possibly very big

remaining_order, 

in which case all the other solutions would fail, you can use this to reduce remaining_order:

import numpy as np

remaining_order = 13
package_numbers = [9,5,3]
required_packages = []
sub_max=np.sum([(np.product(package_numbers)/i-1)*i for i in package_numbers])

while remaining_order > sub_max:
    remaining_order -= np.product(package_numbers)
    required_packages.append([max(package_numbers)]*np.product(package_numbers)/max(package_numbers))

Because if any package is in required_packages more often than (np.product(package_numbers)/i-1)*i it's sum is equal to np.product(package_numbers). In case the package max(package_numbers) isn't the one with the samllest price per unit, take the one with the smallest price per unit instead.

Example:

remaining_order = 100
package_numbers = [5,3]

Any part of remaining_order bigger than 5*2 plus 3*4 = 22 can be sorted out by adding 5 three times to the solution and taking remaining_order - 5*3. So remaining order that actually needs to be calculated is 10. Which can then be solved to beeing 2 times 5. The rest is filled with 6 times 15 which is 18 times 5.

In case the number of possible package_numbers is bigger than just a handful, I recommend building a lookup table (with one of the others answers' code) for all numbers below sub_max which will make this immensely fast for any input.

DonQuiKong
  • 413
  • 4
  • 15