多子集和计算

Posted

技术标签:

【中文标题】多子集和计算【英文标题】:Multiple subset sum calculation 【发布时间】:2017-07-14 08:35:05 【问题描述】:

我有 2 个集合,集合 A 包含一组随机数,集合 B 的元素是集合 A 的子集的总和。

例如,

A = [8, 9, 15, 15, 33, 36, 39, 45, 46, 60, 68, 73, 80, 92, 96]

B = [183, 36, 231, 128, 137]

我想用这样的数据找出哪个数字是哪个子集的总和。

S = [[45, 46, 92], [36], [8, 15, 39, 73, 96], [60, 68], [9, 15, 33, 80]]

我能够用 python 编写非常愚蠢的蛮力代码。

class SolvedException(BaseException):
    pass

def solve(sums, nums, answer):
    num = nums[-1]

    for i in range(0, len(sums)):
        sumi = sums[i]
        if sumi == 0:
            continue
        elif sumi - num < 0:
            continue
        answer[i].append(num)

        sums[i] = sumi - num

        if len(nums) != 1:
            solve(sums, nums[:-1], answer)
        elif sumi - num == 0:
            raise SolvedException(answer)

        sums[i] = sumi

        answer[i].pop()

try:
    solve(B, A, [list() for i in range(0, len(B))])
except SolvedException as e:
    print e.args[0]

这段代码非常适用于小数据,但计算我的数据(有 71 个数字和 10 个和)需要十亿年。

我可以使用一些更好的算法或优化。

对不起,我的英语不好和糟糕的低效代码。


编辑:抱歉,我意识到我没有准确描述问题。

由于A 中的每个元素都用于制作 B 的元素,sum(A) == sum(B)

另外,集合S 必须是集合A的分区。

【问题讨论】:

对我来说看起来像背包/背包问题。查一下 @Jean-FrançoisFabre 是的,你是对的,这是错误的代码。已修复,谢谢! 您是要查找产生给定目标总和的所有集合还是仅查找一个集合?另外,A 中的数字是否假定为非负数? 你现在明白了 :) 这里有很多可能性,你选择子集的标准是什么? 【参考方案1】:

这被称为子集和问题,它是一个众所周知的 NP 完全问题。所以基本上没有有效的解决方案。例如见https://en.wikipedia.org/wiki/Subset_sum_problem

但是如果你的数 N 不是太大,有一个伪多项式算法,使用动态规划: 您从左到右阅读列表 A 并保留可行且小于 N 的总和列表。如果您知道给定 A 可行的数字,您可以轻松获得 A + [a ]。因此,动态规划。对于您在此处给出的大小问题,它通常足够快。

这是一个 Python 快速解决方案:

def subsetsum(A, N):
    res = 0 : []
    for i in A:
        newres = dict(res)
        for v, l in res.items():
            if v+i < N:
                newres[v+i] = l+[i]
            elif v+i == N:
                return l+[i]
        res = newres
    return None

然后

>>> A = [8, 9, 15, 15, 33, 36, 39, 45, 46, 60, 68, 73, 80, 92, 96]
>>> subsetsum(A, 183)
[15, 15, 33, 36, 39, 45]

OP 编辑​​后:

现在我正确理解了你的问题,我仍然认为你的问题可以有效地解决,只要你有一个有效的子集和求解器:我会在 B 上使用分而治之的解决方案:

将 B 切成两块大致相等的 B1 和 B2 使用您的子集和求解器在 A 中搜索总和等于 sum(B1) 的所有子集 S。 对于每个这样的 S: 递归调用solve(S, B1)和solve(A - S, B2) 如果两者都成功,您就有了解决方案

但是,您下面的 (71, 10) 问题对于我建议的动态规划解决方案来说是遥不可及的。


顺便说一句,这里是您的问题的快速解决方案不是使用分而治之,而是包含我的动态求解器的正确适应以获得所有解决方案:

class NotFound(BaseException):
    pass

from collections import defaultdict
def subset_all_sums(A, N):
    res = defaultdict(set, 0 : ())
    for nn, i in enumerate(A):
        # perform a deep copy of res
        newres = defaultdict(set)
        for v, l in res.items():
            newres[v] |= set(l)
            for v, l in res.items():
                if v+i <= N:
                    for s in l:
                        newres[v+i].add(s+(i,))
                        res = newres
                        return res[N]

