Как пропорционально распределить скидку на строки заказа? - PullRequest
0 голосов
/ 15 февраля 2019

У меня есть заказ с количеством строк и скидкой, который должен быть распределен между этими линиями пропорционально стоимости линии.

Я не математик, поэтому я бы ввел эту запись, чтобы объяснить случай.Заказ имеет 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, скидка не превышает общую стоимость заказа
  • D, Di и Qi - целочисленные значения, 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

Этот алгоритм адаптирует некоторые из найденных решений здесь и работает для большинства случаев.Однако, это терпит неудачу в случаях как:

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

Существует по крайней мере одно возможное распределение: D1=22; D2=30, но текущий алгоритм не может обнаружить его.Так что же лучше алгоритм распределения или лучший алгоритм распределения остатка в частности?

1 Ответ

0 голосов
/ 15 февраля 2019

Давайте интерпретировать Di/D ~ Ti/T как Di ∈ {Qi*floor(D*Pi/T), Qi*ceiling(D*Pi/T)}.Тогда мы можем решить эту проблему как подмножество суммы, а именно, для каждого i, такого, что D*Ti/T/Qi не является целым числом и для которого Qi*ceiling(D*Pi/T) ≤ Pi, у нас есть элемент веса Q_i, а целевая сумма равнаD - sum_i Q_i*floor(D*Pi/T).Сумма подмножества является NP-сложной, но очень слабой, и, если у вас нет огромных количеств, ее решение не должно быть проблемой при использовании обычной динамической программы.Если проблема неосуществима, вы можете использовать финальные таблицы из динамической программы, чтобы выяснить, каков наилучший остаток.

В качестве расширения вы можете предпочесть решения, где, хотя D*Ti/T не являетсяцелое число, Di ближе к этому отношению, чем альтернатива.Вы можете определить прибыль, например, |floor(D*Ti/T/Qi) - D*Ti/T|^2 - |ceiling(D*Ti/T/Qi) - D*Ti/T|^2, которая предпочитает товары, где оптимальная скидка ближе к потолку, чем к полу.Затем вы решаете проблему ранца (ну, вроде как, поскольку «прибыль» может быть отрицательной) вместо суммы подмножества, но DP не сильно меняется.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...