Как правильно распределить сумму между элементами массива?

Как правильно распределить сумму скидки, применяемую ко всему заказу, по позициям товаров, учитывая их количество и стоимость, чтобы итоговая сумма соответствовала ожидаемому значению? 

Учитывая, что у меня есть переменные: 
- <b>discount</b> — величина скидки, 
- <b>total_price</b> — сумма заказа без скидки, 
- <b>total_price_final</b> — корректная сумма заказа (это сумма всех пунктов массива: price * amount).

Пример данных выглядит следующим образом:

<pre><code>{
   "items":[
   "discount":3748,
   "total_price":11050,
   "total_price_final":7302,
      {
         "id":1,
         "name":"Item #1",
         "artikul":"110062600000",
         "price":10000,
         "amount":1
      },
      {
         "id":2,
         "name":"Item #2",
         "artikul":"110000800000",
         "price":350,
         "amount":6,
         "total":2100
      }
   ]
}</code></pre>

Проблема в том, что текущий алгоритм распределения скидки дает некорректные результаты — итоговые суммы не совпадают или превышают допустимые значения. Есть ли у кого-то опыт в решении подобной задачи или имеются работающие алгоритмы, которые можно использовать?
  • 22 ноября 2024 г. 12:13
Ответы на вопрос 2
Для корректного распределения общей скидки по элементам массива товаров можно использовать пропорциональный метод, который учитывает стоимость и количество каждого товара. 

### Подход

1. **Вычислите общую стоимость всех товаров**:
   Общая стоимость всех товаров рассчитывается как сумма произведений стоимости каждого товара на количество:

   \[
   \text{total\_value} = \sum (\text{price} \times \text{amount})
   \]

   В вашем случае это уже известная переменная `total_price_final`, что упрощает расчет.

2. **Рассчитайте пропорцию скидки для каждого товара**:
   Для каждого товара можно рассчитать долю его стоимости по отношению к общей стоимости и умножить на общую скидку:

   \[
   \text{discount\_for\_item} = \left( \frac{\text{price} \times \text{amount}}{\text{total\_value}} \right) \times \text{discount}
   \]

3. **Обновите итоговые суммы для каждого товара**:
   Итоговая стоимость для каждого товара будет равна его исходной стоимости минус рассчитанная скидка для него:

   \[
   \text{final\_price} = (\text{price} \times \text{amount}) - \text{discount\_for\_item}
   \]

4. **Проверьте корректность итоговой суммы**:
   Убедитесь, что сводная сумма всех скорректированных цен равна ожидаемому значению.

### Пример реализации на Python
```python
data = {
    "discount": 3748,
    "total_price": 11050,
    "total_price_final": 7302,
    "items": [
        {
            "id": 1,
            "name": "Item #1",
            "artikul": "110062600000",
            "price": 10000,
            "amount": 1,
        },
        {
            "id": 2,
            "name": "Item #2",
            "artikul": "110000800000",
            "price": 350,
            "amount": 6,
        }
    ]
}

# Шаг 1: Вычисляем общую стоимость товаров
total_value = sum(item['price'] * item['amount'] for item in data['items'])

# Шаг 2 и 3: Распределяем скидку и обновляем финальные цены
for item in data['items']:
    item_total_value = item['price'] * item['amount']
    discount_for_item = (item_total_value / total_value) * data['discount']
    final_price = item_total_value - discount_for_item
    item['final_price'] = final_price

# Проверяем итоговую сумму
total_final_price = sum(item['final_price'] for item in data['items'])

# Результаты
print("Итоговая сумма:", total_final_price)
for item in data['items']:
    print(f"{item['name']}: финальная цена {item['final_price']:.2f}")
```

### Результат
Этот подход гарантирует, что скидка будет корректно распределена среди всех элементов, в итоге общая итоговая сумма будет равна ожидаемой `total_price_final`. Вы можете дополнительно доработать алгоритм для обработки погрешностей при округлении, если необходимо.
Во-первых, давайте считать в копейках. Умножте все цены на 100 перед началом алгоритма, и поделите на 100 при выводе. Потому что никогда нельзя деньги считать или хранить в вещественных числах. При сериализации и внешних интерфейсах, конечно, придется запятые ставить, но лучше при выводе выводить x/100 , "," и x%100 . И молиться, что у читателя хватит точности это прочитать, если он, конечно, не заморачивается с подобным трюком. 

Далее алгоритм (на псевдокоде):
total_price - изначальная цена. goods[i].price - цена товара, goods[i].amount - количество товара, discount - сколько скидки.
discount_left = discount
for i in 1..n {
  cur_discount = discount * goods[i].price / total_price # целочисленное деление нацело с округлением вниз.
  goods[i].price -= cur_discount
  discount_left -= cur_discount*goods[i].amount
}
for i in 1..n {
  if discount_left == 0: break
  if goods[i].amount <= discount_left {
    goods[i].price -= 1;
    discount_left -= goods[i].amount
  }  else {
   // разбиваем категорию на 2 штуки, в одной discount_left товаров, в другой остальное.
   new_good = Good{.price = goods[i].price-1, .amount = discount_left}
   goods[i].amount -= discount_left
   discount_left = 0
   goods.append(new_good)
   break
  }
}


Как это работает: если бы мы могли идеально делить копейки с любой точностью, то каждый товар получил бы скидку price*discount/total_price. Одинаковый процент скидки везде. но у нас-то целые копейки, поэтому если мы эти скидки округлим вниз, то мы сколько-то копеек недоскинем. Но для каждой штуки товара мы потеряли меньше одной копейки, а значит суммарно - меньше общего количества товаров. Значит можно из каких-то товаров вычесть 1 и все сойдется. Вот и вычитаем из первых discount_left товаров. Если категория целиком покрыта - просто уменьшаем у нее цену. Если нет, то разбиваем на 2 куска и у одного цену уменьшаем.

Тут могут быть проблемы, если скидка очень большая и есть товары очень маленькой стоимости. Скажем, товар стоимостью 5 копеек и кидка в 85% уже между 0 и 1. Округленная вниз она будет 4, но вот если вы из этого товара потом еще и 1 вычтите, то может так получится, что вы снизите цену до 0. Допустимо ли это? Чтобы этого избежать надо товары отсортировать по убыванию цены. Но все равно может не помочь, тогда надо тогда такие товары ценой в 1 копейку исключить из рассмотрения и запустить цикл вычитания 1 опять, если после цикла discount_left не 0.

Второй момент, если вам очень хочется избегать разбиения категорий на части, то можно попробовать реализовать динамическое программирование для решения задачи размена монет , и таким образом найти какие категории надо взять целиком, чтобы набрать discount_left. Но вряд ли вам это так уж надо, ибо предвижу весьма частый случай, когда разбивать все-равно придется. Да и разбиение тут максимум одно происходит.
Похожие вопросы