web-dev-qa-db-ja.com

システムを「ゼロ」状態にするために必要なアクションの最小数を決定するために使用するアルゴリズムは何ですか?

これは一種のより一般的な質問であり、言語固有ではありません。使用するアイデアとアルゴリズムの詳細。

システムは次のとおりです。

それは友人のグループ間の小さなローンを登録します。 AliceBillは昼食をとりますが、ビルのカードは機能していません。そのため、アリスは食事代として$ 10を支払います。
翌日、BillCharlesが駅で出会ったとき、Chalesにはチケットのお金がないので、Billは彼に5ドルで購入します。その日遅く、AliceCharlesから5ドル、Billから1ドルを借りて、友人にプレゼントを購入します。

さて、彼ら全員がシステムにtransactionsを登録したと仮定すると、次のようになります。

Alice -> Bill $10
Bill -> Alice $1
Bill -> Charles $5
Charles -> Alice $5

したがって、今、実行する必要があるのはBillAlice $ 4を与えることだけです(彼は彼女に$ 1とCharlieを与えました転送彼の$ 5をAlicealredyに与えました)そしてそれらは初期状態にあります。

複数のトランザクションを持つ多くの異なる人々にそれをスケーリングする場合、トランザクションをできるだけ少なくするための最良のアルゴリズムは何でしょうか?

39
kender

これは実際には複式簿記の概念が役立つ仕事のように見えます。

したがって、トランザクションは簿記エントリとして構成できます。

                          Alice  Bill  Charles  Balance
Alice   -> Bill    $10      10    10-       0        0
Bill    -> Alice    $1       9     9-       0        0
Bill    -> Charles  $5       9     4-       5-       0
Charles -> Alice    $5       4     4-       0        0

そして、あなたはそれを持っています。各トランザクションで、残高が常にゼロになるように、1つの元帳勘定に貸方記入し、別の勘定科目から借方記入します。最後に、各アカウントに適用される最小数のトランザクションを計算して、アカウントをゼロに戻します。

この単純なケースでは、ビルからアリスへの単純な4ドルの送金です。あなたがする必要があるのは、追加されたトランザクションごとに少なくとも1つのアカウント(できれば2つ)をゼロに減らすことです。もっと複雑だったとしましょう:

                          Alice  Bill  Charles  Balance
Alice   -> Bill    $10      10    10-       0        0
Bill    -> Alice    $1       9     9-       0        0
Bill    -> Charles  $5       9     4-       5-       0
Charles -> Alice    $5       4     4-       0        0
Charles -> Bill     $1       4     5-       1        0

その場合、必要なトランザクションは次のようになります。

Bill     -> Alice   $4       0     1-       1        0
Bill     -> Charles $1       0     0        0        0

残念ながら、この単純な欲張り戦略が最良の解決策を生み出さない州がいくつかあります(これを指摘するためのj_random_hackerへの賛辞)。一例は次のとおりです。

                 Alan  Bill  Chas  Doug  Edie  Fred  Bal
Bill->Alan   $5    5-    5     0     0     0     0    0
Bill->Chas  $20    5-   25    20-    0     0     0    0
Doug->Edie   $2    5-   25    20-    2     2-    0    0
Doug->Fred   $1    5-   25    20-    3     2-    1-   0

明らかに、これは4つの動きで逆転する可能性があります(4つの動きがそこに到達するのに必要なすべてであるため)が、最初の動きを賢明に選択しない場合は(Edie->Bill $2)、5があなたが逃げる最小です。

次のルールでこれを解決できます特定の問題:

  • (1)2つの天びんを一掃できる場合は、それを行います。
  • (2)それ以外の場合、1つの天びんを一掃し、次の動きで2つを一掃するように設定できる場合は、それを実行します。
  • (3)それ以外の場合は、いずれかの天びんを拭き取ります。

その結果、次のシーケンスになります。

  • (a)[1]該当なし、[2]はAlan->Bill $5で実現できます。
  • (b)[1]はChas->Bill $20で実行できます。
  • (c)と(d)、ダグ、エディ、フレッドと同様の推論で、合計4回の動き。

