6

Lets say that I have 3 sellers of a particular item. Each seller has different amounts of this items stored. The also have a different price for the item.

Name            Price      Units in storage
Supplier #1     17$        1 Unit
Supplier #2     18$        3 Units
Supplier #3     23$        5 Units

If I do not order enough items from the same supplier, I have to pay some extra costs per unit. Let's say, for example, that if I do not order at least 4 units, I do have to pay extra 5$ for each unit ordered.

Some examples:

If I wanted to buy 4 units, the best price would come from getting them from Supplier #1 and Supplier #2, rather than getting it all from Supplier #3

(17+5)*1 + (18+5)*3 = 91                 <--- Cheaper
            23   *4 = 92

But if I were to buy 5 units, getting them all from Supplier 3 gives me a better price, than getting first the cheaper ones and the rest from more expensive suppliers

(17+5)*1 + (18+5)*3 + (23+5)*1 = 119
                       23   *5 = 115$    <--- Cheaper

The question

Keeping all this in mind... If I knew beforehand how many items I want to order, what would be an algorithm to find out what is the best combination I can chose?

Enrique Moreno Tent
  • 24,127
  • 34
  • 104
  • 189
  • In an RDBMS, say, you could iterate through all combinations in a single query, and arrange them by total. – Strawberry Mar 02 '17 at 13:35
  • Could you write me please an answer, explaining how such query look like, please? – Enrique Moreno Tent Mar 02 '17 at 13:37
  • 3
    This is basically a shortest path problem, right? So, while I may post an RDBMS solution if I have time, you might be better off looking at Djikstra's algorithm, and similar np complete problems. – Strawberry Mar 02 '17 at 13:42
  • @Strawberry I would be interested in that solution too. I am trying this as well but I struggle with translating the requirements into a graph so if you do find the time, that would be nice :) (Or if you have an example somewhere that I can look at) – pandaadb Mar 02 '17 at 14:17

2 Answers2

5

As noted in comments, you can use a graph search algorithm for this, like Dijkstra's algorithm. It might also be possible to use A*, but in order to do so, you need a good heuristic function. Using the minimum price might work, but for now, let's stick with Dijkstra's.

One node in the graph is represented as a tuple of (cost, num, counts), where cost is the cost, obviously, num the total number of items purchased, and counts a breakdown of the number of items per seller. With cost being the first element in the tuple, the item with the lowest cost will always be at the front of the heap. We can handle the "extra fee" by adding the fee if the current count for that seller is lower than the minimum, and subtracting it again once we reach that minimum.

Here's a simple implementation in Python.

import heapq

def find_best(goal, num_cheap, pay_extra, price, items):
    # state is tuple (cost, num, state)
    heap = [(0, 0, tuple((seller, 0) for seller in price))]
    visited = set()

    while heap:
        cost, num, counts = heapq.heappop(heap)
        if (cost, num, counts) in visited:
            continue  # already seen this combination
        visited.add((cost, num, counts))

        if num == goal:  # found one!
            yield (cost, num, counts)

        for seller, count in counts:
            if count < items[seller]:
                new_cost = cost + price[seller]  # increase cost
                if count + 1 < num_cheap: new_cost += pay_extra  # pay extra :(
                if count + 1 == num_cheap: new_cost -= (num_cheap - 1) * pay_extra  # discount! :)
                new_counts = tuple((s, c + 1 if s == seller else c) for s, c in counts)
                heapq.heappush(heap, (new_cost, num+1, new_counts))  # push to heap

The above is a generator function, i.e. you can either use next(find_best(...)) to find just the best combination, or iterate over all the combinations:

price = {1: 17, 2: 18, 3: 23}
items = {1: 1, 2: 3, 3: 5}
for best in find_best(5, 4, 5, price, items):
    print(best)

And as we can see, there's an even cheaper solution for buying five items:

(114, 5, ((1, 1), (2, 0), (3, 4)))
(115, 5, ((1, 0), (2, 0), (3, 5)))
(115, 5, ((1, 0), (2, 1), (3, 4)))
(119, 5, ((1, 1), (2, 3), (3, 1)))
(124, 5, ((1, 1), (2, 2), (3, 2)))
(125, 5, ((1, 0), (2, 3), (3, 2)))
(129, 5, ((1, 1), (2, 1), (3, 3)))
(130, 5, ((1, 0), (2, 2), (3, 3)))

Update 1: While the above works fine for the example, there can be cases where it fails, since subtracting the extra cost once we reach the minimum number means that we could have edges with negative cost, which can be a problem in Dijkstra's. Alternatively, we can add all four elements at once in a single "action". For this, replace the inner part of the algorithm with this:

            if count < items[seller]:
                def buy(n, extra):  # inner function to avoid code duplication
                    new_cost = cost + (price[seller] + extra) * n
                    new_counts = tuple((s, c + n if s == seller else c) for s, c in counts)
                    heapq.heappush(heap, (new_cost, num + n, new_counts))

                if count == 0 and items[seller] >= num_cheap:
                    buy(num_cheap, 0)     # buy num_cheap in bulk
                if count < num_cheap - 1: # do not buy single item \
                    buy(1, pay_extra)     #   when just 1 lower than num_cheap!
                if count >= num_cheap:
                    buy(1, 0)             # buy with no extra cost

