在群组之间共享/结算费用的算法

时间:2009-06-10 11:05:01

标签: algorithm

我期待着针对以下问题的算法。

问题:会有一群人欠他们一些钱或没有钱。现在,我需要一个算法(最好和最好的)来解决这个群体的费用。

Person AmtSpent
------ ---------
A       400  
B      1000  
C       100  
Total  1500

现在,每人的费用是1500/3 = 500.意思是B给A 100.给B 400.我知道,我可以用最少的金额开始工作。

如果有的话,有人可以指出我最好的那个。

提前致谢。

总结一下, 1.查找总费用和人均费用 2.找出欠款或欠款的金额(-ve表示未付款) 3.从最低+ ve量开始。将其分配到-ve金额 4.继续重复步骤3,直到用完量为止 秒。转到下一个更大的+ ve号码。继续重复3& 4直到有+ ve号码。

还是有更好的方法吗?我只是好奇。 :)

10 个答案:

答案 0 :(得分:10)

此问题涵盖了回归零状态(最小交易次数)的最佳方式here

答案 1 :(得分:4)

你已经描述过了。汇总所有费用(在您的情况下为1500),除以共享费用的人数(500)。对于每个人,扣除该人从个人分享中做出的贡献(对于人A,从500减去400)。结果是那个人“欠”中央游泳池的网。如果任何人的数字为负,则中央池“欠”该人。

因为你已经描述过这个解决方案,所以我不知道你在问什么。 也许你正试图在没有中央游泳池“银行”的情况下解决问题?

我也不知道你的意思是“以最少花费的金额开始工作。”

答案 2 :(得分:4)

我创建了一个解决此问题的Android应用。您可以在旅行期间输入费用,甚至建议您“谁应该下次付款”。最后计算“谁应该向谁发送多少”。我的算法计算所需的最小交易数量,你可以设置“交易容忍度”,这可以进一步减少交易(你不关心1美元的交易)尝试一下,它叫做定居:

https://market.android.com/details?id=cz.destil.settleup

我的算法说明:

我有基本算法解决了n-1事务的问题,但它不是最优的。它的工作原理如下:从付款开始,我为每个成员计算余额。平衡就是他付出的代价,减去他应该付出的代价。我越来越平衡地对成员进行排序。然后我总是把最贫穷和最富有的人交易完成。其中至少有一个以零余额结束,并被排除在进一步计算之外。有了这个,交易数量不能比n-1差。它还最大限度地减少了交易中的金额。但它并不是最优的,因为它不会检测到可以在内部稳定的子组。

找到可以在内部解决的子群很难。我通过生成所有成员组合并检查子组中的余额总和是否等于零来解决它。我从2对开始,然后是3对......(n-1)对。可以使用组合生成器的实现。当我找到一个子组时,我使用上面描述的基本算法计算子组中的事务。对于每个找到的子组,都可以节省一笔交易。

解决方案是最佳的,但复杂性增加到O(n!)。这看起来很糟糕,但诀窍是现实中只有少数成员。我在Nexus One(1 Ghz处理器)上进行了测试,结果是:直到10名成员:< 100 ms,15名成员:1 s,18名成员:8 s,20名成员:55 s。因此,直到18名成员执行时间很好。 > 15个成员的解决方法可以仅使用基本算法(它快速且正确,但不是最佳)。

源代码:

源代码在报告中提供有关用捷克语编写的算法。源代码在最后,它是英文:

http://www.settleup.info/files/master-thesis-david-vavra.pdf

答案 3 :(得分:1)

这个想法(类似于被要求但有一点扭曲/使用一些分类帐概念)是使用池帐户,对于每个帐单,成员要么支付到池,要么从池中获取。 例如 在下面的附图中,Costco费用由P先生支付,Pool需要93.76美元,其他会员支付46.88美元到游泳池。

enter image description here

答案 4 :(得分:1)

我最近写了a blog post,描述了解决集团成员之间费用结算的方法,可能每个人都欠其他人,这样解决债务所需的付款数量是最不可能的。它使用线性编程公式。我还展示了一个使用实现解决方案的小型R包的示例。

答案 5 :(得分:1)

我和朋友旅行后不得不这样做,这是一个python3版本:

import numpy as np
import pandas as pd

# setup inputs
people = ["Athos", "Porthos", "Aramis"]   # friends names
totals = [300, 150, 90]  # total spent per friend

