用于确定组中最小支付的算法

时间:2009-07-22 04:48:05

标签: algorithm language-agnostic

问题

我最近被要求计算一群人一起旅行时所欠的钱,并且遇到了一个有趣的问题:假设你知道每个人欠另一个人的金额,那么巩固这个问题的一般算法是什么?人与人之间的债务,以便只需要支付最低数额的款项?以此为例:

  • Mike欠John 100
  • John欠Rachel 200
  • Mike欠Rachel 400

我们可以通过重新制定这样的债务来取消迈克和约翰之间的付款:

  • Mike欠John 0
  • John欠Rachel 100
  • Mike欠Rachel 500

我手工完成了数学运算,因为它很容易,但是我的程序员很想找出一个通用的算法来为一个任意大的组做这件事。这对我来说似乎是一个图形算法,所以我将其重新表示为图形:

以图表形式查看

  • 顶点是群组中的人
  • 根据所欠的金额指示和加权边缘。例如,从Mike到Rachel的重量为500的边缘意味着Mike欠Rachel 500。
  • 约束:每个节点的净重和必须保持不变。
  • 目标是找到一个具有仍然满足约束条件的最小边数的图形。

8 个答案:

答案 0 :(得分:15)

我的意见:你让这个过于复杂。

将其视为金钱的“池”,并完全失去关系:

而不是:

  • Mike欠John 100
  • John欠Rachel 200
  • Mike欠Rachel 400

算法只需要思考:

  • Mike欠100
  • 约翰欠100
  • John欠200
  • Rachel欠200
  • Mike欠400
  • Rachel欠400

净化这个:

  • Mike欠500
  • 约翰欠100
  • Rachel欠600

将其分为“给予者”和“接收者”列表。列表中的每个给予者将通过接收者列表,为每个接收者提供他们需要的东西,直到给予者为止。当接收者收到他们需要的所有内容时,他们会从列表中删除。

稍后修改

正如其他海报所观察到的,这简化了问题。但是,可能存在“给予者”和“接收者”列表的最佳排序,但我们还没有确定一种直接的方法来确定这种排序。

答案 1 :(得分:8)

仅仅弄清楚接收者和提供者是不够的。虽然我认为这种策略是在正确的轨道上,但它也无法确保算法能够找到尽可能少的付款。

例如,

  • A人欠25
  • B人欠50
  • 人C欠75
  • 人D欠100
  • 人E欠50

虽然很明显这可以用3次付费(A和C到D,B到E)来完成。我想不出一个能够满足所有问题集的高效算法。

更好的例子,

  • 人A欠10
  • B人欠49
  • 人C欠50
  • 人D欠65
  • 人E欠75
  • 人F欠99

如果我们采用让人D付给F的贪婪方法,我们将使用次优解决方案而不是最优解(A& D到E,B& C到F)。

这个问题与已被证明是NP难的Bin Packing Problem有很多相似之处。唯一的区别是我们有多个不同大小的箱子以及所有箱子中的总空间等于所有物品的总大小的条件。这让我相信这个问题很可能是NP难的,但是在附加约束的情况下,可以用多项式时间来解决。

答案 2 :(得分:5)

请看一下这篇博客文章“Optimal Account Balancing”,完全解决您的问题。

答案 3 :(得分:4)

虽然我同意 @Andrew ,但将其转化为图形问题可能过于复杂,我不确定他的方法是否会产生最少数量的事务。这就是你如何在现实生活中解决问题以避免头痛;只是汇集资金。

看似“正确”的几个步骤:

  • 删除所有欠债的人;他们不需要向任何人发送或接收资金。
  • 将所有赠送者和接收者配对相同的欠款/欠款。由于非零债务的每个节点的最小连接性为1,如果它们只是相互支付,它们的交易已经很少。从图表中删除它们。
  • 从支付金额最大的个人开始,创建一个欠款少于该金额的所有收款人列表。尝试所有付款组合,直到找到满足一次交易的大多数收款人的付款。 “节省”剩余的剩余债务。
  • 转移到下一个最大的提供者等
  • 将所有剩余债务分配给剩余的接收方。

