23

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

1.00
2.50
3.75
8.00

而且我知道我的总存款是10.50,我可以很容易地看到它是由8.002.50交易组成的。但是,考虑到一百笔交易和数百万笔存款,它很快就会变得更加困难。

在测试蛮力解决方案(这需要太长时间而不实用)时,我有两个问题:

  1. 对于大约 60 个数字的列表,似乎可以找到十几个或更多组合来构成任何合理的总数。我期待一个组合来满足我的总数,或者可能是几个可能性,但似乎总是有很多组合。有没有描述为什么会这样的数学原理?似乎给定一个中等大小的随机数集合,你可以找到一个多重组合,加起来几乎是你想要的任何总数。

  2. 我为这个问题建立了一个蛮力解决方案,但它显然是 O(n!),并且很快就失去了控制。除了明显的捷径(不包括大于总数本身的数字),有没有办法缩短计算时间?

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

明细金额列表从大到小排序,然后递归运行以下流程:

  • 取出列表中的下一项,看看将其添加到您的运行总数中是否会使您的总数与目标相匹配。如果是这样,请将当前链放在一边作为匹配项。如果它没有达到您的目标,请将其添加到您的运行总计中,将其从详细金额列表中删除,然后再次调用此过程

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

谢谢你的帮助!

4

8 回答 8

18

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

于 2010-10-21T17:24:32.880 回答
10

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 重复,就会出现重复的结果(预期的效果)。实际上,您可能希望使用带有一些 ID 的 subTotal Tupled,以便您可以将其与您的数据相关联。

于 2011-06-27T19:08:35.287 回答
2

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

10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376

您必须测试的集合。蛮力将很难成为解决这些问题的可行方案。

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

于 2010-10-21T17:04:48.733 回答
2

有一个便宜的 Excel 插件可以解决这个问题: SumMatch

SumMatch 在行动

于 2012-12-30T02:18:33.517 回答
2

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

于 2013-07-30T14:50:15.757 回答
1

它有点像0-1背包问题,是NP完全的,可以通过多项式时间内的动态规划来解决。

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

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

于 2010-10-21T16:46:54.590 回答
0

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

于 2010-10-27T18:15:40.380 回答
0

不是一个超级有效的解决方案,但这是咖啡脚本中的一个实现

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 ]

于 2013-06-27T00:03:14.003 回答