非重叠购买的最佳顺序

时间:2012-10-02 00:39:59

标签: algorithm scheduling

我认为这是一个调度问题,但我对此并不确定!我想要的是找到非重叠购买决策的最佳顺序,当我完全了解它们的价值以及将来会出现什么样的机会时。

想象一下,批发商出售我想为自己的商店购买的各种商品。他们可能随时有多个特别优惠;我会以全价出售,所以他们的折扣是我的利润。

我希望利润最大化,但问题是我一次只能购买一件东西,而不是信贷,更糟糕的是,有交货延迟。好消息是我会在交付后立即出售这些物品,然后我可以再次花钱。因此,所有选择的一条路径可能是:我周​​一购买100公斤苹果,周二送货。然后我在星期天买了20件尼姑服装。我跳过了几天,正如我周三所知,他们将有一个很大的折扣法拉利。所以我买了其中一个,它是在下周二交付的。等等。

您可以考虑是否复利。该算法归结为在选择今天的特价之一或等待一天之间的每个阶段的决定,因为明天会有更好的事情发生。

让我们抽象一下。购买和交付成为自纪元以来的日子。利润写成卖价除以买入价。即1.00表示收支平衡,1.10表示10%的利润,2.0表示我的钱增加了一倍。

  buy    delivery   profit
    1        2       1.10    Apples
    1        3       1.15    Viagra
    2        3       1.15    Notebooks
    3        7       1.30    Nun costumes
    4        7       1.28    Priest costumes
    6        7       1.09    Oranges
    6        8       1.11    Pears
    7        9       1.16    Yellow shoes
    8       10       1.15    Red shoes
   10       15       1.50    Red Ferrari
   11       15       1.40    Yellow Ferrari
   13       16       1.25    Organic grapes
   14       19       1.30    Organic wine

注意:机会只存在于购买日(例如,如果没有人购买,有机葡萄会变成葡萄酒!),我会在交货的同一天卖掉,但是不能买到我的下一个商品直到次日。因此,我不能在t = 7时出售我的修女服装,并在​​t = 7时立即购买黄色鞋子。

我希望存在一个已知的最佳算法,并且已经有一个R模块,但算法或学术文献也会很好,就像任何其他语言一样。速度很重要,但主要是在数据变大时,所以我想知道它是否是O(n 2 ),或者其他什么。

顺便说一下,如果有最大可能的传递延迟,最好的算法会改变吗?例如。如果delivery - buy <= 7

以上数据为CSV:

buy,delivery,profit,item
1,2,1.10,Apples
1,3,1.15,Viagra
2,3,1.15,Notebooks
3,7,1.30,Nun costumes
4,7,1.28,Priest costumes
6,7,1.09,Oranges
6,8,1.11,Pears
7,9,1.16,Yellow shoes
8,10,1.15,Red shoes
10,15,1.50,Red Ferrari
11,15,1.40,Yellow Ferrari
13,16,1.25,Organic grapes
14,19,1.30,Organic wine

或者作为JSON:

{"headers":["buy","delivery","profit","item"],"data":[[1,2,1.1,"Apples"],[1,3,1.15,"Viagra"],[2,3,1.15,"Notebooks"],[3,7,1.3,"Nun costumes"],[4,7,1.28,"Priest costumes"],[6,7,1.09,"Oranges"],[6,8,1.11,"Pears"],[7,9,1.16,"Yellow shoes"],[8,10,1.15,"Red shoes"],[10,15,1.5,"Red Ferrari"],[11,15,1.4,"Yellow Ferrari"],[13,16,1.25,"Organic grapes"],[14,19,1.3,"Organic wine"]]}

或作为R数据框:

structure(list(buy = c(1L, 1L, 2L, 3L, 4L, 6L, 6L, 7L, 8L, 10L, 
11L, 13L, 14L), delivery = c(2L, 3L, 3L, 7L, 7L, 7L, 8L, 9L, 
10L, 15L, 15L, 16L, 19L), profit = c(1.1, 1.15, 1.15, 1.3, 1.28, 
1.09, 1.11, 1.16, 1.15, 1.5, 1.4, 1.25, 1.3), item = c("Apples", 
"Viagra", "Notebooks", "Nun costumes", "Priest costumes", "Oranges", 
"Pears", "Yellow shoes", "Red shoes", "Red Ferrari", "Yellow Ferrari", 
"Organic grapes", "Organic wine")), .Names = c("buy", "delivery", 
"profit", "item"), row.names = c(NA, -13L), class = "data.frame")

链接

Are there any R Packages for Graphs (shortest path, etc.)?igraph提供了一个shortest.paths函数,除了C库之外,还有一个R package和一个python接口)