def list_difference(l1, l2):
    ## Similar to merge.
    res = []
    i1 = 0; i2 = 0
    while i1 < len(l1) and i2 < len(l2):
        if l1[i1] == l2[i2]:
            i1 += 1
            i2 += 1
        elif l1[i1] < l2[i2]:
            res.append(l1[i1])
            i1 += 1
        else:
            raise NotFound
            while i1 < len(l1):
                res.append(l1[i1])
                i1 += 1
                return res

def solve(A, B):
    assert sum(A) == sum(B)
    if not B:
        return [[]]
        res = []
        ss = subset_all_sums(A, B[0])
        for s in ss:
            rem = list_difference(A, s)
            for sol in solve(rem, B[1:]):
                res.append([s]+sol)
                return res

然后:

>>> solve(A, B)
[[(15, 33, 39, 96), (36,), (8, 15, 60, 68, 80), (9, 46, 73), (45, 92)],
 [(15, 33, 39, 96), (36,), (8, 9, 15, 46, 73, 80), (60, 68), (45, 92)],
 [(8, 15, 15, 33, 39, 73), (36,), (9, 46, 80, 96), (60, 68), (45, 92)],
 [(15, 15, 73, 80), (36,), (8, 9, 33, 39, 46, 96), (60, 68), (45, 92)],
 [(15, 15, 73, 80), (36,), (9, 39, 45, 46, 92), (60, 68), (8, 33, 96)],
 [(8, 33, 46, 96), (36,), (9, 15, 15, 39, 73, 80), (60, 68), (45, 92)],
 [(8, 33, 46, 96), (36,), (15, 15, 60, 68, 73), (9, 39, 80), (45, 92)],
 [(9, 15, 33, 46, 80), (36,), (8, 15, 39, 73, 96), (60, 68), (45, 92)],
 [(45, 46, 92), (36,), (8, 15, 39, 73, 96), (60, 68), (9, 15, 33, 80)],
 [(45, 46, 92), (36,), (8, 15, 39, 73, 96), (15, 33, 80), (9, 60, 68)],
 [(45, 46, 92), (36,), (15, 15, 60, 68, 73), (9, 39, 80), (8, 33, 96)],
 [(45, 46, 92), (36,), (9, 15, 15, 39, 73, 80), (60, 68), (8, 33, 96)],
 [(9, 46, 60, 68), (36,), (8, 15, 39, 73, 96), (15, 33, 80), (45, 92)]]

>>> %timeit solve(A, B)
100 loops, best of 3: 10.5 ms per loop

所以对于这种规模的问题来说它是相当快的,尽管这里没有优化。

【讨论】:

它似乎与子集和问题有关,但它与 AFAICT 的问题并不完全相同。看起来S 也是A 的一个分区。不过你是对的,它肯定是 NP 难的。 感谢您的回答和代码!实际上,它与正常的子集和问题有点不同。我更新了问题,请您再检查一遍好吗? 问题是,如果你找到一个 sum 的子集(例如 [9800000, 225000, 2805000, 4505000, 154000, 9570000, 7670300] for 34729300),你可能使用了一个应该用于另一个 sum 的元素(例如 37047600) . @hivert 的方法速度快而且效果很好,但是您需要一些额外的逻辑和回溯才能找到正确的分区。 请注意,您的解决方案不在全局解决方案问题中;)【参考方案2】:

一个完整的解决方案,它计算所有的方式来做一个总计。 我使用整数作为速度和内存使用的特征集:19='0b10011' 在这里代表[A[0],A[1],A[4]]=[8,9,33]

A = [8, 9, 15, 15, 33, 36, 39, 45, 46, 60, 68, 73, 80, 92, 96]
B =[183, 36, 231, 128, 137]

def subsetsum(A,N):
    res=[[0]]+[[] for i in range(N)]
    for i,a in enumerate(A):
        k=1<<i        
        stop=[len(l) for l in res] 
        for shift,l in enumerate(res[:N+1-a]):
            n=a+shift   
            ln=res[n]
            for s in l[:stop[shift]]: ln.append(s+k)
    return res

res = subsetsum(A,max(B))
solB = [res[b] for b in B]
exactsol = ~-(1<<len(A))

def decode(answer):
    return [[A[i] for i,b in enumerate(bin(sol)[::-1]) if b=='1'] for sol in answer] 

def solve(i,currentsol,answer):
        if currentsol==exactsol : print(decode(answer))
        if i==len(B): return
        for sol in solB[i]:
                if not currentsol&sol:
                    answer.append(sol)
                    solve(i+1,currentsol+sol,answer)
                    answer.pop()

对于:

solve(0,0,[])

