如何在订单行上按比例分配折扣?

时间:2019-02-15 12:54:42

标签: algorithm math f#

我有一个带有行数和折扣的订单,需要在这些行之间按行成本成比例地分配折扣。

我不是数学家,因此我将引入这种表示法来解释这种情况。订单有N行,项目价格为Pi,项目数量为Qi,总行成本为Ti,其中Ti = Qi * Pi。订单总价为T = sum(Ti)。算法需要分配折扣D,结果是Di的列表-每个订单行的分配折扣。 结果必须满足以下条件:

  • D = sum(Di):线路折扣总和必须等于原始折扣
  • Di%Qi = 0:折扣必须能被数量整除而没有余数
  • Di <= Ti:折扣不得超过订单总费用
    • Di/D ~ Ti/T:折扣尽可能按比例分配

输入数据满足以下条件:

  • D <= T,折扣不超过订单总费用
  • DDiQi是整数值,Pi是十进制值
  • 有些输入数据不能满足要求的条件。例如,3行,每行有3个项目,价格为10,输入折扣为10(N=3; Qi=3; Pi=10; D=10)。无法将其整除为行数。在这种情况下,算法应返回无法分配的折扣金额错误(在我的示例中为1)

现在我们的算法实现如下所示(F#的简化版本)

type Line = {
  LineId: string
  Price: decimal
  Quantity: int
  TotalPrice: decimal
  Discount: decimal
}

module Line =
  let minimumDiscount line =
    line.Quantity
    |> decimal
    |> Some
    |> Option.filter (fun discount -> discount <= line.TotalPrice - line.Discount)

  let discountedPerItemPrice line = line.Price - line.Discount / (decimal line.Quantity)

let spread discount (lines: Line list) =
  let orderPrice = lines |> List.sumBy (fun l -> l.TotalPrice)
  let preDiscountedLines = lines |> List.map (fun line ->
    let rawDiscount = line.TotalPrice / orderPrice * discount
    let preDiscount = rawDiscount - rawDiscount % (decimal line.Quantity)
    {line with Discount = preDiscount})

  let residue = discount - List.sumBy (fun line -> line.Discount) preDiscountedLines

  let rec spreadResidue originalResidue discountedLines remainResidue remainLines =
    match remainLines with
    | [] when remainResidue = 0m -> discountedLines |> List.rev |> Ok
    | [] when remainResidue = originalResidue -> sprintf "%f left to spread" remainResidue |> Error
    | [] -> discountedLines |> List.rev |> spreadResidue remainResidue [] remainResidue
    | head :: tail ->
      let minimumDiscountForLine = Line.minimumDiscount head
      let lineDiscount = minimumDiscountForLine
                           |> Option.filter (fun discount -> discount <= remainResidue)
                           |> Option.defaultValue 0m
      let discountedLine = {head with Discount = head.Discount + lineDiscount}
      let discountedLines = discountedLine :: discountedLines
      let remainResidue = remainResidue - lineDiscount
      spreadResidue originalResidue discountedLines remainResidue tail

  spreadResidue residue [] residue preDiscountedLines

该算法适用于here找到的一些解决方案,并且适用于大多数情况。 但是,在以下情况下失败:

P1=14.0; Q1=2;
P2=11.0; Q2=3;
D=52

至少存在一种可能的分布:D1=22; D2=30,但是当前算法无法发现它。那么,什么是更好的扩散算法,或者是更好的扩散残留物算法?

1 个答案:

答案 0 :(得分:0)

让我们将Di/D ~ Ti/T解释为Di ∈ {Qi*floor(D*Pi/T), Qi*ceiling(D*Pi/T)}。然后,我们可以解决这个问题,例如,对于每个i都不是整数,并且对于每个D*Ti/T/Qi来说,我们有一个权重为{{1 }},目标总和为Qi*ceiling(D*Pi/T) ≤ Pi。子集总和是NP硬性的,但是只有很少的一部分,除非您有大量,否则使用常规动态程序解决它应该没有问题。如果问题不可行,则可以使用动态程序中的最终表来找出剩余的最好的部分。

作为扩展,您可能希望使用以下解决方案:尽管Q_i不是整数,但D - sum_i Q_i*floor(D*Pi/T)比替代方案更接近该比例。您可以定义诸如D*Ti/T之类的项目利润,以支持最佳折扣比最低折扣更接近最低折扣的项目。然后,您要解决一个背包问题(因为“利润”可能为负),而不是子集总和,但是DP的变化不大。