【问题标题】:All possible combinations of a set that sum to a target value总和为目标值的集合的所有可能组合
【发布时间】:2019-11-07 20:28:24
【问题描述】:

我有一个输入向量,例如:

weights <- seq(0, 1, by = 0.2)

我想生成所有权重组合(允许重复),使得总和等于 1。 我想出了

l <- rep(list(weights), 10)
combinations <- expand.grid(l)
combinations[which(apply(combinations, 1, sum) == 1),]

问题当然是我生成了更多我需要的组合。有没有办法更有效地完成它?

编辑: 感谢您的回答。这是问题的第一部分。正如@Frank 指出的那样,现在我拥有所有加起来为 1 的“解决方案”,问题是从长度为 10 的向量中的解决方案中获取所有排列(不确定它是否是正确的词)。对于实例:

s1 <- c(0, 0, 0.2, 0, 0, 0, 0.8, 0, 0, 0)
s2 <- c(0.8, 0, 0, 0, 0, 0, 0, 0, 0.2, 0)
etc...

【问题讨论】:

标签: r


【解决方案1】:

找到一组整数的任何子集,总和为某个目标tsubset sum problem 的一种形式,它是 NP 完全的。因此,有效计算集合中总和为目标值的所有组合(允许重复)在理论上具有挑战性。

为了轻松解决子集和问题的特殊情况,让我们通过假设输入是正整数来重铸您的问题(例如w &lt;- c(2, 4, 6, 8, 10);我不会在此答案中考虑非正整数或非整数) 并且目标也是一个正整数(在您的示例 10 中)。将D(i, j) 定义为在集合w 的第一个j 元素中总和为i 的所有组合的集合。如果w中有n元素,那么你对D(t, n)感兴趣。

让我们从几个基本情况开始:D(0, k) = {{}} 代表所有 k &gt;= 0(求和为 0 的唯一方法是不包含任何元素)和 D(k, 0) = {} 代表任何 k &gt; 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,]

