【问题标题】:Find combinations of numbers that sum to some desired number查找总和为某个所需数字的数字组合
【发布时间】:2010-07-29 13:59:49
【问题描述】:

我需要一种算法来识别一组数字的所有可能组合,这些数字的总和为某个其他数字。

例如,给定集合{2,3,4,7},我需要知道总和为x 的所有可能子集。如果x == 12,则答案为{2,3,7};如果x ==7 的答案是{{3,4},{7}}(即两个可能的答案);如果x==8 没有答案。请注意,正如这些示例所暗示的,集合中的数字不能重复使用。

这个问题was asked on this site a couple years ago 但答案是在 C# 中,我需要在 Perl 中完成并且不知道如何翻译答案。

我知道这个问题很难(请参阅其他帖子进行讨论),但我只需要一个蛮力解决方案,因为我正在处理相当小的集合。

【问题讨论】:

  • +1 表示您的最后一段。知道什么运行时是可接受的总是至关重要的。
  • Note that, as these example imply, numbers in the set cannot be reused. 所以如果你有{2,3,4,6,9}x==11,那么你就不能有{{2,3,6},{2,9}},因为2的重用?或者set 是指subset
  • @vol7ron,他的意思是 {2,3,3,3} 不是您示例的有效答案,因为您只能使用 3 一次(每个子集)。
  • @cjm:我会这么想,但是不同的组合游戏有不同的规则
  • @vol7ron,@cjm 是正确的。对于我需要解决的问题,集合的每个数字只能使用一次。

标签: perl math


【解决方案1】:
sub Solve
{
  my ($goal, $elements) = @_;

  # For extra speed, you can remove this next line
  # if @$elements is guaranteed to be already sorted:
  $elements = [ sort { $a <=> $b } @$elements ];

  my (@results, $RecursiveSolve, $nextValue);

  $RecursiveSolve = sub {
    my ($currentGoal, $included, $index) = @_;

    for ( ; $index < @$elements; ++$index) {
      $nextValue = $elements->[$index];
      # Since elements are sorted, there's no point in trying a
      # non-final element unless it's less than goal/2:
      if ($currentGoal > 2 * $nextValue) {
        $RecursiveSolve->($currentGoal - $nextValue,
                          [ @$included, $nextValue ],
                          $index + 1);
      } else {
        push @results, [ @$included, $nextValue ]
            if $currentGoal == $nextValue;
        return if $nextValue >= $currentGoal;
      }
    } # end for
  }; # end $RecursiveSolve

  $RecursiveSolve->($goal, [], 0);
  undef $RecursiveSolve; # Avoid memory leak from circular reference
  return @results;
} # end Solve


my @results = Solve(7, [2,3,4,7]);
print "@$_\n" for @results;

这开始是the C# version from the question you linked 的相当直接的翻译,但我简化了一点(现在又简化了一点,还删除了一些不必要的变量分配,根据被排序的元素列表添加了一些优化,并重新排列条件稍微更有效)。

我现在还添加了另一个重要的优化。在考虑是否尝试使用未完成总和的元素时,如果该元素大于或等于当前目标的一半,则没有意义。 (我们添加的下一个数字会更大。)根据您尝试的设置,这可能会短路更多。 (您也可以尝试添加下一个元素而不是乘以 2,但是您必须担心会超出列表的末尾。)

【讨论】:

  • 谢谢!我只是将它与一组 52 个数字一起使用,不到一分钟就得到了结果。
  • @itzy,不客气。通过移动条件,我又做了一个小的改进。在测试互斥条件时,首先尝试最有可能为真的条件通常会更快。
【解决方案2】:

大致的算法如下:

