找出所有可能的数字组合以达到给定的总和
您将如何测试给定N
组数字中所有可能的加法组合,以便它们相加达到给定的最终数字?
一个简短的示例:
- 要添加的一组数字:
N = {1,5,22,15,0,...}
- 所需结果:
12345
How would you go about testing all possible combinations of additions from a given set N
of numbers so they add up to a given final number?
A brief example:
- Set of numbers to add:
N = {1,5,22,15,0,...}
- Desired result:
12345
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(30)
这个问题可以通过所有可能的总和的递归组合过滤掉达到目标的总和来解决。下面是 Python 中的算法:
这种类型的算法在下面的斯坦福抽象编程讲座<中得到了很好的解释。 /a> - 非常推荐此视频来了解递归如何生成解决方案的排列。
编辑
以上作为生成器函数,使其更有用一点。由于
yield from
,需要 Python 3.3+。这是相同算法的 Java 版本:
它是完全相同的启发式算法。我的Java有点生疏,但我认为很容易理解。
Java 解决方案的 C# 转换: (作者:@JeremyThompson)
Ruby 解决方案: (作者:@emaillenin)
编辑:复杂性讨论
正如其他人提到的,这是一个 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
可以过滤掉许多组合。如果
N
和Target
是大数,则应该采用解决方案的近似版本。This problem can be solved with a recursive combinations of all possible sums filtering out those that reach the target. Here is the algorithm in Python:
This type of algorithms are very well explained in the following Stanford's Abstract Programming lecture - this video is very recommendable to understand how recursion works to generate permutations of solutions.
Edit
The above as a generator function, making it a bit more useful. Requires Python 3.3+ because of
yield from
.Here is the Java version of the same algorithm:
It is exactly the same heuristic. My Java is a bit rusty but I think is easy to understand.
C# conversion of Java solution: (by @JeremyThompson)
Ruby solution: (by @emaillenin)
Edit: complexity discussion
As others mention this is an NP-hard problem. It can be solved in exponential time O(2^n), for instance for n=10 there will be 1024 possible solutions. If the targets you are trying to reach are in a low range then this algorithm works. So for instance:
subset_sum([1,2,3,4,5,6,7,8,9,10],100000)
generates 1024 branches because the target never gets to filter out possible solutions.On the other hand
subset_sum([1,2,3,4,5,6,7,8,9,10],10)
generates only 175 branches, because the target to reach10
gets to filter out many combinations.If
N
andTarget
are big numbers one should move into an approximate version of the solution.这个问题的解决方案已经在互联网上给出了一百万次。该问题称为硬币兑换问题。人们可以在http://rosettacode.org/wiki/Count_the_coins找到解决方案,并在< a href="http://jaqm.ro/issues/volume-5,issue-2/pdfs/patterson_harmel.pdf" rel="noreferrer">http://jaqm.ro/issues/volume-5,issue- 2/pdfs/patterson_harmel.pdf(或 Google硬币找零问题)。
顺便说一句,Tsagadai 的 Scala 解决方案很有趣。此示例生成 1 或 0。作为副作用,它会在控制台上列出所有可能的解决方案。它显示了解决方案,但无法使其以任何方式可用。
为了尽可能有用,代码应该返回一个
List[List[Int]]
,以便获取解决方案的数量(列表列表的长度)、“最佳”解决方案(最短的列表),或所有可能的解决方案。这是一个例子。虽然效率很低,但是很容易理解。
运行时,它显示:
sumCombinations()
函数可以单独使用,并且可以进一步分析结果以显示“最佳”解决方案(最短列表),或解决方案的数量(列表的数量)。请注意,即使这样,也可能无法完全满足要求。解决方案中每个列表的顺序可能很重要。在这种情况下,每个列表都必须重复其元素组合的次数。或者我们可能只对不同的组合感兴趣。
例如,我们可能认为
List(5, 10)
应该给出两种组合:List(5, 10)
和List(10, 5)
代码>.对于List(5, 5, 5)
,它可以给出三种组合或仅一种组合,具体取决于要求。对于整数来说,这三种排列是等价的,但如果我们处理硬币,就像“硬币兑换问题”一样,它们就不是等价的。要求中还没有说明每个数字(或硬币)是否只能使用一次或多次的问题。我们可以(而且我们应该!)将问题概括为每个数字出现的列表。这在现实生活中转化为“用一组硬币(而不是一组硬币价值)赚取一定数量的钱的可能方法是什么”。最初的问题只是这个问题的一个特殊情况,其中我们根据需要使每种硬币出现尽可能多的出现次数,以得出每种硬币价值的总金额。
The solution of this problem has been given a million times on the Internet. The problem is called The coin changing problem. One can find solutions at http://rosettacode.org/wiki/Count_the_coins and mathematical model of it at http://jaqm.ro/issues/volume-5,issue-2/pdfs/patterson_harmel.pdf (or Google coin change problem).
By the way, the Scala solution by Tsagadai, is interesting. This example produces either 1 or 0. As a side effect, it lists on the console all possible solutions. It displays the solution, but fails making it usable in any way.
To be as useful as possible, the code should return a
List[List[Int]]
in order to allow getting the number of solution (length of the list of lists), the "best" solution (the shortest list), or all the possible solutions.Here is an example. It is very inefficient, but it is easy to understand.
When run, it displays:
The
sumCombinations()
function may be used by itself, and the result may be further analyzed to display the "best" solution (the shortest list), or the number of solutions (the number of lists).Note that even like this, the requirements may not be fully satisfied. It might happen that the order of each list in the solution be significant. In such a case, each list would have to be duplicated as many time as there are combination of its elements. Or we might be interested only in the combinations that are different.
For example, we might consider that
List(5, 10)
should give two combinations:List(5, 10)
andList(10, 5)
. ForList(5, 5, 5)
it could give three combinations or one only, depending on the requirements. For integers, the three permutations are equivalent, but if we are dealing with coins, like in the "coin changing problem", they are not.Also not stated in the requirements is the question of whether each number (or coin) may be used only once or many times. We could (and we should!) generalize the problem to a list of lists of occurrences of each number. This translates in real life into "what are the possible ways to make an certain amount of money with a set of coins (and not a set of coin values)". The original problem is just a particular case of this one, where we have as many occurrences of each coin as needed to make the total amount with each single coin value.
JavaScript 版本:
A Javascript version:
在 Haskell 中:
和 J< /a>:
正如您可能注意到的,两者都采用相同的方法并将问题分为两部分:生成幂集的每个成员,并检查每个成员与目标的总和。
还有其他解决方案,但这是最简单的。
您是否需要其中任何一种方法的帮助,或者寻找不同的方法?
In Haskell:
And J:
As you may notice, both take the same approach and divide the problem into two parts: generate each member of the power set, and check each member's sum to the target.
There are other solutions but this is the most straightforward.
Do you need help with either one, or finding a different approach?
到目前为止,解决方案有很多,但都是“生成然后过滤”的形式。这意味着他们可能会花费大量时间在无法得出解决方案的递归路径上。
这是一个
O(size_of_array * (number_of_sums + number_of_solutions))
的解决方案。换句话说,它使用动态编程来避免枚举永远不会匹配的可能解决方案。为了咯咯地笑,我用正数和负数进行了这项工作,并将其设为迭代器。它将适用于 Python 2.3+。
这是一个与数组和目标一起使用的示例,其中其他解决方案中使用的过滤方法实际上永远不会完成。
这将在 2 秒内打印所有 522 个答案。以前的方法如果能在宇宙当前的生命周期中找到任何答案就很幸运了。 (整个空间有
2^168 = 3.74144419156711e+50
种可能的组合来运行。这......需要一段时间。)解释
我被要求解释代码,但解释数据结构通常更能说明问题。所以我将解释数据结构。
让我们考虑一下 subset_sum_iter([-2, 2, -3, 3, -5, 5, -7, 7, -11, 11], 10)。
在检查点 A,我们意识到我们的目标是正数,因此
sign = 1
。我们对输入进行了排序,使得 array = [-11, -7, -5, -3, -2, 2, 3, 5, 7, 11]。由于我们最终通过索引访问它很多,这里是从索引到值的映射:通过检查点 B,我们使用了 动态编程生成我们的
last_index
数据结构。它包含什么?(旁注,它不是对称的,因为条件
if 0 < (new_s - target) * sign
阻止我们记录超过target
的任何内容,在我们的例子中是 10 .)这是什么意思?好吧,输入
10: [7, 8, 9]
。这意味着我们最终的总和可以是10
,最后选择的数字位于索引 7、8 或 9 处。也就是说,最后选择的数字可以是 5、7 或 11。让我们仔细看看如果我们选择索引 7 会发生什么。这意味着我们以 5 结束。因此,在我们到达索引 7 之前,我们必须得到 10-5 = 5。5 的条目为:
5:[6,7,8,9]
。所以我们可以选择索引 6,即 3。虽然我们在索引 7、8 和 9 处到达 5,但在索引 7 之前我们没有到达那里。所以我们倒数第二个选择必须是索引 6 处的 3现在我们必须在索引 6 之前到达 5-3 = 2。条目 2 为:
2: [5, 6, 7, 8, 9]
。同样,我们只关心索引5
处的答案,因为其他答案发生得太晚了。因此,倒数第三个选择必须是索引 5 处的 2。最后,我们必须在索引 5 之前达到 2-2 = 0。条目 0 为:
0: [-1, 5, 6, 7,8,9]
。同样,我们只关心-1
。但-1
不是索引 - 事实上我用它来表示我们已经完成选择。所以我们刚刚找到了解决方案
2+3+5 = 10
。这是我们打印出来的第一个解决方案。现在我们进入
recur
子函数。因为它是在我们的主函数内部定义的,所以它可以看到last_index
。首先要注意的是,它调用的是
yield
,而不是return
。这使其成为一个生成器。当你调用它时,你会返回一种特殊的迭代器。当您循环该迭代器时,您将获得它可以产生的所有内容的列表。但是当它生成它们时你就会得到它们。如果列表很长,则不会将其放入内存中。 (这很重要,因为我们可以得到一个很长的列表。)recur(new_target, max_i)
将产生的结果是您可以使用new_target
总结的所有方法仅具有最大索引max_i
的array
元素。这就是它的答案:“我们必须在索引max_i+1
之前到达new_target
。”当然,它是递归的。因此,recur(target, len(array)) 是使用任何索引达到目标的所有解决方案。这就是我们想要的。
There are a lot of solutions so far, but all are of the form generate then filter. Which means that they potentially spend a lot of time working on recursive paths that do not lead to a solution.
Here is a solution that is
O(size_of_array * (number_of_sums + number_of_solutions))
. In other words it uses dynamic programming to avoid enumerating possible solutions that will never match.For giggles and grins I made this work with numbers that are both positive and negative, and made it an iterator. It will work for Python 2.3+.
And here is an example of it being used with an array and target where the filtering approach used in other solutions would effectively never finish.
This prints all 522 answers in under 2 seconds. The previous approaches would be lucky to find any answers in the current lifetime of the universe. (The full space has
2^168 = 3.74144419156711e+50
possible combinations to run through. That...takes a while.)Explanation
I was asked to explain the code, but explaining data structures is usually more revealing. So I'll explain the data structures.
Let's consider
subset_sum_iter([-2, 2, -3, 3, -5, 5, -7, 7, -11, 11], 10)
.At checkpoint A, we have realized that our target is positive so
sign = 1
. And we've sorted our input so thatarray = [-11, -7, -5, -3, -2, 2, 3, 5, 7, 11]
. Since we wind up accessing it by index a lot, here the the map from indexes to values:By checkpoint B we have used Dynamic Programming to generate our
last_index
data structure. What does it contain?(Side note, it is not symmetric because the condition
if 0 < (new_s - target) * sign
stops us from recording anything pasttarget
, which in our case was 10.)What does this mean? Well, take the entry,
10: [7, 8, 9]
. It means that we can wind up at a final sum of10
with the last number chosen being at indexes 7, 8, or 9. Namely the last number chosen could be 5, 7, or 11.Let's take a closer look at what happens if we choose index 7. That means we end on a 5. So therefore before we came to index 7, we had to get to 10-5 = 5. And the entry for 5 reads,
5: [6, 7, 8, 9]
. So we could have picked index 6, which is 3. While we get to 5 at indexes 7, 8, and 9, we didn't get there before index 7. So our second to last choice has to be the 3 at index 6.And now we have to get to 5-3 = 2 before index 6. The entry 2 reads:
2: [5, 6, 7, 8, 9]
. Again, we only care about the answer at index5
because the others happened too late. So the third to last choice is has to be the 2 at index 5.And finally we have to get to 2-2 = 0 before index 5. The entry 0 reads:
0: [-1, 5, 6, 7, 8, 9]
. Again we only care about the-1
. But-1
isn't an index - in fact I'm using it to signal we're done choosing.So we just found the solution
2+3+5 = 10
. Which is the very first solution we print out.And now we get to the
recur
subfunction. Because it is defined inside of our main function, it can seelast_index
.The first thing to note is that it calls
yield
, notreturn
. This makes it into a generator. When you call it you return a special kind of iterator. When you loop over that iterator, you'll get a list of all of the things it can yield. But you get them as it generates them. If it is a long list, you don't put it in memory. (Kind of important because we could get a long list.)What
recur(new_target, max_i)
will yield are all of the ways that you could have summed up tonew_target
using only elements ofarray
with maximum indexmax_i
. That is it answers: "We have to get tonew_target
before indexmax_i+1
." It is, of course, recursive.Therefore
recur(target, len(array))
is all solutions that reach target using any index at all. Which is what we want.相同算法的 C++ 版本
C++ version of the same algorithm
@msalvadores 代码答案的 C# 版本
C# version of @msalvadores code answer
Java 非递归版本,只是不断添加元素并将它们重新分配给可能的值。
0
会被忽略,适用于固定列表(给你的就是你可以玩的)或可重复数字的列表。输入示例:
输出示例:
Java non-recursive version that simply keeps adding elements and redistributing them amongst possible values.
0
's are ignored and works for fixed lists (what you're given is what you can play with) or a list of repeatable numbers.Sample input:
Sample output:
我已将上述逻辑从 python 转换为 php..
i have converted above logic from python to php..
另一个 python 解决方案是使用 itertools.combinations 模块,如下所示:
输出:
[(8, 7), (5, 10), (3, 8, 4), (3 , 5, 7)]
Another python solution would be to use the
itertools.combinations
module as follows:Output:
[(8, 7), (5, 10), (3, 8, 4), (3, 5, 7)]
我想我会使用这个问题的答案,但我不能,所以这是我的答案。它使用 计算机程序的结构和解释。我认为这是一个更好的递归解决方案,应该更能让纯粹主义者满意。
我的答案是 Scala(如果我的 Scala 很糟糕,我很抱歉,我刚刚开始学习它)。 findSumCombinations 的疯狂之处在于对递归的原始列表进行排序和唯一以防止重复。
使用方法:
I thought I'd use an answer from this question but I couldn't, so here is my answer. It is using a modified version of an answer in Structure and Interpretation of Computer Programs. I think this is a better recursive solution and should please the purists more.
My answer is in Scala (and apologies if my Scala sucks, I've just started learning it). The findSumCombinations craziness is to sort and unique the original list for the recursion to prevent dupes.
To use it:
Excel VBA 版本如下。我需要在 VBA 中实现这一点(不是我的偏好,请不要评判我!),并使用本页上的答案来实现该方法。我正在上传以防其他人也需要 VBA 版本。
输出(写入立即窗口)应该是:
Excel VBA version below. I needed to implement this in VBA (not my preference, don't judge me!), and used the answers on this page for the approach. I'm uploading in case others also need a VBA version.
Output (written to the Immediate window) should be:
这是 R 中的解决方案
Here's a solution in R
Perl 版本(主要答案):
结果:
Javascript 版本:
实际上返回结果(而不是打印它)的 JavaScript 一行代码:
还有我最喜欢的带有回调的单行:
Perl version (of the leading answer):
Result:
Javascript version:
Javascript one-liner that actually returns results (instead of printing it):
And my favorite, one-liner with callback:
这是一个 Java 版本,非常适合小 N 和非常大的目标总和,当复杂度 O(t*N)(动态解)大于指数算法时。我的版本使用中间相遇攻击,并进行了一点转换,以降低复杂性,从经典的简单
O(n*2^n)
到O(2^( n/2))
。如果您想将其用于包含 32 到 64 个元素的集合,则应将表示步骤函数中当前子集的
int
更改为long
,尽管性能显然会下降随着集合大小的增加而急剧减少。如果要将其用于包含奇数个元素的集合,则应向该集合添加 0 以使其编号为偶数。Here is a Java version which is well suited for small N and very large target sum, when complexity
O(t*N)
(the dynamic solution) is greater than the exponential algorithm. My version uses a meet in the middle attack, along with a little bit shifting in order to reduce the complexity from the classic naiveO(n*2^n)
toO(2^(n/2))
.If you want to use this for sets with between 32 and 64 elements, you should change the
int
which represents the current subset in the step function to along
although performance will obviously drastically decrease as the set size increases. If you want to use this for a set with odd number of elements, you should add a 0 to the set to make it even numbered.使用我几年前用 C++ 编写的表的非常有效的算法。
如果设置 PRINT 1 它将打印所有组合(但不会使用有效的方法)。
它非常高效,可以在不到 10 毫秒的时间内计算出超过 10^14 个组合。
Very efficient algorithm using tables i wrote in c++ couple a years ago.
If you set PRINT 1 it will print all combinations(but it wont be use the efficient method).
Its so efficient that it calculate more than 10^14 combinations in less than 10ms.
这类似于硬币找零问题
This is similar to a coin change problem
我将 C# 示例移植到 Objective-c,但在响应中没有看到它:
I ported the C# sample to Objective-c and didn't see it in the responses:
这是一个更好的版本,具有更好的输出格式和 C++ 11 功能:
Here is a better version with better output formatting and C++ 11 features:
首先推导0。零是加法的一个恒等式,因此在这种特殊情况下,根据幺半群定律,零是没有用的。如果你想上升到正数,也可以推导负数。否则还需要减法运算。
所以...在这个特定的工作中你可以得到的最快的算法如下 JS 中给出的。
这是一个非常快的算法,但如果您对
data
数组进行降序排序,它会更快。使用.sort()
是无关紧要的,因为算法最终会减少递归调用次数。Deduce 0 in the first place. Zero is an identiy for addition so it is useless by the monoid laws in this particular case. Also deduce negative numbers as well if you want to climb up to a positive number. Otherwise you would also need subtraction operation.
So... the fastest algorithm you can get on this particular job is as follows given in JS.
This is a very fast algorithm but if you sort the
data
array descending it will be even faster. Using.sort()
is insignificant since the algorithm will end up with much less recursive invocations.PHP 版本,受到 Keith Beller 的 C# 版本的启发。
bala 的 PHP 版本不适合我,因为我不需要对数字进行分组。我想要一个更简单的实现,具有一个目标值和一组数字。此功能还将删除任何重复的条目。
编辑 2021 年 10 月 25 日:添加了精度参数以支持浮点数(现在需要 bcmath 扩展)。
示例:
这将返回一个包含两个数字组合数组的索引数组:
浮点数示例:
这将返回单个匹配项:
PHP Version, as inspired by Keith Beller's C# version.
bala's PHP version did not work for me, because I did not need to group numbers. I wanted a simpler implementation with one target value, and a pool of numbers. This function will also prune any duplicate entries.
Edit 25/10/2021: Added the precision argument to support floating point numbers (now requires the bcmath extension).
Example:
This will return an indexed array with two number combination arrays:
Example with floating point numbers:
This will return a single match:
使用 Excel 查找组合 - (相当简单)。
(您的计算机速度不能太慢)
下载“目标总和”Excel 文件。
按照网站页面上的说明进行操作。
希望这有帮助。
To find the combinations using excel - (its fairly easy).
(You computer must not be too slow)
Download the "Sum to Target" excel file.
Follow the directions on the website page.
hope this helps.
Swift 3 转换 Java 解决方案:(by @JeremyThompson)
用法:
Swift 3 conversion of Java solution: (by @JeremyThompson)
usage:
这也可以用来打印所有答案,
时间复杂度是指数级的。 2^n 的阶数
This can be used to print all the answers as well
Time Complexity is exponential. Order of 2^n
我正在为 scala 作业做类似的事情。想在这里发布我的解决方案:
I was doing something similar for a scala assignment. Thought of posting my solution here:
@KeithBeller 的答案稍微改变了变量名称和一些评论。
@KeithBeller's answer with slightly changed variable names and some comments.
推荐作为答案:
这是使用 es2015 生成器的解决方案:
使用生成器实际上非常有用,因为它允许您在找到有效子集后立即暂停脚本执行。这与没有生成器(即缺少状态)的解决方案形成鲜明对比,后者必须迭代
数字
的每个子集Recommended as an answer:
Here's a solution using es2015 generators:
Using generators can actually be very useful because it allows you to pause script execution immediately upon finding a valid subset. This is in contrast to solutions without generators (ie lacking state) which have to iterate through every single subset of
numbers
我不喜欢上面看到的 Javascript 解决方案。这是我使用部分应用、闭包和递归构建的一个:
好吧,我主要关心的是,如果组合数组能够满足目标要求,希望这种方法您将开始找到其余的组合
这里只需设置目标并传递组合数组。
我目前提出的实现
I did not like the Javascript Solution I saw above. Here is the one I build using partial applying, closures and recursion:
Ok, I was mainly concern about, if the combinations array could satisfy the target requirement, hopefully this approached you will start to find the rest of combinations
Here just set the target and pass the combinations array.
the currently implementation I came up with
针对此问题的一种迭代 C++ 堆栈解决方案。与其他一些迭代解决方案不同,它不会生成不必要的中间序列副本。
输出
print_all_sum(6)
:An iterative C++ stack solution for a flavor of this problem. Unlike some other iterative solutions, it doesn't make unnecessary copies of intermediate sequences.
Output
print_all_sum(6)
:这是 JS 的动态解决方案,用于告诉任何人可以通过多少种方式获得一定的金额。如果您考虑时间和空间复杂性,这可能是正确的解决方案。
This is a Dynamic Solution for JS to tell how many ways anyone can get the certain sum. This can be the right solution if you think about time and space complexity.