【问题标题】:All possible sets of integers with sum of zero所有可能的整数集,总和为零
【发布时间】:2021-03-27 19:14:09
【问题描述】:

在以下条件下找到所有可能的集合或包(可以有重复的元素),其中元素的总和为零的最佳算法是什么:

  • 每个元素都是一个整数
  • 添加值为 V 的元素的成本为 |V|+2
  • 每套总成本应小于M

例如:

Sets ---------- Cost
{0}              2
{0, 0}           4
{0, 0, 0}        6
{1, -1}          6
{2, -2}          8
{0, 1, -1}       8
{0, 0, 0, 0}     8
...

一种方法是开始制作长度为 K (K=1, 2, ...) 的集合,然后遍历所有可能的组合并选择满足条件的组合。但是,这种方法效率非常低。我不是图论专家,但我想应该有更好的方法。特别是,如果它可以在python中有效地实现。

【问题讨论】:

  • 既然有总成本小于M的条件
  • 只是好奇,为什么是|V|+2,这和图论有什么关系?
  • @Owen 图论只是根据 OP 的建议,我认为
  • 集合不能有重复,所以{0, 0}不是集合。
  • 有时称为“包”。

标签: python algorithm set graph-theory directed-acyclic-graphs


【解决方案1】:

很容易做到非常有效。让我们对常见问题做两个简化。


首先我们了解发电机组的结构。集合包含零个或多个0(我们称之为Zeros集),Pos集,仅包含正数,Neg集, 只包含负数。

请注意,PosNeg 中的数字之和(如果每个数字都取反)是等价的。这是因为所有数字之和为零。

很酷的是我们可以独立生成ZerosPosNegZeros 集合有一个要生成的参数 - 元素的数量。但是 PosNeg 有两个 -- 元素的数量和它的总和(第二个对于两个集合是相等的)。

假设我们有 M - 我们的边界,SPos 中元素的总和(因此 (-S) in Neg), a, bzPos、否定。那么我们的集合的代价是2(a+b+c+S)。

因此,我们可以通过调整这些数字轻松管理这一代。


现在让我们独立生成所有三个集合(使用我们之前找到的约束)并将其合并。要生成由参数 z 设置的 Zeros,我们需要...就这样做

很明显,生成PosNeg是相似的,只是参数ab不同。这只是一个partition of numbers


所以,朴素的解决方案是:

  1. 获取M
  2. 生成abzS的所有组合,满足简单约束(都是非-负整数和2(a+b+c+S))
  3. z 生成 Zeros = {0}^z
  4. aS生成所有S长度a的分区
  5. for each a_i: partition 来自上一步:
  6. bS生成所有S长度a的分区
  7. yield Zeros + a_i + b_i

尽管此解决方案简单,但它相当比生成所有集合和过滤更有效。


为了boost-speedup-accelerate,让我们将这个集合看成一个数字Pos_Zeros_Neg。它只有三位数字——我们的集合。我们将..增加它!

  1. 生成 abzS
  2. init_pos <- {S}; init_neg <- {S}
  3. 初始化 Pos, Zeros, Neg <- init_pos, {0}^z, init_neg
  4. while has_next(Pos, a):
  5. |......while has_next(Neg, b):
  6. |......|......yield Zeros + Pos + {-Pos}
  7. |......|......next(Neg, b)
  8. |......否定 <-init_neg
  9. |......next(Pos, a)

在哪里

  • {-Pos} 是一个 Pos 集合,但所有数字都取反;
  • next(Set, len) -- 按字典顺序生成 Setnext 分区的函数,长度为 len;
  • has_next(Set, len) -- 检查该集合是否存在。

编辑:

这是我的 .py file fast 版本

编辑++

这里是完整的代码:

def gen(n, leng):
    if leng == 0:
        yield []
        return
    if leng == 1:
        yield [n]
        return
    if leng > n:
        return

    s = [n - leng + 1]
    for i in range(leng - 1):
        s.append(1)
    s.append(-1)

    while True:
        yield s[:-1]

        if s[1] <= s[0] - 2:
            s[0] -= 1
            s[1] += 1
            continue

        ind = 2
        summ = s[0] + s[1] - 1
        while s[ind] >= s[0] - 1:
            summ += s[ind]
            ind += 1

        if ind >= leng:
            break

        x = s[ind] + 1
        s[ind] = x
        ind -= 1
        for i in range(ind, 0, -1):
            s[i] = x
            summ -= x
        s[0] = summ