# compute matrix
total_spent = np.array(totals).reshape(-1,1)
share = total_spent / len(totals)
mat = (share.T - share).clip(min=0) 

# create a readable dataframe
column_labels = [f"to_{person}" for person in people] 
index_labels = [f"{person}_owes" for person in people]
df = pd.DataFrame(data=mat, columns=column_labels, index=index_labels)
df.round(2)

返回此数据帧:

<头>
to_Athos to_Porthos to_Aramis
Athos_owes 0 0 0
Porthos_owes 50 0 0
Aramis_owes 70 20 0

像这样读:“Porthos 欠 Athos 50 美元”......

这不是优化版本,这是简单版本,但它是简单的代码,可以在许多情况下工作。

答案 6 :(得分:0)

直截了当,正如你在文中所做的那样:

返回原始数组中每个人支付的费用。 Negativ的价值观:这个人得到了一些回报

将你欠下的任何东西都交给下一个然后辍学。如果你得到一些,只需等待第二轮。完成后,反转整个事情。在这两轮之后,每个人都支付了相同的金额。

procedure SettleDepth(Expenses: array of double);
var
  i: Integer;
  s: double;
begin
  //Sum all amounts and divide by number of people
  // O(n) 
  s := 0.0;
  for i := Low(Expenses) to High(Expenses) do
     s := s + Expenses[i];

  s := s / (High(Expenses) - Low(Expenses));

  // Inplace Change to owed amount
  // and hand on what you owe
  // drop out if your even 
  for i := High(Expenses) downto Low(Expenses)+1 do begin
     Expenses[i] := s - Expenses[i];
     if (Expenses[i] > 0) then begin
        Expenses[i-1] := Expenses[i-1] + Expenses[i];
        Expenses.Delete(i);
     end else if (Expenses[i] = 0) then begin
        Expenses.Delete(i);
     end;
  end;

  Expenses[Low(Expenses)] := s - Expenses[Low(Expenses)];
  if (Expenses[Low(Expenses)] = 0) then begin
     Expenses.Delete(Low(Expenses));
  end;

  // hand on what you owe
  for i := Low(Expenses) to High(Expenses)-1 do begin
     if (Expenses[i] > 0) then begin
        Expenses[i+1] := Expenses[i+1] + Expenses[i];
     end;
  end;
end;  

答案 7 :(得分:0)

已接受算法​​的Javascript解决方案:

const payments = {
  John: 400,
  Jane: 1000,
  Bob: 100,
  Dave: 900,
};

function splitPayments(payments) {
  const people = Object.keys(payments);
  const valuesPaid = Object.values(payments);

  const sum = valuesPaid.reduce((acc, curr) => curr + acc);
  const mean = sum / people.length;

  const sortedPeople = people.sort((personA, personB) => payments[personA] - payments[personB]);
  const sortedValuesPaid = sortedPeople.map((person) => payments[person] - mean);

  let i = 0;
  let j = sortedPeople.length - 1;
  let debt;

  while (i < j) {
    debt = Math.min(-(sortedValuesPaid[i]), sortedValuesPaid[j]);
    sortedValuesPaid[i] += debt;
    sortedValuesPaid[j] -= debt;

    console.log(`${sortedPeople[i]} owes ${sortedPeople[j]} $${debt}`);

    if (sortedValuesPaid[i] === 0) {
      i++;
    }

    if (sortedValuesPaid[j] === 0) {
      j--;
    }
  }
}

splitPayments(payments);

/*
  C owes B $400
  C owes D $100
  A owes D $200
*/

答案 8 :(得分:0)

如果您不介意的话,我想从UX的角度提出更改核心参数的建议。

无论其服务或产品是否在一组中消费,有时这些东西都可以共享。例如,开胃菜或会议中的私人/半私人会议。

对于开胃聚会托盘之类的事物,这意味着每个人都可以访问,但不一定每个人都可以访问。如果说每个人都要分摊费用,那么在分摊账单时只有30%的人参与竞争。其他人群可能根本不在乎。因此,从算法的角度来看,您需要首先决定使用这三个选择中的哪个(可能是按费用计算):

通用拆分
由参加者平分
按参与人的比例划分

我个人更喜欢第二种,因为它具有处理全部费用所有权的功能,该所有权仅用于一个人,某些人以及整个小组。它还通过一概而论地解决了比例差异的伦理问题,如果您分手,则不管实际个人拥有多少,都将平均分配。作为一种社会元素,我认为有人只是尝试了一些“小样本”,然后决定不再有理由将该人从分配费用的人群中剔除。

