разделение счета алгоритмически и честно, потом :)

Я пытаюсь решить следующую реальную проблему, с которой вы могли столкнуться сами:

Вы ужинали с друзьями и согласились разделить счет поровну. За исключением того, что когда, наконец, приходит счет, вы обнаруживаете, что не у всех достаточно денег (если есть, то дешевые ублюдки).

Итак, некоторые из вас платят больше, чем другие ... Потом вы приходите домой и пытаетесь решить, «кто кому и какую сумму должен?».

Я пытаюсь решить эту проблему алгоритмически и честно :)

Сначала это кажется таким простым, но я застреваю с округлением, а что нет, я чувствую себя полным неудачником;)

Есть идеи, как с этим справиться?

РЕДАКТИРОВАТЬ: какой-то код Python, чтобы показать мою путаницу

>>> amounts_paid = [100, 25, 30]
>>> total = sum(amounts_paid)
>>> correct_amount = total / float(len(amounts_paid))
>>> correct_amount
51.666666666666664
>>> diffs = [amnt-correct_amount for amnt in amounts_paid]
>>> diffs
[48.333333333333336, -26.666666666666664, -21.666666666666664]
>>> sum(diffs)
7.1054273576010019e-015

Теоретически сумма разностей должна быть равна нулю, верно?

для другого примера это работает :)

>>> amounts_paid = [100, 50, 150]
>>> total = sum(amounts_paid)
>>> correct_amount = total / float(len(amounts_paid))
>>> correct_amount
100.0
>>> diffs = [amnt-correct_amount for amnt in amounts_paid]
>>> diffs
[0.0, -50.0, 50.0]
>>> sum(diffs)
0.0

person gumuz    schedule 12.10.2010    source источник
comment
Проблема состоит из двух частей: 1) решить, сколько должен платить каждый человек, и 2) указать, сколько человек когда-либо должен денег (или должен получить, если он заплатил больше, чем его доля). Оба кажутся довольно простыми. Какая часть доставляет вам проблемы?   -  person Nikita Rybak    schedule 13.10.2010
comment
привет, спасибо, я думаю, что моя проблема заключается в том, что сумма не всегда делится поровну, поэтому придумывание алгоритма, который обрабатывает это элегантно, кажется, снова и снова сбивает меня с толку.   -  person gumuz    schedule 13.10.2010
comment
не всегда делятся поровну? В США это означает доли цента. Обычно шум округляется в ВВЕРХ, чтобы гарантировать покрытие всего счета. Насколько это проблема? Действительно?   -  person S.Lott    schedule 13.10.2010
comment
-1: Никогда. Делать. Финансы. С участием. float. Никогда.   -  person S.Lott    schedule 13.10.2010
comment
Я думаю, что все сводится к элегантной разработке алгоритма решения задачи. Если в конце числа не складываются правильно, как узнать, что это конец? Но опять же, возможно, я сейчас запутываю себя больше, чем мне нужно.   -  person gumuz    schedule 13.10.2010
comment
Если в конце числа не складываются правильно, как узнать, что это конец? Вы знаете, что это конец, потому что математика тривиальна. float округление не является частью алгоритма. Ваша ошибка 10 ** - 15. Вам придется разделить миллиарды приемов пищи, чтобы даже накопить ошибку 0,01 доллара. Начать сейчас. За несколько столетий, когда вы накопили ошибку в 0,01 доллара, вам есть о чем поговорить. А пока перестаньте кодировать и начните думать.   -  person S.Lott    schedule 13.10.2010
comment
Однажды я попытался написать веб-приложение, которое решало бы надмножество этой проблемы, и сдался из-за сложности ...   -  person rmeador    schedule 13.10.2010
comment
Дубликат всего этого: stackoverflow.com/questions/tagged/python+floating-point   -  person S.Lott    schedule 13.10.2010


Ответы (5)


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

Вы можете легко определить (по оригинальному счету), сколько каждый человек должен заплатить. Установите это как отрицательную сумму для каждого человека. Затем запишите сумму, которую каждый человек заплатил, добавив уплаченную сумму на свой счет. На этом этапе у людей, которые переплатили (кредиторы), будет положительный баланс, а у людей, которым недоплатили (заемщики), будет отрицательный баланс.

Не существует однозначного правильного ответа на вопрос, какой заемщик должен деньги каждому кредитору, за исключением очевидного случая, когда есть только один кредитор. Суммы, уплаченные заемщиком, могут перейти к любому из кредиторов. Просто добавьте сумму к общей сумме заемщика и вычтите суммы из суммы кредиторов, получающих платеж.

Когда все счета достигают нуля, все заплатили.

Изменить (в ответ на комментарии):

