алгоритм определения минимальных выплат среди группы - PullRequest
11 голосов
/ 22 июля 2009

Проблема

Меня недавно попросили подсчитать деньги, причитающиеся группе людей, которые вместе отправились в путешествие и столкнулись с интересной проблемой: учитывая, что вы знаете суммы, которые каждый должен другому, каков общий алгоритм для долги между людьми, так что нужно сделать только минимальное количество платежей? Возьмите это как пример:

  • Майк должен Джону 100
  • Джон должен Рэйчел 200
  • Майк должен Рэйчел 400

Мы можем удалить платеж между Майком и Джоном, переформулировав долги следующим образом:

  • Майк должен Джону 0
  • Джон должен Рэйчел 100
  • Майк должен Рэйчел 500

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

Просмотр в виде графика

  • Вершинами являются люди в группе
  • Края направлены и взвешены на сумму задолженности. Например, переход от Майка к Рейчел с весом 500 означает, что Майк должен Рейчел 500.
  • Ограничение: чистая сумма весов для каждого узла должна оставаться неизменной.
  • Цель состоит в том, чтобы найти граф с минимальным количеством ребер, которые все еще удовлетворяют ограничению.

Ответы [ 8 ]

15 голосов
/ 22 июля 2009

Мое мнение: Вы делаете это слишком сложным.

Думайте об этом как о "пуле" денег и вообще теряйте отношения:

Вместо:

  • Майк должен Джону 100
  • Джон должен Рэйчел 200
  • Майк должен Рэйчел 400

Алгоритм должен думать только:

  • Майк должен 100
  • Джон должен 100
  • Джон должен 200
  • Рэйчел задолжала 200
  • Майк должен 400
  • Рэйчел задолжала 400

Неттинг это:

  • Майк должен 500
  • Джон должен 100
  • Рэйчел задолжала 600

Разделите это на список "дающих" и "получателей". Каждый дающий в списке просматривает список получателей, давая каждому получателю то, что ему нужно, до тех пор, пока даритель не заплатит. Когда получатель получает все, что ему нужно, он исчезает из списка.

Позже Редактировать

Как заметили другие авторы, это упрощает проблему. Однако может быть оптимальное упорядочение списков «дающих» и «получателей», но мы пока не определили простой способ определить это упорядочение.

8 голосов
/ 22 июля 2009

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

Например,

  • Человек А должен 25
  • Человек B должен 50
  • Человек С должен 75
  • Человек D должен 100
  • Человек E должен 50

Хотя очевидно, что это можно сделать за 3 платежа (от A и C до D, от B до E). Я не могу придумать эффективный алгоритм, который удовлетворит это для всех проблемных наборов.

Лучший пример,

  • Человек А должен 10
  • Человек B должен 49
  • Лицо С должно 50
  • Человек D должен 65
  • Человек E должен 75
  • Человек F должен 99

Если бы мы использовали жадный подход, заключающийся в том, чтобы человек D платил F, мы бы получили неоптимальное решение, а не оптимальное (A & D - E, B & C - F).

Эта проблема во многом схожа с Проблема с упаковкой в ​​бункер , доказавшая, что она NP-сложная. Единственное отличие состоит в том, что у нас есть несколько корзин разного размера и условие, что общее пространство во всех корзинах равно общему размеру всех элементов. Это наводит меня на мысль, что проблема, скорее всего, NP-сложная, но с добавленными ограничениями ее можно решить за полиномиальное время.

5 голосов
/ 22 июля 2009

Взгляните на эту статью блога " Оптимальное балансирование аккаунта ", которая точно решит вашу проблему.

4 голосов
/ 27 декабря 2011

В мире корпоративного казначейства это известно как платеж или расчетный неттинг .

Многонациональные корпорации обычно имеют множество потоков между своими дочерними предприятиями каждый месяц, часто в разных валютах. Они могут сэкономить значительные суммы путем оптимизации урегулирования этих потоков. Как правило, компания будет проводить такую ​​оптимизацию ( цикл неттинга ) один раз в месяц. При наличии нескольких валют существует три источника экономии:

  • комиссия за банковские операции (меньшее количество платежей означает меньшую комиссию)
  • снижение процентных ставок и расчетный риск, возникающий в результате упорядоченной обработки банком (деньги тратят меньше времени на привязку к банковской системе)
  • Подбор валют (значительно меньше иностранной валюты обменивается, потому что большая часть ее «зачтена»)

Существует два способа расчета оптимизированного расчета.

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

Многосторонний неттинг решает сеть, добавляя новую дочернюю компанию, которая называется центром неттинга, и перенаправляет все суммы через нее. Сравните диаграммы до и после:

до взаимозачета

Before netting

После взаимозачета

After netting

Хотя это добавляет еще один поток, чем необходимо (по сравнению с двусторонней сеткой), преимущества:

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

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