所以small-sampling != partaking;)

然后,您将承担所有费用并遍历谁参与其中的工作,并原子地处理每个项目,最后提供每个人的总费用。

因此,最后,您列出了支出清单,并与每个人进行了遍历。在单笔费用检查结束时,您请分担费用的人员将这笔费用平均分配给每个人,并更新每个人当前的帐单分配。

请原谅伪代码:

list_of_expenses[] = getExpenseList()
list_of_agents_to_charge[] = getParticipantList()

for each expense in list_of_expenses
    list_of_partakers[] = getPartakerList(expense)
    for each partaker in list_of_partakers
       addChargeToAgent(expense.price / list_of_partakers.size, list_of_agents_to_charge[partaker])

然后仅遍历您的list_of_agents_to_charge[]并将每个总数报告给每个业务代表。

您可以通过简单地将小费视为额外费用来增加对小费的支持。

对不起,OP,我为您全力以赴。

ps:我有点想现在编写一个移动应用程序来执行此操作xD这就是我在离开工作之前检查SO的结果...

答案 9 :(得分:0)

显然有更好的方法可以做到这一点。但这需要运行NP时间复杂度算法,该算法才能真正显示您的应用程序。无论如何,这就是我使用优先级队列在Java中为android application实现解决方案的方式:

class calculateTransactions {
public static void calculateBalances(debtors,creditors) {
// add members who are owed money to debtors priority queue
// add members who owe money to others to creditors priority queue
}

public static void calculateTransactions() {
    results.clear(); // remove previously calculated transactions before calculating again
    PriorityQueue<Balance> debtors = new PriorityQueue<>(members.size(),new BalanceComparator()); // debtors are members of the group who are owed money, balance comparator defined for max priority queue
    PriorityQueue<Balance> creditors = new PriorityQueue<>(members.size(),new BalanceComparator()); // creditors are members who have to pay money to the group

    calculateBalances(debtors,creditors);

    /*Algorithm: Pick the largest element from debtors and the largest from creditors. Ex: If debtors = {4,3} and creditors={2,7}, pick 4 as the largest debtor and 7 as the largest creditor.
    * Now, do a transaction between them. The debtor with a balance of 4 receives $4 from the creditor with a balance of 7 and hence, the debtor is eliminated from further
    * transactions. Repeat the same thing until and unless there are no creditors and debtors.
    *
    * The priority queues help us find the largest creditor and debtor in constant time. However, adding/removing a member takes O(log n) time to perform it.
    * Optimisation: This algorithm produces correct results but the no of transactions is not minimum. To minimize it, we could use the subset sum algorithm which is a NP problem.
    * The use of a NP solution could really slow down the app! */
    while(!creditors.isEmpty() && !debtors.isEmpty()) {
        Balance rich = creditors.peek(); // get the largest creditor
        Balance poor = debtors.peek(); // get the largest debtor
        if(rich == null || poor == null) {
            return;
        }
        String richName = rich.name;
        BigDecimal richBalance = rich.balance;
        creditors.remove(rich); // remove the creditor from the queue

        String poorName = poor.name;
        BigDecimal poorBalance = poor.balance;
        debtors.remove(poor); // remove the debtor from the queue

        BigDecimal min = richBalance.min(poorBalance);

        // calculate the amount to be send from creditor to debtor
        richBalance = richBalance.subtract(min);
        poorBalance = poorBalance.subtract(min);

        HashMap<String,Object> values = new HashMap<>(); // record the transaction details in a HashMap
        values.put("sender",richName);
        values.put("recipient",poorName);
        values.put("amount",currency.charAt(5) + min.toString());

        results.add(values);

        // Consider a member as settled if he has an outstanding balance between 0.00 and 0.49 else add him to the queue again
        int compare = 1;
        if(poorBalance.compareTo(new BigDecimal("0.49")) == compare) {
            // if the debtor is not yet settled(has a balance between 0.49 and inf) add him to the priority queue again so that he is available for further transactions to settle up his debts
            debtors.add(new Balance(poorBalance,poorName));
        }

        if(richBalance.compareTo(new BigDecimal("0.49")) == compare) {
            // if the creditor is not yet settled(has a balance between 0.49 and inf) add him to the priority queue again so that he is available for further transactions
            creditors.add(new Balance(richBalance,richName));
        }
    }
}
}