4 个答案:

答案 0 :(得分:6)

考虑这个问题的最简单方法与shortest-path problem类似(尽管将其视为maximum flow problem可能在技术上更好)。日期编号1 ... 19可用作节点名称;每个节点 j 都有一个到节点 j + 1 的链接,权重为1,每个产品( b,d,g,p )都在列表添加了从 b 到日期 d + 1 的链接,其权重为 g 。当我们在路径寻找时通过节点前进时,我们会跟踪到目前为止在每个节点看到的最佳乘法值。

下面显示的Python代码在时间O(V + E)中运行,其中V是顶点数(或天数),E是边数。在该实施方式中,E = V +正在销售的产品数量。 添加了注释:循环 for i,t in enumerate(graf):将每个顶点视为一次。在该循环中,表示边缘中的e:每个处理来自当前顶点的边。因此,没有边缘被处理多次,因此性能为O(V + E)。

编辑注释2: krjampani声称O(V + E)比O(n log n)慢,其中n是产品数量。但是,除非我们对所考虑的天数做出假设,否则这两个订单是不可比较的。请注意,如果交货延迟有界且产品日期重叠,则天数为O(n),其中O(V + E)= O(n),这比O(n log n)快。

然而,在给定的一组假设下,我的方法和krjampani的运行时顺序可以是相同的:对于大量的天来,改变我的方法只在x [0]的有序联合中的几天创建图节点和x [1]值,并使用指向day [i-1]和day [i + 1]的链接,而不是指向i-1和i + 1的链接。对于少数天,更改krjampani的方法以使用O(n)计数排序。

程序的输出如下所示:

 16 :   2.36992 [11, 15, 1.4, 'Yellow Ferrari']
 11 :   1.6928 [8, 10, 1.15, 'Red shoes']
 8 :    1.472 [4, 7, 1.28, 'Priest costumes']
 4 :    1.15 [1, 3, 1.15, 'Viagra']