4 голосов
/ 22 июля 2009

Хотя я согласен с @ Andrew , что превращение этого в проблему с графиком, вероятно, слишком сложно, я не уверен, что его подход дает минимальное количество транзакций. Это то, как вы решили бы проблему в реальной жизни, чтобы избавить себя от головной боли; просто объедините деньги.

Несколько шагов, которые кажутся «правильными»:

  • Удалить всех лиц с нулевым долгом; им не нужно ни отправлять, ни получать деньги от кого-либо.
  • Соедините всех дающих и получателей с одинаковыми суммами задолженности. Поскольку минимальное количество соединений на узел ненулевого долга равно 1, их транзакции уже минимальны, если они просто платят друг другу. Уберите их с графика.
  • Начиная с лица, имеющего наибольшую сумму для погашения, создайте список всех получателей, задолжавших меньше этой суммы. Попробуйте все комбинации оплаты, пока не будет найден один, который удовлетворяет большинство получателей одной транзакцией. «Сохранить» оставшуюся избыточную задолженность.
  • Переходите к следующему по величине дающему и т. Д.
  • Распределите всю избыточную задолженность оставшимся получателям.

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

1 голос
/ 22 июля 2009

если A, B и C каждый должен по 1 $ каждому из D, E и F, решение "список" или "центральный банк" создает пять транзакций (например, A, B, C - $ 3-> D, D - $ 3 -> E, F) тогда как наивное решение приводит к девяти транзакциям. Однако, если A должен только D, B только E и C только F, решение центрального банка создает еще пять транзакций (A, B, C - $ 1-> D, D - $ 1-> E, F), тогда как лучший решение требует только три (A - $ 1-> D, B - $ 1-> E, C - $ 1-> F). Это показывает, что решение "список" или "центральный банк" в целом не является оптимальным.

Следующий жадный алгоритм может быть использован для создания лучших решений проблемы, но они не всегда оптимальны. Пусть «задолженность [i, j]» обозначает сумму денег на человека, которого я должен человеку j; изначально этот массив инициализируется в соответствии с ситуацией.

repeat until last:
  find any (i, j) such that |K = {k : debt[i,k] > 0 and debt[j,k] > 0}| >= 2
  if such (i, j) found:
     // transfer the loans from i to j
     total = 0
     for all k in K:
       debt[j,k] += debt[i,k]
       total += debt[i,k]
       debt[i,k] = 0 
     person i pays 'total' to person j
  else:
     last

for every i, j in N:
  if (debt[i,j] > 0)
    person i pays debt[i,j] to person j

Ключом к этому алгоритму является наблюдение, что если и А, и В должны деньги как С, так и С, вместо четырех транзакций, необходимых для прямых платежей, В может выплатить чистый долг А, который может позаботиться о выплате кредиты как А, так и Б.

Чтобы увидеть, как работает этот алгоритм, рассмотрим случай, когда каждый из A, B и C имеет $ 1 для каждого из D, E, F:

  1. A переводит долги A в B и платит 3 $ B (одна транзакция)
  2. B переводит долги B в C и платит C $ 6 (одна транзакция)
  3. Только у C больше нет долгов; C платит по 3 доллара США D, E и F каждая (три транзакции)

Но в случае, когда A должен D, B должен E, а C должен F, алгоритм сразу попадает в цикл платежей, что приводит к оптимальному количеству транзакций (только три) вместо пяти транзакций, которые могут возникнуть в результате Подход "центрального банка".

Примером неоптимальности является случай, когда A должен D и E, B должен E и F, а C должен F и D (предположим, 1 доллар за каждый долг). Алгоритм не в состоянии консолидировать кредиты, потому что нет двух плательщиков, имеющих двух общих получателей. Это можно исправить, изменив «> = 2» во второй строке на «> = 1», но тогда алгоритм, скорее всего, станет очень чувствительным к порядку, в котором долги будут обеспечены.

0 голосов
/ 06 сентября 2018

Итак, я реализовал это для электронной таблицы, чтобы отслеживать долги моих соседей по комнате друг перед другом. Я знаю, что это действительно старая версия, но я сослался на нее в своем решении, и она высоко ценится в Google при поиске этой темы, поэтому я хотел опубликовать и посмотреть, есть ли у кого-нибудь какие-либо комментарии.

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

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

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

Input: {(Person, Net Profit)} //Net Profit < 0 is debt, Net Profit > 0 is credit.
Output: {(Payer, Payee, Amount paid)}

