Algorithm 确定组中最低付款额的算法 问题

Algorithm 确定组中最低付款额的算法 问题,algorithm,language-agnostic,Algorithm,Language Agnostic,最近,我被要求计算一起旅行的一群人所欠的钱,他们遇到了一个有趣的问题:既然你知道每个人欠另一个人的钱的数额,那么什么是合并人与人之间债务的通用算法,以便只需要支付最低数量的款项?以此为例: 迈克欠约翰100英镑 约翰欠雷切尔200英镑 迈克欠雷切尔400英镑 我们可以通过如下方式重新安排债务,免除Mike和John之间的付款: 迈克欠约翰0英镑 约翰欠雷切尔100英镑 迈克欠雷切尔500英镑 我手工计算,因为这很简单,但是我的程序员很想找出一个通用的算法来为任意大的群体计算。对我来说,这

最近,我被要求计算一起旅行的一群人所欠的钱,他们遇到了一个有趣的问题:既然你知道每个人欠另一个人的钱的数额,那么什么是合并人与人之间债务的通用算法,以便只需要支付最低数量的款项?以此为例:

  • 迈克欠约翰100英镑
  • 约翰欠雷切尔200英镑
  • 迈克欠雷切尔400英镑
我们可以通过如下方式重新安排债务,免除Mike和John之间的付款:

  • 迈克欠约翰0英镑
  • 约翰欠雷切尔100英镑
  • 迈克欠雷切尔500英镑
我手工计算,因为这很简单,但是我的程序员很想找出一个通用的算法来为任意大的群体计算。对我来说,这似乎是一个图形算法,所以我将把它重新表述为一个图形:

以图表的形式看
  • 顶点是组中的人
  • 根据欠款金额对边缘进行定向和加权。例如,迈克对瑞秋的优势是500磅,这意味着迈克欠瑞秋500磅
  • 约束:每个节点的权重净和必须保持不变
  • 目标是找到一个边数最少且仍满足约束的图

    • 我的意见是:你把这件事弄得太复杂了

      将其视为一个“资金池”,并完全失去关系:

      而不是:

      • 迈克欠约翰100英镑
      • 约翰欠雷切尔200英镑
      • 迈克欠雷切尔400英镑
      算法只需考虑:

      • 迈克欠100英镑
      • 约翰欠了100英镑
      • 约翰欠200英镑
      • 瑞秋欠了200英镑
      • 迈克欠400英镑
      • 瑞秋欠了400英镑
      扣除此项:

      • 迈克欠500英镑
      • 约翰欠100英镑
      • 瑞秋欠了600英镑
      将其分为“给予者”和“接受者”列表。名单上的每个送礼人都会仔细查看收礼人名单,给每个收礼人他们需要的东西,直到送礼人付清为止。当接收者收到他们所需要的一切时,他们就不在名单上了

      以后编辑


      正如其他海报所观察到的,这简化了问题。然而,“给予者”和“接受者”列表可能有一个最佳的顺序,但我们还没有确定一个简单的方法来确定这个顺序。

      仅仅找出接受者和给予者是不够的。虽然我认为这一策略是正确的,但它也不能确保找到尽可能少的支付金额的算法

      比如说,

      • A欠25英镑的人
      • B个人欠50英镑
      • C人欠75英镑
      • D人欠100英镑
      • 被拖欠50英镑
      很明显,这可以通过3种方式实现(A和C到D,B到E)。我想不出一个有效的算法能满足所有问题集的要求

      更好的例子

      • A欠10英镑的人
      • B个人欠49英镑
      • C人欠50英镑
      • D人欠65英镑
      • 被拖欠75英镑
      • F人欠他99英镑
      如果我们采取贪婪的方法,让D支付给F,我们将得到一个次优解,而不是最优解(a&D到E,B&C到F)


      这个问题和被证明是NP难的问题有很多相似之处。唯一的区别是我们有多个大小不同的箱子,并且所有箱子中的总空间等于所有物品的总大小。这让我相信这个问题很可能是NP难的,但加上附加的约束,它可能在多项式时间内解决。

      虽然我同意安德鲁的观点,将其转化为图形问题可能过于复杂,但我不确定他的方法是否能产生最小数量的事务。这是你在现实生活中如何解决问题以避免头痛;把钱集中起来

      看起来“正确”的几个步骤:

      • 免除所有零债务的个人;他们不需要发送或接收任何人的钱
      • 将所有给予者和接受者与相同的欠/欠金额配对。由于非零债务的每个节点的最小连通性为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钱,而不是直接支付所需的四笔交易,
      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