找到一组整数的任何子集,总和为某个目标t 是subset sum problem 的一种形式,它是 NP 完全的。因此,有效计算集合中总和为目标值的所有组合(允许重复)在理论上具有挑战性。
为了轻松解决子集和问题的特殊情况,让我们通过假设输入是正整数来重铸您的问题(例如w <- c(2, 4, 6, 8, 10);我不会在此答案中考虑非正整数或非整数) 并且目标也是一个正整数(在您的示例 10 中)。将D(i, j) 定义为在集合w 的第一个j 元素中总和为i 的所有组合的集合。如果w中有n元素,那么你对D(t, n)感兴趣。
让我们从几个基本情况开始:D(0, k) = {{}} 代表所有 k >= 0(求和为 0 的唯一方法是不包含任何元素)和 D(k, 0) = {} 代表任何 k > 0(你不能求和为零元素的正数)。现在考虑以下伪代码来计算任意 D(i, j) 值:
for j = 1 ... n
for i = 1 ... t
D[(i, j)] = {}
for rep = 0 ... floor(i/w_j)
Dnew = D[(i-rep*w_j, j-1)], with w_j added "rep" times
D[(i, j)] = Union(D[(i, j)], Dnew)
请注意,这仍然可能非常低效(D(t, n) 可以包含指数级数量的可行子集,因此无法避免这种情况),但在许多情况下,总和为target 这可能比简单地考虑集合的每个子集要快得多(有2^n 这样的子集,因此该方法总是具有指数运行时间)。
让我们使用 R 来编写您的示例:
w <- c(2, 4, 6, 8, 10)
n <- length(w)
t <- 10
D <- list()
for (j in 0:n) D[[paste(0, j)]] <- list(c())
for (i in 1:t) D[[paste(i, 0)]] <- list()
for (j in 1:n) {
for (i in 1:t) {
D[[paste(i, j)]] <- do.call(c, lapply(0:floor(i/w[j]), function(r) {
lapply(D[[paste(i-r*w[j], j-1)]], function(x) c(x, rep(w[j], r)))
}))
}
}
D[[paste(t, n)]]
# [[1]]
# [1] 2 2 2 2 2
#
# [[2]]
# [1] 2 2 2 4
#
# [[3]]
# [1] 2 4 4
#
# [[4]]
# [1] 2 2 6
#
# [[5]]
# [1] 4 6
#
# [[6]]
# [1] 2 8
#
# [[7]]
# [1] 10
代码正确识别集合中总和为 10 的所有元素组合。
为了有效地获得所有 2002 唯一长度 10 组合,我们可以使用 multicool 包中的 allPerm 函数:
library(multicool)
out <- do.call(rbind, lapply(D[[paste(t, n)]], function(x) {
allPerm(initMC(c(x, rep(0, 10-length(x)))))
}))
dim(out)
# [1] 2002 10
head(out)
# [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
# [1,] 2 2 2 2 2 0 0 0 0 0
# [2,] 0 2 2 2 2 2 0 0 0 0
# [3,] 2 0 2 2 2 2 0 0 0 0
# [4,] 2 2 0 2 2 2 0 0 0 0
# [5,] 2 2 2 0 2 2 0 0 0 0
# [6,] 2 2 2 2 0 2 0 0 0 0
对于给定的输入,整个操作非常快(在我的计算机上为 0.03 秒)并且不使用大量内存。同时,原始帖子中的解决方案在 22 秒内运行并使用了 15 GB 内存,即使将最后一行替换为(更)高效的combinations[rowSums(combinations) == 1,]。