查找具有选择条件的最便宜的项目组合

Posted

技术标签:

【中文标题】查找具有选择条件的最便宜的项目组合【英文标题】:Finding cheapest combination of items with conditions on the selection 【发布时间】:2017-03-02 13:31:12 【问题描述】:

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

名称 价格 存储单位 供应商 #1 17$ 1 单位 供应商 #2 18 美元 3 件 供应商 #3 23$ 5 件

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

一些例子:

如果我想购买 4 个单位,最好的价格来自 Supplier #1Supplier #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

问题

记住这一切...如果我事先知道我想订购多少商品,有什么算法可以找出我可以选择的最佳组合?

【问题讨论】:

例如,在 RDBMS 中,您可以在单个查询中遍历所有组合,并按总计排列。 你能给我写一个答案吗,请解释一下这样的查询是什么样的? 这基本上是一个最短路径问题,对吧?因此,如果我有时间,我可能会发布一个 RDBMS 解决方案,但您最好看看 Djikstra 的算法和类似的 np 完全问题。 @Strawberry 我也会对该解决方案感兴趣。我也在尝试这个,但我很难将需求转换成图表,所以如果你真的找到时间,那就太好了:)(或者如果你有一个我可以看的例子) 【参考方案1】:

如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))(点击放大):

请注意,有许多“低于”目标状态的状态,成本要高得多。这是因为这些都是已添加到队列中的状态,并且对于这些状态中的每一个,前驱状态仍然优于最佳状态,因此它们被扩展并添加到队列中,但从未真正从队列中弹出。其中许多可能可以通过使用带有启发式函数(如items_left * min_price)的 A* 来消除。

【讨论】:

嗨,如果这是一个奇怪的问题,很抱歉,但是你的图表中的边是什么?将图表的可视化添加到您的答案中会不会是一项可怕的工作? @pandaadb 边对应于“从供应商 X 购买一件(或四件)商品”,节点对应于“每个供应商的商品数量”;例如(22, 1, ((1,1), (2,0), (3,0)) --(2,1)--&gt; (45, 2, ((1,1), (2,1), (3,0)) 谢谢!这是否意味着您有一个具有 1 个级别的图表(源(无)-> 目标(满足要求的有效组合))。所以每条路径本身就是一个结果,你会拥有尽可能多的节点,因为有可能的购买项目组合?然后你会简单地尝试所有节点,看看哪个节点的边缘最便宜? @pandaadb 不,不是每个节点都是有效的组合。只有num 属性等于目标的那些。每条边对应于只购买单个商品,在最后一个代码中一次购买num_cheap 商品以避免负成本。稍后我可能会添加示例图表,但不是现在。 另外,我刚刚注意到算法可以改进,因为购买物品的顺序没有任何作用。因此,您可以通过例如减少分支因子当您已经从供应商二购买了商品时,禁止从供应商一购买更多商品。【参考方案2】:

这是一个Bounded Knapsack 问题。您想在价格和数量的约束下优化(最小化)成本的地方。

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

阅读如何扩展给定数量的0-1 KnapSack问题(称为Bounded Knapsack)here

Bounded KnapSack更详细的讨论是here

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

【讨论】:

不太确定这如何适用于这个问题...:S [1/2]访问here。尝试将描述的问题与您的问题相匹配;具体来说,在网站上带有粉红色标题的表格中,“项目”相当于您的“供应商名称”,“重量”=>您的“要订购的商品数量”(在您的情况下,它是相同的,因为您“订购的商品”没有上限,您有一个“目标”),价值 => 您的价格(您必须最小化,而给出的示例最大化它),价格 => 您的存储单位。 [2/2]几乎所有著名的语言都编写了算法,虽然不优雅但让路。有一次不同:问题的目标是通过选择不同的(项目、价值、件)组合来尽可能地填满背包,而你必须通过不同的(供应商、价格、单位)组合来达到目标​​。并进行了调整,如果从特定供应商处购买的数量高于某个数量,那么额外的成本就会被移除

以上是关于查找具有选择条件的最便宜的项目组合的主要内容,如果未能解决你的问题,请参考以下文章

查找具有给定总和的数字列表的所有组合

按组查找向量中最常见的组合

如何知道在multiSelect组合框中选择了多少项

可以用作 AKS 节点的最便宜的 VM 是啥?

项目开发中使用存储过程和直接使用SQL语句的区别

具有多个条件的数据表选择