Узнайте, какие комбинации чисел в наборе складываются до заданной суммы - PullRequest
20 голосов
/ 21 октября 2010

Мне было поручено помочь некоторым бухгалтерам решить общую проблему, с которой они столкнулись - учитывая список транзакций и общий депозит, какие транзакции являются частью депозита? Например, скажем, у меня есть этот список чисел:

1.00
2.50
3.75
8.00

И я знаю, что мой общий депозит составляет 10.50, я легко вижу, что он состоит из транзакций 8.00 и 2.50. Однако, учитывая сотню транзакций и депозит в миллионах, это быстро становится намного сложнее.

При тестировании решения для грубой силы (которое занимает слишком много времени, чтобы быть практичным) у меня было два вопроса:

  1. Со списком из примерно 60 чисел, кажется, можно найти дюжину или более комбинаций для любой разумной суммы. Я ожидал, что одна комбинация удовлетворит мою общую сумму, или, возможно, несколько возможностей, но, кажется, всегда есть масса комбинаций. Есть ли математический принцип, который описывает, почему это так? Кажется, что, учитывая набор случайных чисел даже среднего размера, вы можете найти кратную комбинацию, которая в сумме дает почти любую сумму, которую вы хотите.

  2. Я построил решение проблемы грубой силой, но оно явно O (n!) И быстро выходит из-под контроля. Помимо очевидных ярлыков (исключая числа, превышающие общее количество), есть ли способ сократить время для вычисления этого?

Подробная информация о моем текущем (супер-медленном) решении:

Список сумм деталей сортируется по возрастанию от наименьшего, а затем следующий процесс выполняется рекурсивно:

  • Возьмите следующий элемент в списке и посмотрите, соответствует ли его добавление вашему итоговому итоговому значению цели. Если это так, отложите текущую цепочку как совпадение. Если он не соответствует вашей цели, добавьте его к промежуточному итогу, удалите его из списка сумм сведений, а затем снова вызовите этот процесс

Таким образом, он быстро исключает большие числа, сокращая список до тех чисел, которые ему необходимо учитывать. Тем не менее, это все еще п! и более крупные списки, кажется, никогда не заканчиваются, поэтому меня интересуют любые ярлыки, которые я мог бы использовать, чтобы ускорить это - я подозреваю, что даже удаление 1 числа из списка сократит время вычисления вдвое.

Спасибо за вашу помощь!

Ответы [ 8 ]

15 голосов
/ 21 октября 2010

Этот особый случай задачи о ранце называется Сумма подмножества .

8 голосов
/ 27 июня 2011

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.

Если промежуточные итоги повторяются, результаты будут дублироваться (желаемый эффект).В действительности вам, вероятно, захочется использовать подытог Tupled с некоторым идентификатором, чтобы вы могли связать его с вашими данными.

2 голосов
/ 30 июля 2013

Надстройка Excel Solver, опубликованная на сайте superuser.com, предлагает отличное решение (если у вас есть Excel) https://superuser.com/questions/204925/excel-find-a-subset-of-numbers-that-add-to-a-given-total

2 голосов
/ 30 декабря 2012

Существует дешевая надстройка Excel, которая решает эту проблему: SumMatch

SumMatch in action

2 голосов
/ 21 октября 2010

Если я правильно понимаю вашу проблему, у вас есть набор транзакций, и вы просто хотите знать, какие из них могли бы быть включены в данную сумму. Таким образом, если есть 4 возможных транзакции, то есть 2 ^ 4 = 16 возможных наборов для проверки. Эта проблема состоит в том, что для 100 возможных транзакций пространство поиска имеет 2 ^ 100 = 1267650600228229401496703205376 возможных комбинаций для поиска. Для 1000 потенциальных транзакций в совокупности он увеличивается до

10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376

устанавливает, что вы должны проверить. Грубая сила вряд ли будет жизнеспособным решением этих проблем.

Вместо этого используйте решатель, который может решить проблемы рюкзак . Но даже тогда я не уверен, что вы можете создать полный перечень всех возможных решений без некоторого изменения грубой силы.

1 голос
/ 21 октября 2010

Это своего рода проблема ранца 0-1, которая является NP-полной и может быть решена с помощью динамического программирования за полиномиальное время.

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

Но в конце алгоритма вам также нужно проверить, что сумма соответствует вашей.

0 голосов
/ 27 июня 2013

Не очень эффективное решение, но вот реализация в 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 голосов
/ 27 октября 2010

В зависимости от ваших данных вы можете сначала посмотреть на центовую часть каждой транзакции.Как и в первом примере, вы знаете, что 2,50 должно быть частью общей суммы, потому что это единственный набор транзакций с ненулевым центом, который добавляет 50.

...