web-dev-qa-db-ja.com

セット内の数値のどの組み合わせが特定の合計になるかを調べる

一部の会計士が抱えている一般的な問題を解決するのを手伝う任務を果たしてきました-トランザクションのリストと合計預金を考えると、どのトランザクションが預金の一部ですか?たとえば、次の番号のリストがあるとします。

1.00
2.50
3.75
8.00

私の預金総額は10.50であることを知っています。8.002.50トランザクションで構成されていることが簡単にわかります。ただし、100件のトランザクションと数百万件の預金があると、それはすぐにはるかに困難になります。

ブルートフォースソリューションのテスト(時間がかかりすぎて実用的ではありません)で、2つの質問がありました。

  1. 約60個の数値のリストがあるため、妥当な合計に対して12個以上の組み合わせが見つかるようです。 単一の組み合わせで合計またはいくつかの可能性を満たすことが期待されていましたが、常に大量の組み合わせがあるようです。これがなぜであるかを説明する数学の原理はありますか?中程度のサイズの乱数のコレクションで、合計がほぼすべての合計になる複数の組み合わせを見つけることができます。

  2. 私は問題に対してブルートフォースソリューションを構築しましたが、それは明らかにO(n!)であり、すぐに制御できなくなります。明白なショートカット(合計よりも大きい数値を除外する)以外に、これを計算する時間を短縮する方法はありますか?

私の現在の(超低速)ソリューションの詳細:

詳細金額のリストは最大から最小にソートされ、次のプロセスが再帰的に実行されます。

  • リストの次のアイテムを取り、それを現在の合計に追加すると、合計がターゲットと一致するかどうかを確認します。一致する場合は、現在のチェーンを一致として取っておきます。目標に達していない場合は、現在の合計に追加し、詳細金額のリストから削除してから、このプロセスを再度呼び出します

このようにして、より大きな数値をすばやく除外し、リストを、検討する必要がある数値のみに切り詰めます。ただし、まだnです。そして、より大きなリストが完成するようには見えないので、これをスピードアップするために取ることができるショートカットに興味があります-リストから1つの数値をカットしても、計算時間が半分になると思います。

ご協力いただきありがとうございます!

21
SqlRyan

ナップザック問題のこの特別なケースは サブセット和 と呼ばれます。

16
Falk Hüffner

C#バージョン

セットアップテスト:

using System;
using System.Collections.Generic;

public class Program
{
    public static void Main(string[] args)
    {
    // subtotal list
    List<double> totals = new List<double>(new double[] { 1, -1, 18, 23, 3.50, 8, 70, 99.50, 87, 22, 4, 4, 100.50, 120, 27, 101.50, 100.50 });

    // get matches
    List<double[]> results = Knapsack.MatchTotal(100.50, totals);

    // print results
    foreach (var result in results)
    {
        Console.WriteLine(string.Join(",", result));
    }

    Console.WriteLine("Done.");
    Console.ReadKey();
    }
}

コード:

using System.Collections.Generic;
using System.Linq;

public class Knapsack
{
    internal static List<double[]> MatchTotal(double theTotal, List<double> subTotals)
    {
    List<double[]> results = new List<double[]>();

    while (subTotals.Contains(theTotal))
    {
        results.Add(new double[1] { theTotal });
        subTotals.Remove(theTotal);
    }

    // if no subtotals were passed
    // or all matched the Total
    // return
    if (subTotals.Count == 0)
        return results;

    subTotals.Sort();

    double mostNegativeNumber = subTotals[0];
    if (mostNegativeNumber > 0)
        mostNegativeNumber = 0;

    // if there aren't any negative values
    // we can remove any values bigger than the total
    if (mostNegativeNumber == 0)
        subTotals.RemoveAll(d => d > theTotal);

    // if there aren't any negative values
    // and sum is less than the total no need to look further
    if (mostNegativeNumber == 0 && subTotals.Sum() < theTotal)
        return results;

    // get the combinations for the remaining subTotals
    // skip 1 since we already removed subTotals that match
    for (int choose = 2; choose <= subTotals.Count; choose++)
    {
        // get combinations for each length
        IEnumerable<IEnumerable<double>> combos = Combination.Combinations(subTotals.AsEnumerable(), choose);

        // add combinations where the sum mathces the total to the result list
        results.AddRange(from combo in combos
                 where combo.Sum() == theTotal
                 select combo.ToArray());
    }

    return results;
    }
}