【讨论】:

    【解决方案2】:

    看看partitions库,

    library(partitions)
    ps <- parts(10)
    res <- ps[,apply(ps, 2, function(x) all(x[x>0] %% 2 == 0))] / 10
    

    【讨论】:

      【解决方案3】:

      如果你想使用base R,这里有一段我​​为这个问题想出的漂亮的递归代码;它以列表形式返回结果,因此不是特定问题的完整答案。

      combnToSum = function(target, values, collapse = T) {
      
        if(any(values<=0)) stop("All values must be positive numbers.")
      
        appendValue = function(root) {
          if(sum(root) == target) return(list(root))
      
          candidates = values + sum(root) <= target
          if(length(root)>0 & collapse) candidates = candidates & values >= root[1]
      
          if(!any(candidates)) return(NULL)
      
          roots = lapply(values[candidates], c, root)
          return(unlist(lapply(roots, addValue), recursive = F))
        }
      
        appendValue(integer(0))
      }
      

      代码相当高效,一眨眼就解决了测试问题。

      combnToSum(1, c(.2,.4,.6,.8,1))
      # [[1]]
      # [1] 0.2 0.2 0.2 0.2 0.2
      #
      # [[2]]
      # [1] 0.4 0.2 0.2 0.2
      #
      # [[3]]
      # [1] 0.6 0.2 0.2
      #
      # [[4]]
      # [1] 0.4 0.4 0.2
      #
      # [[5]]
      # [1] 0.8 0.2
      #
      # [[6]]
      # [1] 0.6 0.4
      #
      # [[7]]
      # [1] 1
      

      values 包含相对于target 小的数字时,可能会发生错误。例如,试图找到所有以 10 美元进行零钱的方法:

      combnToSum(1000, c(1, 5, 10, 25))
      

      产生以下错误

      # enter code here`Error: evaluation nested too deeply: infinite recursion / options(expressions=)?
      

      我将appendValue 作为嵌套在combnToSum 范围内的函数,这样targetvalues 就不必为每次调用复制和传递(在内部,在R 中)。我也喜欢漂亮干净的签名combnToSum(target, values);用户不需要知道中间值root

      也就是说,appendValue 可以是带有签名appendValue(target, values, root) 的单独函数,在这种情况下,您可以使用appendValue(1, c(0.2, 0.4, 0.6, 0.8, 1), integer(0)) 来获得相同的答案。但是你要么失去对负值的错误检查,要么如果你把错误检查放入appendValue,每次递归调用函数都会发生错误检查,这似乎有点低效。

      设置collapse = F 将返回所有具有唯一顺序的排列。

      combnToSum(1, c(.2,.4,.6,.8,1), collapse = F)
      # [[1]]
      # [1] 0.2 0.2 0.2 0.2 0.2
      # 
      # [[2]]
      # [1] 0.4 0.2 0.2 0.2
      # 
      # [[3]]
      # [1] 0.2 0.4 0.2 0.2
      # 
      # [[4]]
      # [1] 0.6 0.2 0.2
      # 
      # [[5]]
      # [1] 0.2 0.2 0.4 0.2
      # 
      # [[6]]
      # [1] 0.4 0.4 0.2
      # 
      # [[7]]
      # [1] 0.2 0.6 0.2
      # 
      # [[8]]
      # [1] 0.8 0.2
      # 
      # [[9]]
      # [1] 0.2 0.2 0.2 0.4
      # 
      # [[10]]
      # [1] 0.4 0.2 0.4
      # 
      # [[11]]
      # [1] 0.2 0.4 0.4
      # 
      # [[12]]
      # [1] 0.6 0.4
      # 
      # [[13]]
      # [1] 0.2 0.2 0.6
      # 
      # [[14]]
      # [1] 0.4 0.6
      # 
      # [[15]]
      # [1] 0.2 0.8
      # 
      # [[16]]
      # [1] 1
      

      【讨论】:

        【解决方案4】:

        如果您打算仅使用base R 来实现它,那么另一种方法是递归。

        假设x &lt;- c(1,2,4,8)s &lt;- 9 表示目标总和,那么以下函数可以让您到达那里:

        f <- function(s, x, xhead = head(x,1), r = c()) {
          if (s == 0) {
            return(list(r))
          } else {
            x <- sort(x,decreasing = T)
            return(unlist(lapply(x[x<=min(xhead,s)], function(k) f(round(s-k,10), x[x<= round(s-k,10)], min(k,head(x[x<=round(s-k,10)],1)), c(r,k))),recursive = F)) 
          }
        }
        

        f(s,x) 给出的:

        [[1]]
        [1] 8 1
        
        [[2]]
        [1] 4 4 1
        
        [[3]]
        [1] 4 2 2 1
        
        [[4]]
        [1] 4 2 1 1 1
        
        [[5]]
        [1] 4 1 1 1 1 1
        
        [[6]]
        [1] 2 2 2 2 1
        
        [[7]]
        [1] 2 2 2 1 1 1
        
        [[8]]
        [1] 2 2 1 1 1 1 1
        
        [[9]]
        [1] 2 1 1 1 1 1 1 1
        
        [[10]]
        [1] 1 1 1 1 1 1 1 1 1
        

        注意round(*,digits=10) 用于处理浮点数,其中digits 应适应输入的小数。

        【讨论】:

          【解决方案5】:

          对于组合,你想要这个吗:

          combinations <- lapply(seq_along(weights), function(x) combn(weights, x))
          

          然后是总和:

          sums <- lapply(combinations, colSums)
          inds <- lapply(sums, function(x) which(x == 1))
          lapply(seq_along(inds), function(x) combinations[[x]][, inds[[x]]])
          

          【讨论】:

          • 我认为你误解了这个问题。您可以多次使用 weights 中的值(如其他两个答案所示)。
          猜你喜欢
          • 2012-03-27
          • 1970-01-01
          • 1970-01-01
          • 2013-05-09
          • 1970-01-01
          • 2020-08-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多