find_payments(input_list):
    if input_list.length() > 2: 
        //More than two people to resolve payments between, the non-trivial case

        max_change = input_list[0]
        not_max_change = []

        //Find person who has the highest change to their account, and
        //the list of all others involved who are not that person

        for tuple in input_list:
            if abs(tuple[Net Profit]) > abs(max_change[Net Profit])
                not_max_change.push(max_change)
                max_change = tuple
            else:
                not_max_change.push(tuple)

        //If the highest change person is owed money, they are paid by the 
        //person who owes the most money, and the money they are paid is deducted 
        //from the money they are still owed. 

        //If the highest change person owes money, they pay the person who 
        //is owed the most money, and the money they pay is deducted 
        //from the money they are still owe. 


        not_yet_resolved = []

        if max_change[Net Profit] > 0: 
            //The person with the highest change is OWED money 

            max_owing = not_max_change[0]

            //Find the remaining person who OWES the most money
            //Find the remaining person who has the LOWEST Net Profit
            for tuple in not_max_change:
                if tuple[Net Paid] < max_owing[Net Paid]:
                    not_yet_resolved.push(max_owing)
                    max_owing = tuple
                else:
                    not_yet_resolved.push(tuple)

            //The person who has the max change which is positive is paid
            //by the person who owes the most, reducing the amount of money
            //they are owed. Note max_owing[Net Profit] < 0.

            max_change = [max_change[Person], max_change[Net Profit]+max_owing[Net Profit]]

            //Max_owing[Person] has paid max_owing[Net Profit] to max_change[Person]
            //max_owing = [max_owing[Person], max_owing[Net Profit]-max_owing[Net Profit]]
            //max_owing = [max_owing[Person], 0]
            //This person is fully paid up.

            if max_change[Net Profit] != 0:
                not_yet_resolved.push(max_owing)

            //We have eliminated at least 1 involved individual (max_owing[Person])
            //because they have paid all they owe. This truth shows us 
            //the recursion will eventually end.
            return [[max_owing[Person], max_change[Person], max_owing[Net Profit]]].concat(find_payments(not_yet_resolved))

        if max_change[Net Profit] < 0:
            //The person with the highest change OWES money 
            //I'll be way less verbose here

            max_owed = not_max_change[0]

            //Find who is owed the most money
            for tuple in not_max_change:
                if tuple[Net Paid] > max_owed[Net Paid]:
                    not_yet_resolved.push(max_owed)
                    max_owed = tuple
                else:
                    not_yet_resolved.push(tuple)

            //max_change pays the person who is owed the most. 
            max_change = [max_change[Person], max_change[Net Profit]+max_owed[Net Profit]]                

            if max_change[Net Profit] != 0:
                not_yet_resolved.push(max_owing)

            //Note position of max_change[Person] moved from payee to payer
            return [[max_change[Person], max_owed[Person], max_owed[Net Profit]]].concat(find_payments(not_yet_resolved))

    //Recursive base case
    //Two people owe each other some money, the person who owes pays
    //the person who is owed. Trivial. 
    if input_list.length() == 2:
        if input_list[0][Net Profit] > input_list[1][Net Profit]:
            return [[input_list[1][Person], input_list[0][Person], input_list[0][Net Profit]]];
        else
            return [[input_list[0][Person], input_list[1][Person], input_list[1][Net Profit]]];

Примечание:

max_change = (payee, $A); max_owing = (payer, $B) 
|$A|>=|$B| by nature of 'max_change' 
$A > 0 => $A >= |$B| //max_change is owed money
$B < 0 by nature of 'max_owing'
$A >= -$B => $A + $B >= 0 => Payee does not go into debt

и

max_change = (payee, $A); max_owed = (payer, $B) 
|$A|>=|$B| by nature of 'max_change' 
$A < 0 => -$A >= |$B| //max_change owes money
$B > 0 by nature of 'max_owed'
-$A >= $B => 0 >= $A + $B => Payee does not go into credit

также:

Sum of Payments = 0 = $A + $B + Remainder = ($A + $B) + 0 + Remainder

Сумма, всегда равная 0 после полного погашения чьего-либо долга, является основой логики рекурсии. Кто-то заплатил / заплатил, проблема становится меньше.

Если этот алгоритм работает для n людей с ненулевыми долгами (откажитесь от людей, которые обанкротились еще до запуска алгоритма), этот алгоритм даст не более n-1 платежей для погашения задолженности. Неясно, всегда ли это идеальная схема оплаты (я пока не нашел встречного примера). Я могу попытаться доказать, что если количество транзакций

Я чрезвычайно заинтересован в любых ошибках, которые кто-либо видит в этом. Я давно не занимался разработкой, не говоря уже о алгоритмах, и люди будут платить друг другу за это. Мне было весело, и это интересный, мясистый вопрос, надеюсь, некоторые из вас все еще рядом.

0 голосов
/ 16 февраля 2018

Как сказал один @Edd Barret, это можно решить приблизительно с помощью линейного программирования. Я написал сообщение в блоге , описывающее этот подход, а также небольшой пакет R, который его реализует.

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