购物车最小化算法

7

我有一个产品列表,其中包含了销售它的商店列表。

{
    'Book A': [ShopA, ShopB, ShopC],
    'Book B': [ShopC, ShopD],
    'Movie C': [ShopA, ShopB, ShopD, ShopE],
    ...
}

(价格因商店而异)

每家商店还有一笔运费。这是一笔“按订单”计算的运费,无论购物车中有多少件商品都不会改变。而且,这个费用也因商店而异。

例如:如果我从ShopA购买“A书”,从ShopC购买“B书”,并从ShopA购买“C电影”,那么最终价格为:ShopA中A书的价格 + ShopC中B书的价格 + ShopA中C电影的价格 + ShopC的运费 + ShopA的运费

如果运费为零或按项计费且保持不变,则我只需按price+shipping字段对报价列表进行排序并从每个集合中获取第一个结果。

我需要购买全部商品一次性购买,并找到最小价格对应的商品集。

我不太擅长优化算法和动态规划,所以我需要一个解决方案或者指引正确的方向。


你能给出你需要处理多少商店和产品的估计吗?目前,我已经想出了一个算法,但它只对很少量的产品有效,我有一种感觉这不是我们要处理的情况... - Boris Strandjev
求求你了,实现一下吧。这就是我最讨厌亚马逊之类的网站的原因:不仅我事先不知道运费,而且直到我结账时才知道他们是否会向某个地址发货。 - vgru
4个回答

6
这个问题是NP难的。
我们将展示从Hitting Set问题的约简。
Hitting Set问题:给定集合S1,S2,...,Sn和一个数字k:选择大小为k的集合S,使得对于每个Si,都有一个元素sS中,使得sSi中。[另一种定义:每个SiS之间的交集不为空]。

约简:
给定一个Hitting Set实例,形式为(S1,...,Sn,k),创建该问题的一个实例:

All books cost nothing. In order to buy from each store you pay 1. 
The book i is sold by each store denoted in Si, minimal price for this instance is k.

证明
击中集合 -> 这个问题:假设在大小为k(S1,...,Sn)的最小击中集中存在一个击中集S。通过从S中的每个商店购买,我们可以以成本k购买所有书籍,因为书籍在我们的构建中不花费任何费用,我们购买了所有书籍,并支付了来自商店的订购费用恰好为k,因此总价格为k
这个问题 -> 击中集合:假设问题的定价为k。然后,根据问题的构建,由于书籍不花费任何费用,我们需要在k个不同的商店购买所有书籍。让这些商店为S。从问题的构建中,S(S1,...,Sn)的一个击中集。
证毕。

结论:
因此,这个问题与Hitting Set Problem同样难度,目前没有已知的多项式解决方案,所以-如果你想要最优解,你最好选择指数级别的解决方案,比如回溯算法 [检查所有可能性,并返回最小的解决方案]。


我认为这个答案不正确。也许你不需要从特定的商店预订任何产品,因为该商店提供的所有产品在其他商店里都更便宜。此外,你的解释中没有包括"运费"。 - ABu

5
我有一个解决方案,可以处理如此少的项目。它是动态的。
我们将迭代地处理每个商店。在每个步骤中,我们存储当前最佳价格,以覆盖所有子项。一开始时,除了空子集(价格为0)以外,所有子集的价格都是无限的。请注意,所有子集的数量为2^Num_products,但在您的情况下只有大约1000个。
现在我们如何处理接下来要跟随的商店:考虑用这家商店涵盖产品的所有可能子集(我指商店实际提供的子集),并让所有其余的产品由你已经观察过的商店涵盖,从而改善覆盖每个子集的最小成本。这一步需要2^Num_products * 2^Num_products = 4^Num_products的时间,大约一百万次,可以承受。您对每个商店都要这样做,最终答案是覆盖所有元素的成本。所提出解决方案的整体复杂度为4^Num_products * num_shops,约为5000万,适合使用。
请注意,这仍然是指数级的,这不足为奇。感谢amit提供关于NP难问题的令人难以置信的证明。
编辑:添加算法的伪代码进一步解释:
init:
 cost[subset] = infi
 cost[{}] = 0