[[9, 46, 60, 68], [36], [8, 15, 39, 73, 96], [15, 33, 80], [45, 92]]
[[9, 46, 60, 68], [36], [8, 15, 39, 73, 96], [15, 33, 80], [45, 92]]
[[8, 15, 15, 33, 39, 73], [36], [9, 46, 80, 96], [60, 68], [45, 92]]
[[9, 15, 33, 46, 80], [36], [8, 15, 39, 73, 96], [60, 68], [45, 92]]
[[9, 15, 33, 46, 80], [36], [8, 15, 39, 73, 96], [60, 68], [45, 92]]
[[15, 15, 73, 80], [36], [9, 39, 45, 46, 92], [60, 68], [8, 33, 96]]
[[15, 15, 73, 80], [36], [8, 9, 33, 39, 46, 96], [60, 68], [45, 92]]
[[45, 46, 92], [36], [15, 15, 60, 68, 73], [9, 39, 80], [8, 33, 96]]
[[45, 46, 92], [36], [9, 15, 15, 39, 73, 80], [60, 68], [8, 33, 96]]
[[45, 46, 92], [36], [8, 15, 39, 73, 96], [60, 68], [9, 15, 33, 80]]
[[45, 46, 92], [36], [8, 15, 39, 73, 96], [15, 33, 80], [9, 60, 68]]
[[45, 46, 92], [36], [8, 15, 39, 73, 96], [60, 68], [9, 15, 33, 80]]
[[45, 46, 92], [36], [8, 15, 39, 73, 96], [15, 33, 80], [9, 60, 68]]
[[15, 33, 39, 96], [36], [8, 15, 60, 68, 80], [9, 46, 73], [45, 92]]
[[15, 33, 39, 96], [36], [8, 9, 15, 46, 73, 80], [60, 68], [45, 92]]
[[15, 33, 39, 96], [36], [8, 15, 60, 68, 80], [9, 46, 73], [45, 92]]
[[15, 33, 39, 96], [36], [8, 9, 15, 46, 73, 80], [60, 68], [45, 92]]
[[8, 33, 46, 96], [36], [15, 15, 60, 68, 73], [9, 39, 80], [45, 92]]
[[8, 33, 46, 96], [36], [9, 15, 15, 39, 73, 80], [60, 68], [45, 92]]

注意,当两个15 不在同一个子集中时,解会加倍。

解决了唯一解问题:

A=[1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011,
   1012, 1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020, 1021, 1022, 1023,
   1024, 1025, 1026, 1027, 1028, 1029, 1030, 1031, 1032, 1033, 1034, 1035,
   1036, 1037, 1038, 1039, 1040, 1041, 1042, 1043, 1044, 1045, 1046, 1047, 
   1048, 1049]

B=[5010, 5035, 5060, 5085, 5110, 5135, 5160, 5185, 5210, 5235]

在一秒钟内。不幸的是,它还不足以针对 (71,10) 问题进行优化。

还有一个,纯动态编程精神::

@functools.lru_cache(max(B))
def solutions(n):
    if n==0 : return set(frozenset()) #
    if n<0 :  return set()
    sols=set()
    for i,a in enumerate(A):
            for s in solutions(n-a):
                if i not in s : sols.add(s|i)
    return sols

def decode(answer): return([[A[i] for i in sol] for sol in answer]) 

def solve(B=B,currentsol=set(),answer=[]):
    if len(currentsol)==len(A) : sols.append(decode(answer))
    if B:
        for sol in solutions(B[0]):
            if set.isdisjoint(currentsol,sol):
                solve(B[1:],currentsol|sol,answer+[sol]) 

sols=[];solve()

【讨论】:

非常感谢您的回答和代码。由于问题是 NP-Complete 并且数量太大,我想是时候放弃这个问题了。再次感谢您的回答! 我添加了一些解释。你的 71,10 问题是什么?我有兴趣尝试解决它。​​ 感谢您的关注,这里是link的实际数据。 由于可能的最小子集数量大于 1400,我认为先计算所有可能的子集然后找到不重复的答案是不可能的,也没有效率。 是的。我很快到达了一张 4Go 桌。这真是一个难题;)。

以上是关于多子集和计算的主要内容,如果未能解决你的问题,请参考以下文章

计算给出数组中最小标准差的子集

argparse 位置多项选择默认子集:无效选择

如何根据多对多关系选择用户子集?

在 AGDA 中计算自然数的子集

从数据子集计算平均值和方差的在线算法

NSPredicate,使用一对多关系的子集获取结果