What algorithm to use to determine minimum number of actions required to get the system to “Zero” state?

前端 未结 10 514
隐瞒了意图╮
隐瞒了意图╮ 2020-11-30 18:25

This is kind of more generic question, isn\'t language-specific. More about idea and algorithm to use.

The system is as follows:

It registers small loans bet

相关标签:
10条回答
  • 2020-11-30 19:01

    I have created an Android app which solves this problem. You can input expenses during the trip, it even recommends you "who should pay next". At the end it calculates "who should send how much to whom". My algorithm calculates minimum required number of transactions and you can setup "transaction tolerance" which can reduce transactions even further (you don't care about $1 transactions) Try it out, it's called Settle Up:

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

    Description of my algorithm:

    I have basic algorithm which solves the problem with n-1 transactions, but it's not optimal. It works like this: From payments, I compute balance for each member. Balance is what he paid minus what he should pay. I sort members according to balance increasingly. Then I always take the poorest and richest and transaction is made. At least one of them ends up with zero balance and is excluded from further calculations. With this, number of transactions cannot be worse than n-1. It also minimizes amount of money in transactions. But it's not optimal, because it doesn't detect subgroups which can settle up internally.

    Finding subgroups which can settle up internally is hard. I solve it by generating all combinations of members and checking if sum of balances in subgroup equals zero. I start with 2-pairs, then 3-pairs ... (n-1)pairs. Implementations of combination generators are available. When I find a subgroup, I calculate transactions in the subgroup using basic algorithm described above. For every found subgroup, one transaction is spared.

    The solution is optimal, but complexity increases to O(n!). This looks terrible but the trick is there will be just small number of members in reality. I have tested it on Nexus One (1 Ghz procesor) and the results are: until 10 members: <100 ms, 15 members: 1 s, 18 members: 8 s, 20 members: 55 s. So until 18 members the execution time is fine. Workaround for >15 members can be to use just the basic algorithm (it's fast and correct, but not optimal).

    Source code:

    Source code is available inside a report about algorithm written in Czech. Source code is at the end and it's in English:

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

    0 讨论(0)
  • 2020-11-30 19:05

    Intuitively, this sounds like an NP-complete problem (it reduces to a problem very like bin packing), however the following algorithm (a modified form of bin packing) should be pretty good (no time for a proof, sorry).

    1. Net out everyone's positions, i.e. from your example above:

      Alice = $4 Bill = $-4 Charles = $0

    2. Sort all net creditors from highest to lowest, and all debtors from lowest to highest, then match by iterating over the lists.

    3. At some point you might need to split a person's debts to net everything out - here it is probably best to split into the biggest chunks possible (i.e. into the bins with the most remaining space first).

    This will take something like O(n log n) (again, proper proof needed).

    See the Partition Problem and Bin Packing for more information (the former is a very similar problem, and if you limit yourself to fixed precision transactions, then it is equivalent - proof needed of course).

    0 讨论(0)
  • 2020-11-30 19:10

    I have designed a solution using a somewhat different approach to the ones that have been mentioned here. It uses a linear programming formulation of the problem, drawing from the Compressed Sensing literature, especially from this work by Donoho and Tanner (2005).

    I have written a blog post describing this approach, along with a tiny R package that implements it. I would love to get some feedback from this community.

    Cheers.

    0 讨论(0)
  • 2020-11-30 19:13

    This is the code I wrote to solve this type of a problem in Java. I am not 100% sure if this gives the least number of transactions. The code's clarity and structure can be improved a lot.

    In this example:

    • Sarah spent $400 on car rental. The car was used by Sarah, Bob, Alice and John.
    • John spent $100 on groceries. The groceries were consumed by Bob and Alice.

      import java.util.*;
      public class MoneyMinTransactions {
      
          static class Expense{
              String spender;
              double amount;
              List<String> consumers;
              public Expense(String spender, double amount, String... consumers){
                  this.spender = spender;
                  this.amount = amount;
                  this.consumers = Arrays.asList(consumers);
              }
      
          }
      
          static class Owed{
              String name;
              double amount;
              public Owed(String name, double amount){
                  this.name = name;
                  this.amount = amount;
              }
          }
      
          public static void main(String[] args){
              List<Expense> expenseList = new ArrayList<>();
              expenseList.add(new Expense("Sarah", 400, "Sarah", "John", "Bob", "Alice"));
              expenseList.add(new Expense("John", 100, "Bob", "Alice"));
              //make list of who owes how much.
              Map<String, Double> owes = new HashMap<>();
              for(Expense e:expenseList){
                  double owedAmt = e.amount/e.consumers.size();
                  for(String c : e.consumers){
                      if(!e.spender.equals(c)){
                          if(owes.containsKey(c)){
                              owes.put(c, owes.get(c) + owedAmt);
                          }else{
                              owes.put(c, owedAmt);
                          }
                          if(owes.containsKey(e.spender)){
                              owes.put(e.spender, owes.get(e.spender) + (-1 * owedAmt));
                          }else{
                              owes.put(e.spender, (-1 * owedAmt));
                          }
                      }
                  }
              }
              //make transactions.
              // We need to settle all the negatives with positives. Make list of negatives. Order highest owed (i.e. the lowest negative) to least owed amount.
              List<Owed> owed = new ArrayList<>();
              for(String s : owes.keySet()){
                  if(owes.get(s) < 0){
                      owed.add(new Owed(s, owes.get(s)));
                  }
              }
              Collections.sort(owed, new Comparator<Owed>() {
                  @Override
                  public int compare(Owed o1, Owed o2) {
                      return Double.compare(o1.amount, o2.amount);
                  }
              });
              //take the highest negative, settle it with the best positive match:
              // 1. a positive that is equal to teh absolute negative amount is the best match.  
              // 2. the greatest positive value is the next best match. 
              // todo not sure if this matching strategy gives the least number of transactions.
              for(Owed owedPerson: owed){
                  while(owes.get(owedPerson.name) != 0){
                      double negAmt = owes.get(owedPerson.name);
                      //get the best person to settle with
                      String s = getBestMatch(negAmt, owes);
                      double posAmt = owes.get(s);
                      if(posAmt > Math.abs(negAmt)){
                          owes.put(owedPerson.name, 0.0);
                          owes.put(s, posAmt - Math.abs(negAmt));
                          System.out.println(String.format("%s paid %s to %s", s, Double.toString((posAmt - Math.abs(negAmt))), owedPerson.name));
                      }else{
                          owes.put(owedPerson.name, -1 * (Math.abs(negAmt) - posAmt));
                          owes.put(s, 0.0);
                          System.out.println(String.format("%s paid %s to %s", s, Double.toString(posAmt), owedPerson.name));
                      }
                  }
              }
      
      
      
      
          }
      
          private static String getBestMatch(double negAmount, Map<String, Double> owes){
              String greatestS = null;
              double greatestAmt = -1;
              for(String s: owes.keySet()){
                  double amt = owes.get(s);
                  if(amt > 0){
                      if(amt == Math.abs(negAmount)){
                          return s;
                      }else if(greatestS == null || amt > greatestAmt){
                          greatestAmt = amt;
                          greatestS = s;
                      }
                  }
              }
              return greatestS;
          }
      
      
      }
      
    0 讨论(0)
  • 2020-11-30 19:14

    Well, the first step is to totally ignore the transactions. Just add them up. All you actually need to know is the net amount of debt a person owes/owns.

    You could very easily find transactions by then creating a crazy flow graph and finding max flow. Then a min cut...

    Some partial elaboration: There is a source node, a sink node, and a node for each person. There will be an edge between every pair of nodes except no edge between source node and sink node. Edges between people have infinite capacity in both directions. Edges coming from source node to person have capacity equal to the net debt of the person (0 if they have no net debt). Edges going from person node to sink node have capacity equal to the net amount of money that person is owed (0 if they have no net owed).

    Apply max flow and/or min cut will give you a set of transfers. The actual flow amount will be how much money will be transfered.

    0 讨论(0)
  • 2020-11-30 19:14

    If you take states as nodes of graph then you will be able to use shortest path algorithm to know the answer.

    0 讨论(0)
提交回复
热议问题