等于或超过给定数字的所有组合

问题描述

它不同于硬币兑换问题link

给定一个字典 x=[a:1,b:2,c:1,d:3,e:2],我需要计算所有可能的键组合,使得与这些键关联的值的总和刚好超过或等于给定的值,比如 5。刚超过 5 意味着如果被选中的元素小于5,即使超过5,我们也可以再添加一个元素,但一旦超过或等于5,就不能再添加元素了。

有两个问题使我的方法复杂化 -

  1. 请注意,有些值是重复的——例如1 和 2 各出现两次,需要分别考虑。
  2. 在这种情况下,订购很重要。

订购也很重要,因此 [b,c,d],[b,d,c],[c,b,b],[d,b] 都单独包含在内。理想情况下,只有当我们超过 5 并且总和正好是 5 时,排序才有意义。

因此上述问题的一些解决方案是 [a,d] 和 7、[a,e] 和 6、[b,d] 和 6、[d,e] 和 5 等。

我该如何解决这个问题。我正在考虑使用硬币兑换的输出,然后在每个结果列表中添加一个元素,但这不会涵盖所有情况。

EDIT 1:构建问题的更好方法可能是找到所有组合,使得总和 (i) 等于 5 或​​ (ii) 作为总和,直到倒数第二个元素小于 5,因此添加最后一个元素使其大于 5。一旦我们有 5 或 >5,将不会添加其他元素。

EDIT 2:澄清排序问题。为了给出一些上下文,键是设备,值是它需要的资源。可用的总资源为 5。并且设备根据它们的键分配资源,即第一个键值对将首先得到服务(按键的字母顺序排序)。对于 b,它需要 2 个资源,但说只有 1 个资源可用 - 它将被分配 1 个,剩余的 1 个将在下一次运行(溢出)中分配。因此,当总和恰好为 5 时,顺序无关紧要,因为每个都得到它想要的任何东西,并且不会溢出到下一个插槽。例如。 edde 表示都得到了他们想要的东西,所以他们都应该被包括在内。 但是对于仅仅由于添加了最后一个元素而超过 5 的列表,排序很重要,因为溢出只会发生在最后一个元素上。例如。 dce 表示 d 和 c 分别得到 3 和 1,而 e 只得到 1(1 溢出到下一次运行)。类似地,ced 表示 c 和 d 分别得到 1 和 3,d 会发生溢出,因为它只被分配了 1。

解决方法

更新答案

有关要求的更多详细信息已添加到问题中。我认为这个版本符合他们。但是没有指定预期的输出结构,所以这是一个猜测。最终得到的结果是这样的:

{initial: {a: 1,b: 2,c: 1},last: {d: 3}}

或者像这样:

{initial: {b: 2,c: 1,e: 2}}

它首先使用函数 partitions 查找初始值的所有子集及其补码。 partitions (['a','b','c','d']) 将有 16 个元素,包括 [['a','d'],['c']][['b',['a','c']][[],'d']]

然后,对于每个分区,如果其左半部分的总和等于目标,我们将其包含在内。如果总和小于目标,我们会为右半部分的每个值都包含一个结果,使我们超过目标。

这是一个 Javascript 实现:

// utility functions
const last = (xs) => 
  xs [xs .length - 1]

const partitions = (xs) => 
  xs .length === 0
    ? [[[],[]]]
    : partitions (xs .slice (0,-1)) .flatMap (([p,q]) => [ 
        [[...p,last (xs)],q],[p,[...q,last (xs)]] 
      ])

// helper function
const total = (xs) =>
  xs .reduce ((a,[k,v]) => a + v,0)


// main function
const sumTo = (n,xs,parts = partitions (xs)) =>
  parts .flatMap (([incl,excl],_,__,t = total (incl)) => [
    ... (t == n ? [{initial: incl}] : []),... (t < n
           ? excl .filter (([k,v]) => v + t > n) 
                  .map (e => ({initial: incl,last: [e]}))
           : []
        )
  ])