def gen_abzs(m):
    m = m // 2
    for s in range(m + 1):
        for a in range(min(m-s, s) + 1):
            for b in range(min(m-s-a, a) + 1):
                if a == 0 and b == 0 and s > 0:
                    continue
                for z in range(m-s-a-b + 1):
                    if a == 0 and b == 0 and s == 0:
                        yield [a, b, z, s]
                        continue
                    if a == 0 and b != 0:
                        continue
                    if a != 0 and b == 0:
                        continue
                    yield [a, b, z, s]
                    if a != b:
                        yield [b, a, z, s]

def solve(m):
    for a,b,z,s in gen_abzs(m):
        zeros = [0] * z
        for pos in gen(s, a):
            for neg in gen(s, b):
                neg = list(map(lambda x:-x, neg))
                ans = zeros.copy()
                for i in pos:
                    ans.append(i)
                for i in neg:
                    ans.append(i)
                yield ans

def main():
    for s in solve(20):
        print(s)

if __name__ == '__main__':
    main()

【讨论】:

  • 谢谢,您能否为您的算法提供示例。这对我来说不是很清楚。也应该首先“z”是“c”?
  • "z" -- 类似于 "zeros"
  • 是的,示例在答案的末尾@Roy
  • 你能复制你帖子里的文件内容吗
【解决方案2】:

警告:这并不是最好的算法。有一些重复的检查可能会被删除。但这是一个没有重复结果的有效解决方案。


任何成本 Q

此外,我们可以通过找到成本正好为 M 的袋子集来进一步简化。然后,要找到所有

所以,现在我们的目标是找到一组不包含零且成本正好为 M 的袋子。

为了简单起见,让我们考虑一下有多少种方法可以选择 k 个数字来精确地生成 M,然后遍历不同的 k。然后,我们可以将一些 k 分配给正数,将一些 k 分配给负数,并确保它们总和为相同的值。

所以,现在我们正在寻找一种通往find all partitions of n 的方法,但只有特定的长度。然后有一个包满足这些分区的每对可能对的约束。我调整了链接问题的答案,并将其与上述推理结合起来得到:

import functools

@functools.lru_cache(maxsize=1024*1024)
def partitions_with_k(k, largest, *rest):
    '''
    Finds all partitions which sum to `largest`
    with precisely k elements in it
    '''
    result = [[largest, *rest]]
    if (rest and len(rest) == k-1 or k == 1):
        return result
    min = rest[0] if rest else 1
    max = largest // 2
    for n in range(min, max+1):
        result.extend(partitions_with_k(k, largest-n, n, *rest))
    result = [p for p in result if len(p) == k]
    return result

