查找所有可能的数字组合以达到给定的总和

你将如何去测试给定数字集合中所有可能的加法组合,以便它们加起来给定最终数字?

例:

  • 添加一组数字:{1,5,22,15,0,...}
  • 预期结果:12345

  • 这个问题可以通过递归组合所有可能的总和来解决,这些总和可以过滤那些到达目标的总和。 这里是Python中的算法:

    def subset_sum(numbers, target, partial=[]):
        s = sum(partial)
    
        # check if the partial sum is equals to target
        if s == target: 
            print "sum(%s)=%s" % (partial, target)
        if s >= target:
            return  # if we reach the number why bother to continue
    
        for i in range(len(numbers)):
            n = numbers[i]
            remaining = numbers[i+1:]
            subset_sum(remaining, target, partial + [n]) 
    
    
    if __name__ == "__main__":
        subset_sum([3,9,8,4,5,7,10],15)
    
        #Outputs:
        #sum([3, 8, 4])=15
        #sum([3, 5, 7])=15
        #sum([8, 7])=15
        #sum([5, 10])=15
    

    在下面的斯坦福大学抽象编程讲座中很好地解释了这种类型的算法 - 该视频非常值得推荐,以了解递归如何生成解决方案的排列。

    编辑

    以上作为生成器函数,使其更有用。 需要Python 3.3+,因为yield from

    def subset_sum(numbers, target, partial=[], partial_sum=0):
        if partial_sum == target:
            yield partial
        if partial_sum >= target:
            return
        for i, n in enumerate(numbers):
            remaining = numbers[i + 1:]
            yield from subset_sum(remaining, target, partial + [n], partial_sum + n)
    

    以下是相同算法的Java版本:

    package tmp;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    
    class SumSet {
        static void sum_up_recursive(ArrayList<Integer> numbers, int target, ArrayList<Integer> partial) {
           int s = 0;
           for (int x: partial) s += x;
           if (s == target)
                System.out.println("sum("+Arrays.toString(partial.toArray())+")="+target);
           if (s >= target)
                return;
           for(int i=0;i<numbers.size();i++) {
                 ArrayList<Integer> remaining = new ArrayList<Integer>();
                 int n = numbers.get(i);
                 for (int j=i+1; j<numbers.size();j++) remaining.add(numbers.get(j));
                 ArrayList<Integer> partial_rec = new ArrayList<Integer>(partial);
                 partial_rec.add(n);
                 sum_up_recursive(remaining,target,partial_rec);
           }
        }
        static void sum_up(ArrayList<Integer> numbers, int target) {
            sum_up_recursive(numbers,target,new ArrayList<Integer>());
        }
        public static void main(String args[]) {
            Integer[] numbers = {3,9,8,4,5,7,10};
            int target = 15;
            sum_up(new ArrayList<Integer>(Arrays.asList(numbers)),target);
        }
    }
    

    这与启发式完全相同。 我的Java有点生疏,但我认为很容易理解。

    Java解决方案的C#转换:(由@JeremyThompson提供)

    public static void Main(string[] args)
    {
        List<int> numbers = new List<int>() { 3, 9, 8, 4, 5, 7, 10 };
        int target = 15;
        sum_up(numbers, target);
    }
    
    private static void sum_up(List<int> numbers, int target)
    {
        sum_up_recursive(numbers, target, new List<int>());
    }
    
    private static void sum_up_recursive(List<int> numbers, int target, List<int> partial)
    {
        int s = 0;
        foreach (int x in partial) s += x;
    
        if (s == target)
            Console.WriteLine("sum(" + string.Join(",", partial.ToArray()) + ")=" + target);
    
        if (s >= target)
            return;
    
        for (int i = 0; i < numbers.Count; i++)
        {
            List<int> remaining = new List<int>();
            int n = numbers[i];
            for (int j = i + 1; j < numbers.Count; j++) remaining.Add(numbers[j]);
    
            List<int> partial_rec = new List<int>(partial);
            partial_rec.Add(n);
            sum_up_recursive(remaining, target, partial_rec);
        }
    }
    

    Ruby解决方案:(通过@emaillenin)

    def subset_sum(numbers, target, partial=[])
      s = partial.inject 0, :+
    # check if the partial sum is equals to target
    
      puts "sum(#{partial})=#{target}" if s == target
    
      return if s >= target # if we reach the number why bother to continue
    
      (0..(numbers.length - 1)).each do |i|
        n = numbers[i]
        remaining = numbers.drop(i+1)
        subset_sum(remaining, target, partial + [n])
      end
    end
    
    subset_sum([3,9,8,4,5,7,10],15)
    

    编辑:复杂性讨论

    正如其他人所说,这是一个NP难题。 它可以在指数时间O(2 ^ n)中求解,例如对于n = 10,将会有1024个可能的解。 如果你试图达到的目标是在一个较低的范围内,那么这个算法是有效的。 举例来说:

    subset_sum([1,2,3,4,5,6,7,8,9,10],100000)生成1024个分支,因为目标永远不会过滤掉可能的解决方案。

    另一方面, subset_sum([1,2,3,4,5,6,7,8,9,10],10)仅生成175个分支,因为达到10的目标会过滤掉很多组合。

    如果NTarget是大数字,则应该转换为解决方案的近似版本。


    在Haskell中:

    filter ((==) 12345 . sum) $ subsequences [1,5,22,15,0,..]
    

    和J:

    (]#~12345=+/@>)(]<@#~[:#:@i.2^#)1 5 22 15 0 ...
    

    您可能会注意到,两者都采用相同的方法,并将问题分为两部分:生成权力集的每个成员,并检查每个成员的总和。

    还有其他的解决方案,但这是最直接的。

    你需要任何一方的帮助,还是寻找不同的方法?


    这个问题的解决方案已经在互联网上获得了一百万次。 这个问题被称为硬币改变问题。 人们可以在http://rosqtacode.org/wiki/Count_the_coins找到解决方案,并在http://jaqm.ro/issues/volume-5,issue-2/pdfs/patterson_harmel.pdf(或Google硬币更换问题)。

    顺便说一句,Tsagadai的Scala解决方案很有趣。 这个例子产生1或0.作为副作用,它在控制台上列出了所有可能的解决方案。 它显示解决方案,但无法以任何方式使其可用。

    为了尽可能有用,代码应该返回一个List[List[Int]] ,以便获得解决方案的数量(列表的长度),“最佳”解决方案(最短列表)或全部可能的解决方案。

    这是一个例子。 这是非常低效的,但它很容易理解。

    object Sum extends App {
    
      def sumCombinations(total: Int, numbers: List[Int]): List[List[Int]] = {
    
        def add(x: (Int, List[List[Int]]), y: (Int, List[List[Int]])): (Int, List[List[Int]]) = {
          (x._1 + y._1, x._2 ::: y._2)
        }
    
        def sumCombinations(resultAcc: List[List[Int]], sumAcc: List[Int], total: Int, numbers: List[Int]): (Int, List[List[Int]]) = {
          if (numbers.isEmpty || total < 0) {
            (0, resultAcc)
          } else if (total == 0) {
            (1, sumAcc :: resultAcc)
          } else {
            add(sumCombinations(resultAcc, sumAcc, total, numbers.tail), sumCombinations(resultAcc, numbers.head :: sumAcc, total - numbers.head, numbers))
          }
        }
    
        sumCombinations(Nil, Nil, total, numbers.sortWith(_ > _))._2
      }
    
      println(sumCombinations(15, List(1, 2, 5, 10)) mkString "n")
    }
    

    运行时,它显示:

    List(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)
    List(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2)
    List(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2)
    List(1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2)
    List(1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2)
    List(1, 1, 1, 1, 1, 2, 2, 2, 2, 2)
    List(1, 1, 1, 2, 2, 2, 2, 2, 2)
    List(1, 2, 2, 2, 2, 2, 2, 2)
    List(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5)
    List(1, 1, 1, 1, 1, 1, 1, 1, 2, 5)
    List(1, 1, 1, 1, 1, 1, 2, 2, 5)
    List(1, 1, 1, 1, 2, 2, 2, 5)
    List(1, 1, 2, 2, 2, 2, 5)
    List(2, 2, 2, 2, 2, 5)
    List(1, 1, 1, 1, 1, 5, 5)
    List(1, 1, 1, 2, 5, 5)
    List(1, 2, 2, 5, 5)
    List(5, 5, 5)
    List(1, 1, 1, 1, 1, 10)
    List(1, 1, 1, 2, 10)
    List(1, 2, 2, 10)
    List(5, 10)
    

    sumCombinations()函数本身可以使用,并且可以进一步分析结果以显示“最佳”解决方案(最短列表)或解决方案数目(列表数量)。

    请注意,即使如此,这些要求可能不完全满足。 可能发生的问题是解决方案中每个列表的顺序都很重要。 在这种情况下,每个列表都必须重复尽可能多的时间,因为它们的元素组合在一起。 或者我们可能只对不同的组合感兴趣。

    例如,我们可以考虑List(5, 10)应该给出两种组合: List(5, 10)List(10, 5) 。 对于List(5, 5, 5)它可以给出三种组合或一种,这取决于要求。 对于整数,这三种排列是等价的,但如果我们正在处理硬币,就像在“硬币改变问题”中那样,它们不是。

    在要求中也没有规定每个数字(或硬币)是否只能使用一次或多次的问题。 我们可以(也应该!)将问题概括为每个数字出现次数列表。 这在现实生活中转化为“用一组硬币(而不是一组硬币值)来赚取一定数量的金钱的可能方式”。 原来的问题仅仅是这个问题的一个特例,在这个问题中,每个硬币的出现次数与每个硬币的总金额一样多。

    链接地址: http://www.djcxy.com/p/64419.html

    上一篇: Finding all possible combinations of numbers to reach a given sum

    下一篇: What is a magic number, and why is it bad?