寻找具有选择条件的物品的最便宜组合

6
假设我有3个卖家销售特定商品,每个卖家存储的商品数量不同,价格也不同。
名称 价格 存储单位
供应商1 17美元 1个单位
供应商2 18美元 3个单位
供应商3 23美元 5个单位
如果我没有从同一供应商订购足够数量的商品,则需要按每个单位支付额外费用。例如,如果我未订购至少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

问题

考虑到所有这些因素...如果我预先知道我想要订购多少个物品,有什么算法可以找出我可以选择的最佳组合?


在关系型数据库中,你可以通过单个查询迭代所有组合,并按总计排列它们。 - Strawberry
请问您能否给我写一篇回答,解释一下这样的查询是什么样子的?谢谢。 - Enrique Moreno Tent
3
这基本上是一个最短路径问题,对吧?所以,如果我有时间,我可以发布一个RDBMS解决方案,但你最好看看Djikstra算法和类似的np完全问题。 - Strawberry
@Strawberry 我也对那个解决方案很感兴趣。我也在尝试,但是我很难将要求转化为图形,所以如果你有时间的话,能提供一下就太好了 :) (或者如果你有示例我可以参考的话) - pandaadb
2个回答

5
正如在评论中提到的,您可以使用图搜索算法来解决此问题,例如Dijkstra算法。也可能可以使用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:虽然上述示例中的方法是有效的,但有时它可能会失败,因为在达到最小数量后减去额外成本意味着我们可能会得到带有负成本的边,这可能在迪杰斯特拉算法中会出现问题。作为替代方案,我们可以在一个“操作”中一次性添加所有四个元素。为此,请使用以下算法替换其内部部分:
            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*搜索,可以消除其中许多状态。


嗨,如果这是一个奇怪的问题,我很抱歉,但是你的图中的边缘是什么?将您的图形可视化添加到您的回答中会是一个非常繁琐的工作吗? - pandaadb
@pandaadb 边缘对应于“从供应商X购买一个(或四个)项目”,节点将是“每个供应商的项目数量”;例如,(22, 1, ((1,1), (2,0), (3,0)) --(2,1)--> (45, 2, ((1,1), (2,1), (3,0)) - tobias_k
谢谢!这是否意味着您有一个只有1个级别的图(源(无)->目标(满足要求的有效组合))?因此,每条路径本身都是一个结果,并且您将拥有与购买物品可能组合数量相同的节点?然后,您只需尝试所有节点,以查看哪个从源开始具有最便宜的边缘即可。 - pandaadb
另外,我刚刚注意到算法可以改进,因为购买物品的顺序并不起任何作用。因此,例如,当您已经从供应商二购买了物品时,可以通过禁止从供应商一购买更多物品来减少分支因子。 - tobias_k
嗨 - 谢谢你。我误解了算法,然后我一步一步地打印出来,现在对我来说有点更清晰了 :) 如果你有时间,我仍然想看看图表。跟进问题:每次购买一个项目将创建(我认为)所有可能的购买组合的大量图表,这正是我们需要/想要的,但与通过嵌套循环迭代每个可能的组合以找到最小值相比,这种方法的复杂度如何?并且,该算法是否具有运行时优势,超过常规循环? - pandaadb
显示剩余3条评论

0
这是一个有界背包问题。您需要在价格和数量的约束条件下优化(最小化)成本。

阅读关于0-1背包问题的内容here。在这里,您只有给定供应商的1个数量。

阅读如何扩展给定数量的0-1背包问题(称为有界背包here

有界背包的更详细讨论在here

所有这些都足以提出一个算法,该算法需要进行一些微调(例如,在数量低于某个给定数量时添加5美元)


不太确定这如何适用于这个问题... :S - Enrique Moreno Tent
[1/2]请访问这里。尝试将描述的问题与您的问题匹配;具体来说,在网站上给出的带粉色标题的表格中,“item”相当于您的“供应商名称”,“weight” => 您要订购的“物品数量”(在您的情况下,它相同,因为您没有“订购物品”的上限,而是有一个“目标”),价值=> 您的价格(您必须最小化,而给出的示例则最大化),价格=> 您存储的单位。 - Anand Undavia
几乎所有著名的编程语言都写有算法,虽然不够优雅但可行。其中一个区别是问题的目标是通过选择不同的(物品,价值,数量)组合尽可能地填满背包,而你必须通过不同的(供应商,价格,单位)组合来达到目标。如果从特定供应商购买的数量超过某个数量,则需要进行微调以删除额外费用。 - Anand Undavia

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接