ただし、それは単に可能性の数が少ないために機能します。人数が増え、グループの相互関係がより複雑になるにつれて、必要な最小移動数を見つけるために徹底的な検索が必要になる可能性があります(基本的に上記のルール1、2、および3ですが、より多くの深さを処理するために拡張されています) 。

それが、あらゆる状況でトランザクションの数を最小限に抑えるために必要なことだと思います。ただし、それはbest回答には必要ない場合があります(この場合、最良、つまり最大の「1ドルあたりの強打」を意味します)。基本的な1/2/3ルールセットでさえ、あなたの目的に十分な答えを与えるかもしれません。

26
paxdiablo

直感的には、これはNP完全問題のように聞こえますが(ビンパッキングのような問題になります)、次のアルゴリズム(ビンパッキングの修正された形式)はかなり良いはずです(証明の時間がない、申し訳ありません)。

  1. 全員の立場を相殺します。つまり、上記の例から:

    アリス= $ 4ビル= $-4チャールズ= $ 0

  2. すべての純債権者を最高から最低に、すべての債務者を最低から最高に並べ替えてから、リストを繰り返し処理して照合します。

  3. ある時点で、すべてを相殺するために人の借金を分割する必要があるかもしれません-ここでは、可能な限り最大のチャンクに分割するのがおそらく最善です(つまり、残りのスペースが最も多いビンに最初に)。

これには、O(n log n)のようなものが必要です(ここでも、適切な証明が必要です)。

詳細については、 パーティション問題 および ビンパッキング を参照してください(前者は非常によく似た問題であり、固定精度のトランザクションに制限する場合は同等です-証明が必要ですもちろん)。

21
jwoolard

この問題を解決するAndroidアプリを作成しました。旅行中に経費を入力でき、「次に誰が支払うべきか」をお勧めします。最後に、「誰がいくら送金すべきか」を計算します。私のアルゴリズムは必要最小限のトランザクション数を計算し、トランザクションをさらに減らすことができる「トランザクション許容度」を設定できます(1ドルのトランザクションは気にしません)試してみてください。これはSettleUpと呼ばれます。

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

私のアルゴリズムの説明:

N-1トランザクションの問題を解決する基本的なアルゴリズムがありますが、最適ではありません。これは次のように機能します。支払いから、各メンバーの残高を計算します。残高は、彼が支払った金額から支払うべき金額を差し引いたものです。私はますますバランスに従ってメンバーを分類します。それから私はいつも最も貧しくて最も豊かなものを取り、取引が行われます。それらの少なくとも1つはゼロバランスで終わり、以降の計算から除外されます。これにより、トランザクションの数がn-1より悪くなることはありません。また、トランザクションの金額を最小限に抑えます。ただし、内部で解決できるサブグループを検出しないため、最適ではありません。

内部で解決できるサブグループを見つけるのは難しいです。メンバーのすべての組み合わせを生成し、サブグループの残高の合計がゼロに等しいかどうかを確認することで、これを解決します。私は2ペアから始め、次に3ペア...(n-1)ペアです。組み合わせジェネレーターの実装が利用可能です。サブグループを見つけたら、上記の基本的なアルゴリズムを使用してサブグループ内のトランザクションを計算します。見つかったサブグループごとに、1つのトランザクションが節約されます。

解は最適ですが、複雑さはO(n!)まで増加します。これはひどいように見えますが、実際にはメンバーの数が少ないというトリックがあります。 Nexus One(1 Ghzプロセッサ)でテストした結果は次のとおりです。10メンバーまで:<100ミリ秒、15メンバー:1秒、18メンバー:8秒、20メンバー:55秒。したがって、18人のメンバーまで、実行時間は問題ありません。 15を超えるメンバーの回避策は、基本的なアルゴリズムのみを使用することです(高速で正確ですが、最適ではありません)。

ソースコード:

ソースコードは、チェコ語で書かれたアルゴリズムに関するレポート内にあります。ソースコードは最後にあり、英語です:

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

14
David Vávra

Excelで実装した実用的なソリューションを見つけました。

  • 誰が最も借りているかを見つける

  • その人に、最も多くを得る必要がある人に支払うべき全額を支払わせます

  • それは一人称をゼロにします

  • (n-1)人の1人の金額が変更されたことを考慮して、この手順を繰り返します

それは最大(n-1)の転送をもたらすはずであり、それについての素晴らしいことは誰も複数の支払いをしていないということです。 jrandomhackerの(変更された)例を見てください:

a = -5 b = 25 c = -20 d = 3 e = -2 f = -1(合計はゼロでなければなりません!)

  1. c-> b 20.結果:a = -5 b = 5 c = 0 d = 3 e = -2 f = -1

  2. a-> b 5の結果:a = 0 b = 0 c = 0 d = 3 e = -2 f = -1

  3. e-> d2の結果a = 0 b = 0 c = 0 d = 1 e = 0 f = -1

  4. f-> d 1

今では、誰もが満足しており、2回以上の支払いに悩まされることはありません。ご覧のとおり、1人の人が複数の支払いを受け取る可能性があります(この例では人d)。

6
jvreeburg

ここで説明したものとは多少異なるアプローチを使用してソリューションを設計しました。これは、圧縮センシングの文献、特に Donoho and Tanner(2005)によるこの研究 から引用した、問題の線形計画法の定式化を使用しています。

私はブログ投稿を書きました このアプローチを、それを実装する小さなRパッケージとともに説明しています。このコミュニティからフィードバックをもらいたいです。

乾杯。

2
mbiron

さて、最初のステップは、トランザクションを完全に無視することです。それらを合計するだけです。あなたが実際に知る必要があるのは、人が負っている/所有している借金の正味額だけです。

クレイジーなフローグラフを作成し、最大フローを見つけることで、トランザクションを非常に簡単に見つけることができます。次に、最小カット...

いくつかの部分的な詳細:各人のソースノード、シンクノード、およびノー​​ドがあります。ソースノードとシンクノードの間にエッジがないことを除いて、ノードのすべてのペアの間にエッジがあります。人と人との間のエッジは、両方向に無限の容量を持っています。ソースノードから個人に到達するエッジの容量は、個人の純負債に等しくなります(純負債がない場合は0)。個人ノードからシンクノードに移動するエッジの容量は、個人が支払うべき正味金額に等しくなります(正味未払いの場合は0)。

最大フローおよび/または最小カットを適用すると、一連の転送が得られます。実際の送金金額は、送金される金額になります。

2
Brian

誰かが同じセットに借りている2人以上の借金がある場合にのみ、単純なセットからトランザクションの数を減らすことができます。

つまり、単純なセットは、各残高を見つけて返済するだけです。それはNに過ぎません!トランザクション。

AがBとCに負っている場合、およびB Cの一部のサブセットが互いに負っているので、BはCに負っています。代わりに、A-> B、A-> C(3トランザクション)。使用するもの:A-> B、B-> C(2トランザクション)。

つまり、有向グラフを作成していて、パスの長さを最大化し、エッジ全体を最小化するために頂点をトリミングする必要があります。

申し訳ありませんが、アルゴリズムがありません。

1
Adam Luter

これは、Javaでこの種の問題を解決するために私が書いたコードです。これでトランザクション数が最小になるかどうかは100%わかりません。コードの明快さと構造は大幅に改善できます。

この例では:

  • サラはレンタカーに400ドルを費やしました。車はサラ、ボブ、アリス、ジョンによって使用されました。
  • ジョンは食料品に100ドルを費やしました。食料品はボブとアリスによって消費されました。

    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
septerr

O(n)で、最初に各人の借金と借金を決定することで、これを解決できるはずです。借金よりも借金が少ない人の借金を債務者に譲渡します(したがって、その人をエンドポイントに変えます)あなたがそれ以上借金を移すことができなくなるまで繰り返します。

0
Elie