根据选择条件查找最便宜的项目组合

时间:2017-03-02 13:31:12

标签: algorithm combinatorics

假设我有3个特定商品的卖家。每个卖家都有不同数量的商品存储。该物品的价格也不同。

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

如果我没有从同一供应商处订购足够的物品,我必须支付一些额外的费用每个单位。比方说,例如,如果我不订购至少4个单位,我必须为每个订购的单位额外支付5美元。

一些例子:

如果我想购买4个单位,最好的价格来自供应商#1 供应商#2 ,而不是从供应商#3

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

但是,如果我要购买5个单位,那么从供应商3 获得所有单位给我的价格会比从更便宜的供应商获得更好的价格,其余来自更昂贵的供应商

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

问题

记住这一切......如果我事先知道我想订购多少件物品,那么找出我能选择的最佳组合的算法是什么?

2 个答案:

答案 0 :(得分:4)

comments所述,您可以使用图搜索算法,例如Dijkstra's algorithm。也可以使用A*,但为了做到这一点,您需要一个良好的启发式功能。使用最低价格可能有用,但就目前而言,让我们坚持使用Dijkstra。

图表中的一个节点表示为(cost, num, counts)的元组,其中cost是费用,显然,num购买的商品总数,counts每个卖家的商品数量细分。如果cost是元组中的第一个元素,则成本最低的项目将始终位于heap的前面。如果该卖家的当前计数低于最低值,我们可以通过添加费用来处理“额外费用”,并在达到该最低值后再次减去该费用。

这是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

以上是生成器函数,即您可以使用next(find_best(...))找到最佳组合,或迭代所有组合:

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)

正如我们所看到的,购买五件产品的解决方案更便宜:

(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)))

更新1:虽然以上工作正常但可以是失败的情况,因为一旦达到最小数量减去额外费用就意味着我们可以有边缘负成本,这可能是Dijkstra的一个问题。或者,我们可以在一个“动作”中一次添加所有四个元素。为此,用以下代码替换算法的内部部分:

            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

更新2:此外,由于项目被添加到“路径”的顺序无关紧要,我们可以将卖家限制为不在当前卖家之前的卖家。我们可以将for seller, count in counts:循环添加到他的:

        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]

通过这两项改进,探索图中的状态会像这样查找next(find_best(5, 4, 5, price, items))(点击放大):

enter image description here

请注意,目标状态“低于”许多州,成本更差。这是因为那些是已添加到队列中的所有状态,并且对于每个状态,前驱状态仍然优于最佳状态,因此它们被扩展并添加到但从未实际从队列中弹出。其中许多可能通过使用带有items_left * min_price等启发函数的A *来修剪掉。

答案 1 :(得分:0)

这是一个Bounded Knapsack问题。您希望在价格和数量限制的情况下优化(最小化)成本。

了解0-1 KnapSack问题here。给定供应商的数量只有1个。

了解如何扩展给定数量的0-1 KnapSack问题(称为Bounded Knapsackhere

Bounded KnapSack的更详细讨论是here

这些都足以提出一个需要稍微调整的算法(例如,当数量低于某个给定数量时加5美元)