As noted in the comments , for this you can use the graph search algorithm, for example the Dijkstra algorithm . It is also possible to use A * , but for this you need a good heuristic function. Using the lowest price may work, but for now, let me stick with Dijkstra's.
One node in the graph is represented as a set (cost, num, counts) , where cost is the cost, obviously, num total number of items purchased and counts breakdown of the number of goods per seller. If cost is the first item in the tuple, the item with the lowest cost will always be at the beginning of the heap . We can process the “extra charge” by adding a fee if the current account for this seller is below the minimum and deducts it again once we reach this minimum.
Here is a simple implementation in Python.
import heapq def find_best(goal, num_cheap, pay_extra, price, items):
The above generator function, i.e. you can use next(find_best(...)) to find only the best combination or next(find_best(...)) over all 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 is 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: Although the above example is suitable for the example, there may be times when it fails, since subtracting the extra cost after reaching the minimum number means that we can have faces with a negative value, which can be a problem in Dijkstra. In addition, we can add all four elements at once to one action. To do this, replace the inside of the algorithm as follows:
if count < items[seller]: def buy(n, extra):
Update 2: In addition, since the order in which items are added to the “path” does not matter, we can limit sellers to those that are not in front of the current seller. We can add a cycle for seller, count in counts: to it:
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 these two improvements, the state in the graph under study looks like next(find_best(5, 4, 5, price, items)) (click to enlarge):

Note that there are many states “below” the state of the target, and the costs are much worse. This is because these are all states that have been added to the queue, and for each of these states the state of the predecessor is still better than the best state, therefore they were expanded and added, but they never jumped out of the queue. Many of them can probably be trimmed using A * with a heuristic function such as items_left * min_price .