有一个“求解”函数,它接受一个已经包含的数字列表和一个尚未包含的数字列表。

  • 此函数将遍历所有尚未包含的数字。
  • 如果添加该数字达到目标,则记录该组数字并继续前进,
  • 如果它小于目标,则递归调用函数,并使用您正在查看的数字修改包含/排除列表。
  • 否则,只需进入循环的下一步(因为如果您已经结束,除非您允许负数,否则尝试添加更多数字是没有意义的)
  • 您最初调用此函数时包含的列表为空,而尚未包含的列表包含完整的数字列表。

您可以对此进行一些优化,例如传递总和而不是每次都重新计算。此外,如果您最初对列表进行排序,您可以根据以下事实进行优化:如果在列表中添加数字 k 会使您超过目标,那么添加 k+1 也会使您超过目标。

希望这会给你一个足够好的开始。不幸的是,我的 perl 生锈了。

虽然这是一个蛮力算法,但其中有一些捷径,所以它永远不会那么高效。

【讨论】:

    【解决方案3】:

    您可以使用Data::PowerSet 模块来生成元素列表的所有子集:

    【讨论】:

    • 谢谢,这很有帮助,但它有点太暴力了。我的集合很小,但不会小到 powerset 通常会很小。我想我需要做一些递归,当所有可能的未来总和都太大时停止。
    【解决方案4】:

    使用Algorithm::Combinatorics。这样,您可以提前决定要考虑的子集大小,并将内存使用降至最低。应用一些启发式方法以尽早返回。

    #!/usr/bin/perl
    
    use strict; use warnings;
    use List::Util qw( sum );
    use Algorithm::Combinatorics qw( combinations );
    
    my @x = (1 .. 10);
    my $target_sum = 12;
    
    {
        use integer;
        for my $n ( 1 .. @x ) {
            my $iter = combinations(\@x, $n);
            while ( my $set = $iter->next ) {
                print "@$set\n" if $target_sum == sum @$set;
            }
        }
    }
    

    数字确实增长得相当快:需要数千天才能遍历 40 个元素集的所有子集。因此,您应该决定子集的有趣大小。

    【讨论】:

    • 这种方法的问题是当总和变得太大时它没有办法短路。我的算法可以在不到一秒的时间内在 Core2 Duo E6750 上执行 Solve(44, [1 .. 40])(40 个元素集)。
    【解决方案5】:

    这是一个“为我做作业”的问题吗?

    要确定地做到这一点,需要一个 N 阶算法! (即 (N-0) * (N-1) * (N-2)...) 对于大量输入,这将非常慢。但该算法非常简单:计算集合中每个可能的输入序列,并尝试将序列中的输入相加。如果总和在任何时候匹配,您已经得到了一个答案,保存结果并继续下一个序列。如果在任何时候总和大于目标,则放弃当前序列并继续下一个。

    您可以通过删除任何大于目标的输入来稍微优化这一点。另一种优化方法是取序列中的第一个输入 I 并创建一个新序列 S1,从目标 T 中减去 I 以获得新目标 T1,然后检查 T 是否存在于 S1 中,如果存在则你'找到匹配项,否则对 S1 和 T1 重复该过程。订单还是N!不过。

    如果您需要处理大量数字,那么我建议您阅读遗传算法。

    C.

    【讨论】:

    • 不是我的家庭作业问题。一个实际的现实世界问题。我从事金融研究,并不是一个程序员。但我想谈话很便宜,所以我很可能在撒谎。
    【解决方案6】:

    不久前有人发布了一个类似的问题,另一个人展示了一个巧妙的 shell 技巧来回答它。这是一种 shell 技术,但我认为它不像我之前看到的那样是一种简洁的解决方案(所以我不相信这种方法)。它很可爱,因为它利用了 shell 扩展:

    for i in 0{,+2}{,+3}{,+4}{,+7}; do
      y=$(( $i )); # evaluate expression
      if [ $y -eq 7 ]; then
        echo $i = $y;
      fi;
    done
    

    输出:

    0+7 = 7
    0+3+4 = 7
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-12-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-04-03
      相关资源
      最近更新 更多