Я думаю, что моя проблема заключается в том, что сумма не всегда делится поровну, поэтому создание алгоритма, который элегантно справляется с этой задачей, меня снова и снова сбивает с толку.

При работе с долларами и центами не существует 100% чистого способа обработки округления. Некоторые люди будут платить на цент больше, чем другие. Единственный способ быть справедливым - назначить дополнительные 0,01 доллара наугад (по мере необходимости). Это может быть сделано только один раз, когда «сумма задолженности» рассчитывается путем разделения счета. Иногда помогает хранить денежные значения в центах, а не в долларах (например, 12,34 доллара будут сохранены как 1234). Это позволяет использовать целые числа вместо чисел с плавающей запятой.

Чтобы распределить лишние центы, я бы сделал следующее:

total_cents = 100 * total;
base_amount = Floor(total_cents / num_people);
cents_short = total_cents - base_amount * num_people;
while (cents_short > 0)
{
    // add one cent to a random person
    cents_short--;
}

Примечание: самый простой способ назначить монеты "случайным образом" - это назначить первый дополнительный цент первому человеку, второй - второму и т. д. Это становится проблемой только в том случае, если вы всегда вводите одни и те же люди в таком же порядке.

person e.James    schedule 12.10.2010
comment
Спасибо, в основном это то, к чему я стремился, но если вы увидите мой пример кода, который я добавил в сообщение, вы увидите, что он не всегда будет сводиться к нулю, если я не пойду совершенно неверным путем. - person gumuz; 13.10.2010
comment
С частичными долларами сложно. Я отредактировал свой ответ (надеюсь), чтобы решить эту проблему. - person e.James; 13.10.2010
comment
не существует 100% чистого способа обработки округления. Худший. @gumuz потратил на округление больше времени, чем стоят доли цента. Для @gumuz было бы дешевле просто пожертвовать округленные доли пенни и перестать думать. - person S.Lott; 13.10.2010
comment
@ S.Lott: Согласен. Стоящая дружба не может быть разрушена менее чем за 0,01 доллара :) - person e.James; 13.10.2010
comment
@ e.James. В этом случае. 10 ** - 13 копеек. - person S.Lott; 13.10.2010
comment
@ S.Lott Я думаю, что есть лучшие вещи, на которые можно потратить время, чем жаловаться на мой вопрос на онлайн-форуме. - person gumuz; 13.10.2010
comment
@ e.James, спасибо, что нашли время ответить на мой вопрос, каким бы глупым он ни был. - person gumuz; 13.10.2010
comment
@gumuz: Не беспокойся. Я написал некоторое финансовое программное обеспечение, поэтому я знаю боль центов и округления. - person e.James; 13.10.2010
comment
@gumuz: Я думаю, что есть лучшие вещи, на которые можно потратить время, чем задавать подобные вопросы на онлайн-форуме. Но это только мое мнение. Ваш может отличаться. - person S.Lott; 13.10.2010
comment
@gumuz: Тем не менее, С.Лотт делает вескую точку зрения. Лучший способ справиться с этим, вероятно, - просто попросить всех заплатить дополнительно 0,01 доллара, а затем добавить (минимальную) дополнительную сумму к чаевым. - person e.James; 13.10.2010
comment
@ S.Lott ну, это заняло у меня больше времени, чем вам хотелось бы, но я кое-чему научился. Кроме того, ваш комментарий о числах с плавающей запятой и десятичных дробях имел смысл и помог мне понять проблему, которую я (думал) имел, время потрачено не зря :) спасибо - person gumuz; 13.10.2010
comment
@ S.Lott Rounding UP - стандартное решение? Правила GAAP в лучшем случае неоднозначны, а в худшем - противоречивы в отношении округления. Спросите любого квалифицированного бухгалтера, и вы обнаружите, что правила округления в большую, меньшую или иную сторону довольно произвольны. В большинстве финансовых ситуаций вы можете округлить, используя любой метод, который вам нужен, если это не повлияет на существенность отчета. - person NealB; 13.10.2010
comment
@NealB: Думаю, мои бухгалтеры слишком жестко заявляют о своих требованиях. Я удалил комментарий. - person S.Lott; 13.10.2010
comment
Я не согласен с утверждением. Имея дело с долларами и центами, не существует 100% чистого способа обработки округления. Для этого решения вообще не требуется округление. Смотрите мой ответ, как / почему. знак равно - person Sir Robert; 15.10.2010
comment
@ Сэр Роберт: Ваше решение не предусматривает округления, потому что вы не разделяете счет. Это по-прежнему хорошее решение (и я проголосовал за него), но оно не устраняет необходимости округления, когда, например, три человека соглашаются поровну разделить счет на 100 долларов. Конечно, ваше решение все равно будет работать с разделенным счетом, ему просто нужно будет разделить и округлить, прежде чем заполнять суммы счета. - person e.James; 15.10.2010
comment
@ e.James: Оооо, я пропустил часть о том, как разделить его поровну! Ой! - person Sir Robert; 15.10.2010