Update 2: Also, since the order in which the items are added to the "path" does not matter, we can restrict the sellers to those that are not before the current seller. We can add the for seller, count in counts: loop to his:

        used_sellers = [i for i, (_, c) in enumerate(counts) if c > 0]
        min_sellers = used_sellers[0] if used_sellers else 0
        for i in range(min_sellers, len(counts)):
            seller, count = counts[i]

With those two improvements, the states in the explored graph looks for next(find_best(5, 4, 5, price, items)) like this (click to enlarge):

enter image description here

Note that there are many states "below" the goal state, with costs much worse. This is because those are all the states that have been added to the queue, and for each of those states, the predecessor state was still better than out best state, thus they were expanded and added to, but never actually popped from the queue. Many of those could probably be trimmed away by using A* with a heuristic function like items_left * min_price.

Community
  • 1
  • 1
tobias_k
  • 81,265
  • 12
  • 120
  • 179
  • Hi, sorry if this is a weird question, but what are the edges in your graph? Would it be a terrible amount of work to add a visualisation of your graph to your answer? – pandaadb Mar 06 '17 at 15:29
  • @pandaadb The edges would correspond to "buying one (or four) items from supplier X" and nodes would be "number of items from each supplier"; e.g. `(22, 1, ((1,1), (2,0), (3,0)) --(2,1)--> (45, 2, ((1,1), (2,1), (3,0))` – tobias_k Mar 06 '17 at 15:39
  • Thank you! Would this mean that you have a graph that has 1 level (source(nothing) -> target(a valid combination that fulfils the requirements)). So each path is by itself a result and you would have as many nodes as there are possible combinations of buying items? And then you would simply try all the nodes to see which one has the cheapest Edge from the source? – pandaadb Mar 06 '17 at 15:47
  • @pandaadb No, not each node is a valid combination. Only those where the `num` attributes equals the goal. Each edge corresponds to only buying a single items, _or_ buying `num_cheap` items at once in the last code to avoid negative costs. I might add a sample graph later, but not now. – tobias_k Mar 06 '17 at 15:52
  • Also, I just noticed that the algorithm could be improved, since the order in which items are bought does not play any role. So you could reduce the branching factor by e.g. prohibiting more items being bought from supplier one when you've already bought items from supplier two. – tobias_k Mar 06 '17 at 15:53
  • Hi - thank you for that. I misunderstood the algorithm, i went ahead and printed every step along the way and it starts to make a bit more sense to me :) I'd still like to see the graph (if you do have the time). A follow up question: Buying 1 item at a time will create (I think) a massive graph of all possible buy-combinations, which is what we need/want, but what is the complexity of this vs iterating through every possible combination in a nested loop to find the minimum? And does the Algorithm have a runtime advantage over a regular loop? – pandaadb Mar 06 '17 at 16:14
  • since this graph can have edges with negative weight, wouldn't any graph traversal algorithm need to visit every node at least once? And since the length of a path to any node in this graph is independent of chosen path, we end up iterating over every node and choosing the one with minimal cost. – Tymur Gubayev Mar 08 '17 at 19:09
  • @TymurGubayev See the first update to my answer: With this variation, there are no more negative weights. Thus Dijkstra's or A* should work fine. – tobias_k Mar 08 '17 at 19:37
0

This is a Bounded Knapsack problem. Where you want to optimize(minimize) the cost with the constraints of price and quantity.

Read about 0-1 KnapSack problem here. Where you have only 1 quantity for given supplier.

Read how to extend the 0-1 KnapSack problem for given quantity ( called Bounded Knapsack ) here

A more detailed discussion of Bounded KnapSack is here

These all will be sufficient to come up with an algorithm which requires a bit of tweaking ( i.g. adding 5$ when the quantity is below some given quantity )

Community
  • 1
  • 1
Anand Undavia
  • 3,493
  • 5
  • 19
  • 33
  • Not really sure how this applies to this problem... :S – Enrique Moreno Tent Mar 02 '17 at 14:40
  • [1/2]Visit [here](https://rosettacode.org/wiki/Knapsack_problem/Bounded). Try to match the problem described to yours; to be specific, in the table with pink headers given on the website, the 'item' is equivalent to your 'Supplier Name', 'weight' => your 'how many items to order' (In your case it is same because you do not have upperbound on 'items to order', you have a 'target'), value => your price (You have to minimize the while the example given maximizes it), prices => your units in storage. – Anand Undavia Mar 02 '17 at 18:13
  • [2/2]There are algorithms written in almost all famous language, not elegantly though but gives the way. There is once difference: The problem aims fills the knapsack as much as possible by picking different (item, value, piece) combination, and you have to reach to target by different (supplier, price, unit) combinations. And a tweak to so that if the purchased quantity from a particular supplier is higher than some amount, then the extra cost is removed – Anand Undavia Mar 02 '17 at 18:13