def get_bags_exact(M, a=1, b=2):
    '''
    Finds all bags without zeros with total cost exactly equal to M
    '''
    # let k be the number of integers in the bag.
    # The minimum cost of including a non-zero value is a*1+b,
    # therefore we only need to check k up to M//(a+b) (the +/-1 solution, if it exists)
    result = []
    min_cost = a+b
    for k in range(1, 1+M//min_cost):
        for kn in range(1, k):
            kp = k-kn
            # Let P be the positive sum of integers in the partition
            # M = a*P*2 + k*b
            P = (M-k*b)/(a*2)
            # Clearly we can only accept integer sums
            if P % 1 == 0:
                P = int(P)
                pos = partitions_with_k(kp, P)
                neg = partitions_with_k(kn, P)
                neg = [[-e for e in part] for part in neg]
                for partp in pos:
                    for partn in neg:
                        result.append(partp + partn)
    return result

def get_bags(M, a=1, b=2):
    ''' Finds all bags with cost M or less '''
    result = []
    n_zeros = (M)//b
    for i in range(n_zeros+1):
        result.append([0]*i)
    for Q in range(1, 1+M):
        n_zeros = (M-Q)//b
        exactlyQ = get_bags_exact(Q, a, b)
        for i in range(n_zeros+1):
            for bag in exactlyQ:
                result.append(bag+[0]*i)
    return result

在 python 中使用递归函数的一个主要好处是您可以轻松地缓存结果。所以我会将它保留为递归版本,但如果你愿意,你可以让它迭代。无论如何,我发现partitions_with_k 并不是真正的瓶颈(见下文)。

我手动检查到 M=14,并在 M=50 之前进行了健全性检查:

bags = get_bags(50)
to_check = [tuple(sorted(b)) for b in bags]
assert len(to_check) == len(set(to_check))

从理论上讲,这可以通过不进行显式过滤器在partitions_with_k 内部得到改进,并且永远不会添加任何错误长度的内容,但我认为您必须提前知道所有时间结果将是。我不确定这是否可能。我看不到它。也许你可以。

潜在地,有一个使用双重解决方案的好方法:取任何袋子的负片也是一种解决方案。在这里,我根本没有明确使用它。

在不考虑partitions_with_k 的情况下,get_bags 的运行时复杂度为 O(M^4)。在缓存之前,partitions_with_k 是 O(M^2)。所以,粗略地说,它是 O(M^6)。序列大小增长相当快。它很快就会变慢,而且内存成本也很大。

例如M=70 有大约 900 万个包,在我的机器上花费了大约 9.2 秒,并且在处理时用掉了大约 1.5GB 的 RAM

我使用cProfile 来查看实际花费的时间。它告诉我这 9.2 秒中的大约 5.6 秒,解释器在 get_bags 中,而只有 0.14 秒在 partitions_with_k 中。老实说,我不确定get_bags 有什么这么慢;我猜添加所有带有 0 的解决方案需要时间?

$ python3 -m cProfile -s cumtime bags.py
9012501
         12069213 function calls (12004973 primitive calls) in 9.246 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    9.262    9.262 {built-in method builtins.exec}
        1    0.030    0.030    9.262    9.262 bags.py:1(<module>)
        1    5.670    5.670    9.233    9.233 bags.py:45(get_bags)
       70    2.949    0.042    3.222    0.046 bags.py:19(get_bags_exact)
 11704256    0.444    0.000    0.444    0.000 {method 'append' of 'list' objects}
64614/374    0.108    0.000    0.140    0.000 bags.py:3(partitions_with_k)
     2970    0.015    0.000    0.031    0.000 bags.py:39(<listcomp>)
    52305    0.019    0.000    0.024    0.000 bags.py:16(<listcomp>)
   180736    0.008    0.000    0.008    0.000 {built-in method builtins.len}
    64240    0.004    0.000    0.004    0.000 {method 'extend' of 'list' objects}


您的时间会因硬件和软件版本而异,但与我的原生 python 3.6 安装相比,当我使用 python:3.6 docker 映像运行此程序时,比例是一致的。

【讨论】:

  • 这是非常聪明的方法。是否可以将其推广到 |v| 的通用成本函数? + b,其中 a 和 b 是整数?
  • 当然可以。因为每个v 都乘以a,所以我们仍然可以将P 定义为包中正整数的总和,并将k 定义为包中的整数个数,这样M = a*P*2 + k*b。在get_bags_exact 内部有一个P 的计算传递给partitions_with_k。所以我们要做的就是根据更新后的代价函数改变P的计算,更新get_bags中加0的代价。我会更新我的答案。然而,它的效率有点低。
【解决方案3】:

编辑:好的,我克服了重复问题。


我们可以生成具有可能的总和和权重的组合并将它们放入字典中。 请注意,有些对给出了一些组合,所以如果 dict 是列表的列表,则值。

然后对于每个 weight1/sum 对,我们寻找另一对具有相同总和(使为零)并且 weight2 应该在需要的范围内给出 weight1+weight2

dic = dict()
zeros = []

def makebag(mm, mm0, last, lst):
    if mm == 0:
        sm = sum(lst)
        if sm:
            ky = (mm0, sm)
            if ky in dic:
                dic[ky].append(lst)
            else:
                dic[ky] = [lst]
        return
    for i in range(last, mm - 1):
        makebag(mm - i - 2, mm0, i, lst + [i])

def makezero(m):
    for i in range((m + 2)//2):
        zeros.append([0]*i)
        if i:
            print(zeros[-1])

    for i in range(m + 1):
        makebag(i, i, 1, [])

    for ky in dic:
        for mm in range(1,m + 1):
            cmplmntry = (mm - ky[0], ky[1])
            if cmplmntry in dic:
                for plus in dic[ky]:
                    for y in dic[cmplmntry]:
                        minus = [-t for t in y]
                        for i in range((m - mm + 2) // 2):
                            print(minus + zeros[i] + plus)

makezero(12)
#print(dic)  # for reference

>>>
[0]
[0, 0]
[0, 0, 0]
[0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0]
[-1, 1]
[-1, 0, 1]
[-1, 0, 0, 1]
[-1, 0, 0, 0, 1]
[-2, 2]
[-2, 0, 2]
[-2, 0, 0, 2]
[-1, -1, 2]
[-1, -1, 0, 2]
[-3, 3]
[-3, 0, 3]
[-1, -2, 3]
[-2, 1, 1]
[-2, 0, 1, 1]
[-1, -1, 1, 1]
[-4, 4]
[-3, 1, 2]

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-12-17
    • 1970-01-01
    • 1970-01-01
    • 2013-03-23
    • 1970-01-01
    • 2019-05-24
    相关资源
    最近更新 更多