// public function
const subsetSum = (n,dict) =>
  sumTo (n,Object.entries (dict))
    .map (({initial,last}) => ({
      initial: Object .fromEntries (initial),... (last ? {last: Object .fromEntries (last)} : {})
    }))


// sample data
const dict = {a: 1,d: 3,e: 2}


// demo
console .log (subsetSum (5,dict))
.as-console-wrapper {max-height: 100% !important; top: 0}

我们从一个简单的辅助函数 last 开始,它只是选择数组的最后一个元素。然后我们有 partition,上面已经描述过了。我们的主要函数是 sumTo,它完成所描述的工作。它的输入是目标数字和像 [['a','1],['b',2'],['c',1],['d',3],['e',2]] 这样的结构,它很容易从字典中派生,但更容易使用。它返回一个具有两个属性的对象,类似于 {initial: [['a',2],1]],last: [['d',3]]}。最后,我们有公共函数 subsetSum,它将字典格式转换为 sumTo 所需的输入,然后将结果转换回输出格式。这很容易修复以创建您需要的任何输出格式。

稍微格式化输出,我们可以更简单地看到我们的结果:

subsetSum  (5,dict)
  .map (({initial,last}) => 
      Object .keys (initial) .join ('') +
      (last ? '-' + Object .keys (last) .join ('') : '')
  )
//=> ["abc-d","abc-e","abe","ab-d","acd","ace-b",//    "ace-d","ad-b","ad-e","ae-d","bce","bc-d",//    "bd","be-d","cd-b","cd-e","ce-d","de"]

这可能表现不佳。我对问题可能的增长模式没有真正的了解。但是这个算法在字典的大小上是指数级的,这个技术没有办法绕过它,因为 partitions 核心的子集问题有 2 ^ n 个结果。

但这可能是与其他解决方案进行比较的起点。

原答案

如果所有的排序都很重要(而且我对那个要求的细节感到困惑),那么这个算法应该这样做,使用一个条目列表,每个条目都有一个 key 和一个 value

if (n <= 0) or if our list of entries is empty,return a list containing only the empty string
otherwise return the concatenation of the following lists,one for each entry:
  a recursive call passing 
    - `n - value`
    - the list excluding the current entry,with each result being prepended with the current entry's key 

如果您熟悉 Javascript 语法,那么这应该可行:

const excluding = (i) => (xs) =>
  [...xs .slice (0,i),...xs .slice (i + 1)]

const sumTo = (n,xs) =>
  n <= 0 || xs.length == 0 
    ? ['']
    : xs .flatMap (
        ([k,v],i) => sumTo (n - v,excluding (i) (xs)) .map (r => k + r)
      )
  
const subsetSum = (n,dict) => 
  sumTo (n,Object .entries (dict))

const dict = {a: 1,e: 2}

console .log (subsetSum (5,dict))
.as-console-wrapper {max-height: 100% !important; top: 0}

很难想象这个顺序可能完全无关紧要,因为 'cde' 应该适合,而 'dec' 不应该。但我不明白只有当总数超过目标时订单才相关的概念。你能解释得更全面吗?

,

我认为像这样改变硬币更换算法可以解决问题

import itertools
def subset_sum(numbers,target,partial=[]):
    exceed = sum([ v for k,v in partial[:-1]])
    s = sum([ v for k,v in partial])
    if exceed >= target:
        return
    elif s == target: 
        print (partial,sum([ v for k,v in partial]),target)
    elif s > target:
        print("print all permutation")
        p = list(itertools.permutations(partial))
        for state in p:
            legit = sum([v for k,v in state[:-1]])
            if (legit < target):
                print(state)

    for i in range(len(numbers)):
        k,v = numbers[i]
        remaining = numbers[i+1:]
        subset_sum(remaining,partial + [(k,v)])

if __name__ == "__main__":
    subset_sum([('a',1),('b',2),('c',('d',3),('e',2)],5)