一部の会計士が抱えている一般的な問題を解決するのを手伝う任務を果たしてきました-トランザクションのリストと合計預金を考えると、どのトランザクションが預金の一部ですか?たとえば、次の番号のリストがあるとします。
1.00
2.50
3.75
8.00
私の預金総額は10.50
であることを知っています。8.00
と2.50
トランザクションで構成されていることが簡単にわかります。ただし、100件のトランザクションと数百万件の預金があると、それはすぐにはるかに困難になります。
ブルートフォースソリューションのテスト(時間がかかりすぎて実用的ではありません)で、2つの質問がありました。
約60個の数値のリストがあるため、妥当な合計に対して12個以上の組み合わせが見つかるようです。 単一の組み合わせで合計またはいくつかの可能性を満たすことが期待されていましたが、常に大量の組み合わせがあるようです。これがなぜであるかを説明する数学の原理はありますか?中程度のサイズの乱数のコレクションで、合計がほぼすべての合計になる複数の組み合わせを見つけることができます。
私は問題に対してブルートフォースソリューションを構築しましたが、それは明らかにO(n!)であり、すぐに制御できなくなります。明白なショートカット(合計よりも大きい数値を除外する)以外に、これを計算する時間を短縮する方法はありますか?
私の現在の(超低速)ソリューションの詳細:
詳細金額のリストは最大から最小にソートされ、次のプロセスが再帰的に実行されます。
このようにして、より大きな数値をすばやく除外し、リストを、検討する必要がある数値のみに切り詰めます。ただし、まだnです。そして、より大きなリストが完成するようには見えないので、これをスピードアップするために取ることができるショートカットに興味があります-リストから1つの数値をカットしても、計算時間が半分になると思います。
ご協力いただきありがとうございます!
ナップザック問題のこの特別なケースは サブセット和 と呼ばれます。
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と一緒に使用して、データに関連付けることができます。
私があなたの問題を正しく理解している場合、一連のトランザクションがあり、特定の合計に含まれている可能性のあるトランザクションを知りたいだけです。したがって、4つの可能なトランザクションがある場合、2 ^ 4 = 16の可能なセットが検査されます。この問題は、100の可能なトランザクションの場合、検索スペースに2 ^ 100 = 1267650600228229401496703205376の可能な組み合わせが存在するためです。ミックスで1000の潜在的なトランザクションの場合、合計で次のようになります。
10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686902167672056680692676720566806926767205668069216767205616569676720561656937672056456696969695630569866969562566802666
テストする必要があるセット。ブルートフォースは、これらの問題の実行可能な解決策にはなりません。
代わりに、 knapsack の問題を処理できるソルバーを使用してください。しかし、それでも、ブルートフォースをある程度変化させることなく、考えられるすべてのソリューションの完全な列挙を生成できるかどうかはわかりません。
この問題を解決する安価なExcelアドインがあります: SumMatch
Superuser.comに投稿されたExcelソルバーアドインには素晴らしい解決策があります(Excelがある場合) https://superuser.com/questions/204925/Excel-find-a-subset-of-numbers-that -add-to-a-given-total
そのような一種の0-1ナップザック問題はNP完全であり、多項式時間での動的プログラミングによって解決できます。
http://en.wikipedia.org/wiki/Knapsack_problem
ただし、アルゴリズムの最後に、合計が希望どおりであることを確認する必要もあります。
超効率的なソリューションではありませんが、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 ]
を返します
データによっては、最初に各トランザクションのセント部分を確認できます。最初の例のように、2.50は50に加算されるゼロ以外のセントトランザクションの唯一のセットであるため、合計の一部である必要があることを知っています。