J'ai été chargé d'aider certains comptables à résoudre un problème commun qu'ils ont - donné une liste de transactions et un dépôt total, quelles transactions font partie du dépôt? Par exemple, disons que j'ai cette liste de numéros:
1.00
2.50
3.75
8.00
Et je sais que mon dépôt total est 10.50
, Je peux facilement voir qu'il est composé de 8.00
et 2.50
transaction. Cependant, compte tenu d'une centaine de transactions et d'un dépôt de plusieurs millions, cela devient rapidement beaucoup plus difficile.
En testant une solution de force brute (qui prend trop de temps pour être pratique), j'avais deux questions:
Avec une liste d'environ 60 numéros, il semble trouver une douzaine ou plus de combinaisons pour un total raisonnable. Je m'attendais à une seule combinaison pour satisfaire mon total, ou peut-être quelques possibilités, mais il semble toujours y avoir une tonne de combinaisons. Y a-t-il un principe mathématique qui décrit pourquoi c'est? Il semble que, étant donné une collection de nombres aléatoires, même de taille moyenne, vous pouvez trouver une combinaison multiple qui représente à peu près le total que vous voulez.
J'ai construit une solution de force brute pour le problème, mais c'est clairement O (n!), Et devient rapidement hors de contrôle. Mis à part les raccourcis évidents (exclure les nombres supérieurs au total eux-mêmes), existe-t-il un moyen de raccourcir le temps de calcul?
Détails sur ma solution actuelle (super-lente):
La liste des montants détaillés est triée du plus grand au plus petit, puis le processus suivant s'exécute récursivement:
De cette façon, il exclut rapidement les plus grands nombres, réduisant la liste aux seuls nombres à prendre en compte. Cependant, c'est toujours n! et les listes plus grandes ne semblent jamais se terminer, donc je suis intéressé par les raccourcis que je pourrais utiliser pour accélérer cela - je soupçonne que même la suppression d'un numéro de la liste réduirait de moitié le temps de calcul.
Merci de votre aide!
Ce cas particulier du problème Knapsack est appelé Subset Sum .
version C #
test de configuration:
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();
}
}
code:
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)));
}
}
résultats:
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.
Si des sous-totaux sont répétés, il semblera y avoir des résultats en double (l'effet souhaité). En réalité, vous voudrez probablement utiliser le sous-total multiplié avec une pièce d'identité, afin de pouvoir le relier à vos données.
Si je comprends bien votre problème, vous disposez d'un ensemble de transactions et vous souhaitez simplement savoir lesquelles d'entre elles auraient pu être incluses dans un total donné. Donc, s'il y a 4 transactions possibles, il y a 2 ^ 4 = 16 ensembles possibles à inspecter. Ce problème est, pour 100 transactions possibles, l'espace de recherche a 2 ^ 100 = 1267650600228229401496703205376 combinaisons possibles pour rechercher. Pour 1000 transactions potentielles dans le mix, il atteint un total de
10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376
ensembles que vous devez tester. La force brute ne sera guère une solution viable à ces problèmes.
À la place, utilisez un solveur capable de gérer les problèmes sac à dos . Mais même alors, je ne suis pas sûr que vous puissiez générer une énumération complète de toutes les solutions possibles sans une certaine variation de la force brute.
Il existe un complément Excel bon marché qui résout ce problème: SumMatch
Le complément Excel Solver tel que publié sur superuser.com a une excellente solution (si vous avez Excel) https://superuser.com/questions/204925/Excel-find-a-subset-of-numbers-that -ajouter à un total donné
Son genre de problème de sac à dos 0-1 qui est NP-complet et peut être résolu par une programmation dynamique en temps polynomial.
http://en.wikipedia.org/wiki/Knapsack_problem
Mais à la fin de l'algorithme, vous devez également vérifier que la somme correspond à ce que vous vouliez.
Pas une solution super efficace, mais une implémentation en coffeescript
combinations
renvoie toutes les combinaisons possibles des éléments dans 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
puis find_components
s'en sert pour déterminer quels nombres s'additionnent à 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 []
Voici un exemple
list = [7.2, 3.3, 4.5, 6.0, 2, 4.1]
total = 7.2 + 2 + 4.1
console.log(find_components(total, list))
qui renvoie [ 7.2, 2, 4.1 ]
En fonction de vos données, vous pouvez d'abord regarder la portion en cents de chaque transaction. Comme dans votre exemple initial, vous savez que 2,50 doit faire partie du total parce que c'est le seul ensemble de transactions de cent non nul qui s'ajoute à 50.