问题描述
它不同于硬币兑换问题link。
给定一个字典 x=[a:1,b:2,c:1,d:3,e:2]
,我需要计算所有可能的键组合,使得与这些键关联的值的总和刚好超过或等于给定的值,比如 5。刚超过 5 意味着如果被选中的元素小于5,即使超过5,我们也可以再添加一个元素,但一旦超过或等于5,就不能再添加元素了。
有两个问题使我的方法复杂化 -
- 请注意,有些值是重复的——例如1 和 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 时,顺序无关紧要,因为每个都得到它想要的任何东西,并且不会溢出到下一个插槽。例如。 ed
和 de
表示都得到了他们想要的东西,所以他们都应该被包括在内。
但是对于仅仅由于添加了最后一个元素而超过 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)