Подтвердить что ты не робот

Выясните, какие комбинации чисел в наборе складываются с заданной суммой

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

1.00
2.50
3.75
8.00

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

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

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

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

Информация о моем текущем (сверх медленном) решении:

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

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

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

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

4b9b3361

Ответ 2

Версия С#

установочный тест:

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.

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

Ответ 3

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

10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376

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

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

Ответ 4

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

SumMatch in action

Ответ 5

Его вроде как 0-1 проблема Knapsack, которая NP-полная и может быть решена посредством динамического программирования в полиномиальное время.

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

Но в конце алгоритма вам также нужно проверить, что сумма - это то, что вы хотели.

Ответ 7

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

Ответ 8

Не суперэффективное решение, но реализация реализации в 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 ]