一如既往,我担心我对前两个步骤非常肯定,对其他步骤不太确定。无论如何,它听起来像教科书问题;我确信那里有一个“正确”的答案。

答案 4 :(得分:4)

在公司资金管理的世界中,这被称为付款结算网

跨国公司通常每月在子公司之间流动很多,通常采用不同的货币。通过优化这些流量的结算,它们可以节省大量资金。通常,公司将每月执行一次这样的优化(净结算周期)。当存在多种货币时,有三种节约来源:

  • 银行交易费用(更少的付款意味着更低的费用)
  • 银行精简处理产生的利率浮动和结算风险降低(资金花在银行系统上的时间减少)
  • 外汇匹配(因为大部分外币被'净额结算',所以外汇交易要少得多)

实际计算优化结算有两种方法。

双边网络是@AndrewShepherd在此页面上很好描述的解决方案。但是,在跨境实施中,这种方法可能会产生法律和行政问题,因为每个月都会跨越不同的边界。

多边网络通过添加一个名为净额结算中心的新子公司来解决网络,并通过它重新路由所有金额。比较下面的前后图表:

扣网前

Before netting

净结算后

After netting

虽然这增加了一个必要的流量(与双边网相比),但优点是:

  • 计算更简单,结果更容易可视化(同样,只有一种解决方案,而不是双边方法)
  • 净结算中心成为整个集团内流量和外汇风险的宝贵资源
  • 如果净结算中心在德国,那么跨境支付的所有法律问题都会在净结算中心的国家内进行一次性处理(中央银行报告等)。
  • 优化结算所需的所有外汇可以从净额结算中心购买或出售

(在基本层面,计算很简单,但可能存在许多法律和行政方面的复杂情况,因此企业经常从软件供应商或服务提供商开发或购买 netting system 。)

答案 5 :(得分:1)

如果A,B和C各自欠D,E和F各1美元,则“清单”或“中央银行”解决方案会产生五笔交易(例如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和D的钱,而不是直接支付所需的四笔交易,B可以将净债务支付给可以负责偿还的A A和B的贷款。

要了解此算法的工作原理,请考虑A,B和C各自对D,E,F各自拥有$ 1的情况:

  1. A将A的债务转移到B,并向B支付3美元(一笔交易)
  2. B将B的债务转移到C,并支付6美元到C(一笔交易)
  3. 只有C再有债务; C每次向D,E和F支付3美元(三笔交易)
  4. 但是在A欠D,B欠E和C欠F的情况下,算法立即进入支付循环,导致最佳交易数量(仅三个),而不是由此产生的五个交易“央行”方法。

    非最优性的一个例子是A欠D和E,B欠E和F,C欠F和D(假设每笔债务1美元)。该算法未能合并贷款,因为没有两个付款人共享两个共同的收款人。这可以通过将第二行上的“> = 2”更改为“> = 1”来修复,但随后算法很可能对债务抵押的顺序非常敏感。

答案 6 :(得分:0)

正如@Edd Barret所说,这可以使用线性编程近似解决。  I have written a blog post描述了这种方法,以及一个实现它的小R包。

答案 7 :(得分:0)

因此,我为电子表格实现了此功能,以跟踪室友彼此之间的债务。我知道这确实很老,但是我在解决方案中引用了它,在搜索主题时它在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次付款来清偿债务。尚不清楚它是否始终是理想的付款方式(我还没有找到反例)。我可能会尝试证明如果交易数

我对任何人看到的任何错误都非常感兴趣。我有一段时间没有做过开发了,没关系算法,人们将因此而付出彼此。我很开心,这是一个有趣的,多肉的问题,我希望你们中的一些人仍然在身边。