http://www.billmonk.com/

Среди других. Проблема уже решена. Много раз.


«Теоретически сумма разностей должна быть равна нулю, верно?»

да. Однако, поскольку вы использовали float, у вас возникают проблемы с представлением, когда количество людей не является степенью двойки.

Никогда. Использовать. float Для. Финансы.

Никогда

Всегда. Использовать. decimal Для. Финансы.

Всегда

person S.Lott    schedule 12.10.2010
comment
Спасибо, знаю, тешу себя этой проблемой. Это кажется таким простым, но я не могу этого сделать ... это меня безмерно расстраивает :) - person gumuz; 13.10.2010
comment
Это не интересная проблема. Это сложение и вычитание. - person S.Lott; 13.10.2010
comment
Способ (не) ответить на вопрос. Так что, если @gumuz хочет решить проблему, которая решалась много раз? - person ktdrv; 13.10.2010
comment
@kaloyan: Но @gumuz действительно решил эту проблему. И использовал float и запутался ошибками округления. @gumuz не нуждался в решении. @gumuz нужно было прекратить использовать float. - person S.Lott; 13.10.2010
comment
больше нравится? Ответ не изменился. Что означает ваш комментарий? - person S.Lott; 13.10.2010
comment
@ S.Lott: +1 за ссылку BillMonk (довольно круто). -1 за зануду. Задача была достаточно интересной, чтобы gumuz нашел время, чтобы написать вопрос. Не следует отбрасывать его как универсально неинтересный. Бьюсь об заклад, он (и другие) кое-что извлек из ответов. - person Jon-Eric; 13.10.2010
comment
Если бы я мог дать вам два положительных голоса, я бы дал - один за billmonk, другой за ваш пункт о финансах и плавающих предложениях. - person Nick Johnson; 13.10.2010

Я не любитель питонов, но нашел интересную проблему =) Вот мое решение. Время разработки ~ 45мин. Я пишу чистый Perl ... должно быть легко портировать.

~/sandbox/$ ./bistro_math.pl 
Anna owes Bill 7.57
Anna owes Mike 2.16
John owes Mike 2.62

~/sandbox/$ cat bistro_math.pl 
#!/usr/bin/perl
use strict;
use warnings;

### Dataset.
###    Bill total:  50.00
###    Paid total:  50.00
my @people = (
  { name => 'Bill', bill =>  5.43, paid => 13.00 },
  { name => 'Suzy', bill => 12.00, paid => 12.00 },
  { name => 'John', bill => 10.62, paid =>  8.00 },
  { name => 'Mike', bill =>  9.22, paid => 14.00 },
  { name => 'Anna', bill => 12.73, paid =>  3.00 },
);

### Calculate how much each person owes (or is owed: -/+)
calculate_balances(\@people);

### Tally it all up =)  This algorithm is designed to have bigger lenders
### paid back by the fewest number of people possible (they have the least
### hassle, since they were the most generous!).
sub calculate_balances {
  my $people = shift;

  ### Use two pools    
  my @debtors;
  my @lenders;

  foreach my $person (@$people) {
    ### Ignore people who paid exactly what they owed.
    $person->{owes} = $person->{bill} - $person->{paid};

    push @debtors, $person if ($person->{owes} > 0);
    push @lenders, $person if ($person->{owes} < 0);
  }

  LENDERS: foreach my $lender (@lenders) {
    next if ($lender->{owes} >= 0);

    DEBTORS: foreach my $debtor (@debtors) {
      next if ($debtor->{owes} <= 0);

      my $payment = ($lender->{owes} + $debtor->{owes} < 0) 
        ? abs $debtor->{owes} 
        : abs $lender->{owes};

      $lender->{owes} += $payment;
      $debtor->{owes} -= $payment;

      $debtor->{pays} = [] if (not exists $debtor->{pays});
      print "$debtor->{name} owes $lender->{name} $payment\n";

      next LENDERS if ($lender->{owes} >= 0);
    }
  }
}

exit;
~/sandbox/$ 
person Sir Robert    schedule 14.10.2010

Вы знаете, сколько задолжали все и кому не хватило. Возьмите тот общий дефицит, который был у всех, и разделите его на тех, кто заплатил больше, начиная от самой высокой переплате до самой низкой.

person Derek    schedule 12.10.2010