for shop in shops
 new_prices = costs.dup()
 for set : subsets
   for covered_set : all_subsets(set)
     price = covered_set == {} ? 0 : delivery[shop]
     remaining = set
     for element : covered_set
       if shop do not sale element
         break for, choose next covered_set
       price += el_price[element]
       remaining.remove(element)
     price += costs[remaining]
     new_prices[set] = min(new_prices[set], price)
 costs = new_prices
return costs[all]

请注意,这里我使用集合作为索引 - 这是因为我实际上使用子集的位掩码表示,例如1101是包含第一个、第二个和第四个元素的子集。因此,所有集合的迭代都是 for (int i = 0; i < (1 << n); i++)
还有一件事:如果您想循环遍历一个子集S的所有子集,可以比迭代初始集合的所有子集并检查子集是否是S的子集更快地完成此操作。如果S也用位掩码 bit_mask 表示,则此 for 循环执行此任务:for(int i = bit_mask; i > 0; i = (i - 1) & bitmask)。使用这种方法,您可以将算法复杂度降低到3^Num_products * num_shops。但是,这有点难以理解,您可能需要手写一个示例,以确保我编写的循环实际上循环遍历了S的所有子集。关于复杂度-请相信我。
编辑2:修改了break条件,并让我详细说明一下集合remaining 及其计算:正如dmzkrsk指出的那样,伪代码提到了从集合中删除,但是如果使用位掩码表示子集,则实际上可以将 remaining = set ^ covered_set (再次是位操作)。

对我来说仍然不是完全清楚的。你能提供一个伪代码或者在一些小型书店/商店的设置中跟着步骤吗? - dmzkrsk
完成了,如果您需要更多关于子集表示的解释,请告诉我。 - Boris Strandjev
好的,我已经实现了它,看起来它是有效的。我只有两个问题需要确认:1)如果“set”和“covered_set”是位掩码,那么“remaining”是否等于“set - covered_set”,因此我们不需要“remaining.remove”数组逻辑?2)“break for on covered_set”意味着我们移动到“for covered_set:all_subsets(set)”循环中的下一个“covered_set”或放弃该循环并移动到“for set:subsets”的下一个集合? - dmzkrsk
感谢您的评论。两个问题都是非常合理的。我会在下一次编辑中放置答案。 - Boris Strandjev
我目前在思考这个算法/方法是否可以扩展以支持“数量”:每家商店只有一定数量的书存货,而我的购物车看起来像是“2本A书,3本C书”。有人有想法如何解决吗? - lacco

2
我曾经遇到过这个问题。我没有想到其他解决方案,只是测试了每种可能的商店组合,但是有一种简单的方法可以过滤掉每个产品中的许多商店。
1.计算每个产品(包括运费)的最低价格,我们称之为最佳价格。
2.在每个产品中,仅保留商店的价格(不含运费)<=最佳价格(含运费)。
3.测试每种可能的商店组合以获得最便宜的价格。

另外,算法的另一个巨大时间优化:跟踪最佳组合总数。如果当前总和大于迄今为止找到的最佳总和,则跳过下一次迭代。这两个优化使我的解决方案对于合理数量的组合变得可行。 - Yuri Ghensev

0
一个好的启发式算法可以是蚁群优化。我用它来解决旅行商问题。你可以在谷歌TSP求解器中找到一个工作示例。这是一个使用暴力和动态规划解决方案的JavaScript库。当你需要计算的城市数量超过当前限制的20个城市时,就会使用AOC。我相信你可以使用该库来解决你的问题,只需要稍微改写一下。对于20个城市,程序必须检查20!种可能性。在你的情况下,它可能会轻松一些,但也许只有一个数量级。

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