web-dev-qa-db-fra.com

Découvrez quelles combinaisons de nombres dans un ensemble totalisent un total donné

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:

  1. 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.

  2. 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:

  • Prenez l'élément suivant dans la liste et voyez si l'ajouter à votre total cumulé correspond à votre objectif. Si tel est le cas, mettez la chaîne actuelle de côté comme correspondance. S'il ne correspond pas à votre objectif, ajoutez-le à votre total cumulé, supprimez-le de la liste des montants détaillés, puis appelez à nouveau ce processus.

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!

21
SqlRyan

Ce cas particulier du problème Knapsack est appelé Subset Sum .

16
Falk Hüffner

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.

9
Dan

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.

2
user85109

Il existe un complément Excel bon marché qui résout ce problème: SumMatch

SumMatch in action

2
Albert

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é

2
Omar Shahine

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.

1
vinothkr

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 ]

0
Loourr

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.

0
Jon Snyder