表示我们在第16天到达第16天,复合利润为2.36992,并在第15天卖出黄色法拉利;卖红鞋之后,第11天到达利润1.6928;等等。请注意,产品列表开头的虚拟条目以及数字周围的引号的删除是与JSON数据的主要区别。列表元素graf[j]中的条目以[1, j-1, 0, [[j+1,1,0]]]开头,即形式为[最佳价值 - 迄今为止,最佳节点#,最佳产品密钥,边缘] -list。每个边列表都是列表的列表,其形式为[next-node#,edge-weight,product-key]。将产品0作为虚拟产品可以简化初始化。

products = [[0,0,0,""],[1,2,1.10,"Apples"],[1,3,1.15,"Viagra"],[2,3,1.15,"Notebooks"],[3,7,1.30,"Nun costumes"],[4,7,1.28,"Priest costumes"],[6,7,1.09,"Oranges"],[6,8,1.11,"Pears"],[7,9,1.16,"Yellow shoes"],[8,10,1.15,"Red shoes"],[10,15,1.50,"Red Ferrari"],[11,15,1.40,"Yellow Ferrari"],[13,16,1.25,"Organic grapes"],[14,19,1.30,"Organic wine"]]
hiDay = max([x[1] for x in products])
graf = [[1, i-1, 0, [[i+1,1,0]]] for i in range(2+hiDay)]

for i, x in enumerate(products):
    b, d, g, p = x[:]
    graf[b][3] += [[d+1, g, i]] # Add an edge for each product

for i, t in enumerate(graf):
    if i > hiDay: break
    valu = t[0]                 # Best value of path to current node
    edges = t[3]                # List of edges out of current node
    for e in edges:
        link, gain, prod = e[:]
        v = valu * gain;
        if v > graf[link][0]:
            graf[link][0:3] = [v, i, prod]

day = hiDay
while day > 0:
    if graf[day][2] > 0:
        print day, ":\t", graf[day][0], products[graf[day][2]]
    day = graf[day][1]

答案 1 :(得分:4)

该问题自然地映射到在一组加权区间中找到最大权重无关区间的问题。输入集中的每个项目对应一个间隔,其起点和终点是买入和交付日期,项目的利润代表间隔的权重。最大权重无关区间问题是找到一组总重量最大的不相交区间。 enter image description here

问题可以在O(n log n)中解决,如下所示。按其终点对间隔进行排序(参见图)。然后,我们遍历排序列表中的每个间隔i,并计算子问题的最优解,该子问题由排序列表中1...i的区间组成。区间1...i的问题的最佳解是最大值:

1. The optimal solution of the problem for intervals `1...(i-1)` in the 
   sorted list or

2. Weight of interval `i` + the optimal solution of the problem for intervals 
   `1...j`, where j is the last interval in the sorted list whose end-point
   is less than the start-point of `i`.

请注意,此算法在O(n log n)中运行,并为排序列表的每个前缀计算最佳解法的值。 运行此算法后,我们可以按相反的顺序遍历排序列表,并根据为每个前缀计算的值找到最优解中存在的区间。

编辑:

为了使其正常工作,间隔的权重应该是相应项目的实际利润(即它们应该是sell_price - buy_price)。

更新2:运行时间

V为天数(遵循jwpat7的表示法)。 如果V远小于O(n log n),我们可以使用计数排序在O(n + V)时间内对间隔进行排序,并使用大小为V的数组来记录解决方案子问题。这种方法导致O(V + n)的时间复杂度。 因此算法的运行时间为min(O(V+n), O(n log n))

答案 2 :(得分:2)

这是一个动态编程问题。做出总体最佳选择只需要在每一步做出最佳选择。您可以根据以前的状态和从该状态采取各种步骤的利润,创建一个描述每个步骤的最佳选择的表。你可以通过消除那些显然不是最佳的可能性,将大量可能性折叠成一个较小的集合。

在您的问题中,影响选择的唯一状态是交付日期。例如,在第一天,您有三种选择:您可以购买苹果,将利润设置为1.10,并将交货日期设置为2;购买伟哥,将您的利润设置为1.15,并将您的交货日期设置为3;或什么都不买,将您的利润设置为零,并将交货日期设置为2.我们可以代表这些替代方案:

(choices=[apples], delivery=2, profit=1.10) or
(choices=[viagra], delivery=3, profit=1.15) or
(choices=[wait],   delivery=2, profit=0.00)

在未来做出决定时,无论是购买伟哥还是在第一天不买东西都没有任何区别。无论哪种方式,第二天您可以进行购买,第二天,因此您可以消除等待作为替代,因为利润较低。但是,如果你购买苹果,这将影响未来的决定,而不是购买伟哥或等待,所以这是一个不同的选择,你必须考虑。这只是在第一天结束时给你留下这些替代品。

(choices=[apples], delivery=2, profit=1.10) or 
(choices=[viagra], delivery=3, profit=1.15)

对于第二天,您需要根据第一天的替代方案考虑您的替代方案。这产生了三种可能性:

(choices=[apples,notebooks], delivery=3, profit=2.25) or
(choices=[apples,wait],      delivery=3, profit=1.10) or
(choices=[viagra,wait],      delivery=3, profit=1.15)

考虑到未来的决策,所有这三个选择都会使您处于相同状态,因为它们都将交付日期设置为3,因此您只需选择利润最高的那个:

(choices=[apples,notebooks], delivery=3, profit=2.25)

继续第三天,你有两个选择

(choices=[apples,notebooks,wait],         delivery=4, profit=2.25)
(choices=[apples,notebooks,nun costumes], delivery=7, profit=3.55)

这两种选择都必须保留,因为它们会以不同的方式影响未来的决策。

请注意,我们只是根据交货日期和利润做出未来决策。我们会对这些选择进行跟踪,以便我们能够在最后报告最佳选择。

现在也许你可以看到这种模式。您有一组备选方案,只要您有多个具有相同交货日期的替代方案,您只需选择具有最大利润的方案并消除其他方案。这种折叠替代方案的过程可以使问题成倍增长,从而有效地解决问题。

答案 3 :(得分:1)

您可以将此解决为linear programming问题。这是解决物流问题的标准方法,例如航空公司和公司面临的问题,问题空间比您的大得多。我不会在这里正式定义您的问题,但从广义上讲:您的目标函数是利润的最大化。您可以将购买天数和“每天只有一次购买”表示为线性约束。

标准线性规划算法是simplex method。虽然它具有指数最坏的情况行为,但在实践中,它往往在实际问题上非常有效。有很多免费的可用实现。我最喜欢的是GNU Linear Programming Kit。 R有an interface to GLPKLp_solve是另一个众所周知的项目,也有R interface。每种情况下的基本方法是正式定义您的问题,然后将其交给第三方解决方案来做其事情。

要了解详情,建议您查看Algorithm Design Manual,这应该为您提供足够的背景资料,以便在线进行进一步的研究。 p.412以后是线性规划及其变化的全面总结(例如,如果你有完整性约束,则为整数规划)。

如果您之前从未听说过线性编程,您可能希望看看如何使用它的一些示例。我非常喜欢Python中的这个simple set of tutorial problems。它们包括最大限度地利用猫粮罐头,解决数独问题。