找出一组中的数字组合加起来给定的总数

时间:2010-10-21 16:39:25

标签: algorithm math nested-loops accounting

我的任务是帮助一些会计师解决他们遇到的常见问题 - 给出交易清单和总存款,这些交易是存款的一部分?例如,假设我有这个数字列表:

1.00
2.50
3.75
8.00

我知道我的总存款为10.50,我可以很容易地看到它由8.002.50交易组成。然而,鉴于一百笔交易和数百万美元的存款,它很快变得更加困难。

在测试暴力解决方案(实施时间太长而无法实施)时,我有两个问题:

  1. 有一个大约60个数字的列表,它似乎找到了十几个或更多的组合,任何合理的总和。 我期待一个单一的组合来满足我的总数,或者可能是一些可能性,但似乎总是有很多组合。是否有一个数学原理描述了为什么会这样?看来,即使是中等大小的随机数集合,您也可以找到多个组合,几乎可以达到您想要的总数。

  2. 我为这个问题建立了一个强力解决方案,但它显然是O(n!),并且很快失控。除了明显的快捷方式(排除大于总数的快捷方式),有没有办法缩短计算时间?

  3. 有关我当前(超慢)解决方案的详细信息:

    详细信息量列表从最大到最小排序,然后以下过程以递归方式运行:

    • 选择列表中的下一个项目,看看是否将其添加到正在运行的总计中会使您的总匹配成为目标。如果是,请将当前链作为匹配项。如果未达到目标,请将其添加到运行总计中,将其从详细信息列表中删除,然后再次调用此流程

    通过这种方式,它可以快速排除较大的数字,将列表缩小到只需要考虑的数字。但是,它仍然是n!似乎永远不会完成更大的列表,所以我对我可以采取的任何捷径感兴趣 - 我怀疑即使从列表中删除1个数字也会将计算时间缩短一半。

    感谢您的帮助!

8 个答案:

答案 0 :(得分:15)

背包问题的这种特殊情况称为Subset Sum

答案 1 :(得分:8)

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一起使用,这样您就可以将其与您的数据联系起来。

答案 2 :(得分:2)

如果我理解你的问题,你有一组交易,而你只想知道哪些交易可以包含在给定的总数中。因此,如果有4个可能的事务,那么有2 ^ 4 = 16个可能的集合要检查。这个问题是,对于100个可能的事务,搜索空间有2 ^ 100 = 1267650600228229401496703205376可能的搜索组合。对于混合中的1000个潜在交易,它增长到总数

10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376

设置你必须测试。蛮力很难成为解决这些问题的可行办法。

相反,请使用可以处理knapsack问题的解算器。但即便如此,我也不确定你是否可以在没有蛮力变化的情况下生成所有可能解决方案的完整枚举。

答案 3 :(得分:2)

有一个便宜的Excel加载项可以解决这个问题:SumMatch

SumMatch in action

答案 4 :(得分:2)

在superuser.com上发布的Excel Solver Addin有一个很好的解决方案(如果你有Excel)https://superuser.com/questions/204925/excel-find-a-subset-of-numbers-that-add-to-a-given-total

答案 5 :(得分:1)

类似于0-1背包问题,它是NP完全的,可以通过多项式时间的动态编程来解决。

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

但是在算法结束时,您还需要检查总和是否是您想要的。

答案 6 :(得分:0)

根据您的数据,您可以先查看每笔交易的分数部分。就像你最初的例子一样,你知道2.50必须是总数的一部分,因为它是唯一一组非零分的交易,增加到50。

答案 7 :(得分:0)

不是一个超级高效的解决方案,而是继承了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 ]