public static class Combination
{
    public static IEnumerable<IEnumerable<T>> Combinations<T>(this IEnumerable<T> elements, int choose)
    {
    return choose == 0 ?                        // if choose = 0
        new[] { new T[0] } :                    // return empty Type array
        elements.SelectMany((element, i) =>     // else recursively iterate over array to create combinations
        elements.Skip(i + 1).Combinations(choose - 1).Select(combo => (new[] { element }).Concat(combo)));
    }
}

結果:

100.5
100.5
-1,101.5
1,99.5
3.5,27,70
3.5,4,23,70
3.5,4,23,70
-1,1,3.5,27,70
1,3.5,4,22,70
1,3.5,4,22,70
1,3.5,8,18,70
-1,1,3.5,4,23,70
-1,1,3.5,4,23,70
1,3.5,4,4,18,70
-1,3.5,8,18,22,23,27
-1,3.5,4,4,18,22,23,27
Done.

SubTotalsが繰り返される場合、結果が重複しているように見えます(望ましい効果)。実際には、subTotal Tupledを何らかのIDと一緒に使用して、データに関連付けることができます。

9
Dan

私があなたの問題を正しく理解している場合、一連のトランザクションがあり、特定の合計に含まれている可能性のあるトランザクションを知りたいだけです。したがって、4つの可能なトランザクションがある場合、2 ^ 4 = 16の可能なセットが検査されます。この問題は、100の可能なトランザクションの場合、検索スペースに2 ^ 100 = 1267650600228229401496703205376の可能な組み合わせが存在するためです。ミックスで1000の潜在的なトランザクションの場合、合計で次のようになります。

10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686902167672056680692676720566806926767205668069216767205616569676720561656937672056456696969695630569866969562566802666

テストする必要があるセット。ブルートフォースは、これらの問題の実行可能な解決策にはなりません。

代わりに、 knapsack の問題を処理できるソルバーを使用してください。しかし、それでも、ブルートフォースをある程度変化させることなく、考えられるすべてのソリューションの完全な列挙を生成できるかどうかはわかりません。

2
user85109

この問題を解決する安価なExcelアドインがあります: SumMatch

SumMatch in action

2
Albert

Superuser.comに投稿されたExcelソルバーアドインには素晴らしい解決策があります(Excelがある場合) https://superuser.com/questions/204925/Excel-find-a-subset-of-numbers-that -add-to-a-given-total

2
Omar Shahine

そのような一種の0-1ナップザック問題はNP完全であり、多項式時間での動的プログラミングによって解決できます。

http://en.wikipedia.org/wiki/Knapsack_problem

ただし、アルゴリズムの最後に、合計が希望どおりであることを確認する必要もあります。

1
vinothkr

超効率的なソリューションではありませんが、coffeescriptでの実装をここに示します

combinationsは、listの要素の可能なすべての組み合わせを返します

combinations = (list) ->
        permuations = Math.pow(2, list.length) - 1
        out = []
        combinations = []

        while permuations
            out = []

            for i in [0..list.length]
                y = ( 1 << i )
                if( y & permuations and (y isnt permuations))
                    out.Push(list[i])

            if out.length <= list.length and out.length > 0
                combinations.Push(out)

            permuations--

        return combinations

そしてfind_componentsはそれを利用して、どの数値がtotalになるかを決定します

find_components = (total, list) ->
    # given a list that is assumed to have only unique elements

        list_combinations = combinations(list)

        for combination in list_combinations
            sum = 0
            for number in combination
                sum += number

            if sum is total
                return combination
        return []

例はこちら

list = [7.2, 3.3, 4.5, 6.0, 2, 4.1]
total = 7.2 + 2 + 4.1

console.log(find_components(total, list)) 

[ 7.2, 2, 4.1 ]を返します

0
Loourr

データによっては、最初に各トランザクションのセント部分を確認できます。最初の例のように、2.50は50に加算されるゼロ以外のセントトランザクションの唯一のセットであるため、合計の一部である必要があることを知っています。

0
Jon Snyder