【问题标题】:"Smart" method to solve a subset sum problem with multiple sets解决多组子集和问题的“智能”方法
【发布时间】:2020-06-10 19:19:13
【问题描述】:

我有一定数量的集合,每个集合都包含不同数量的唯一数字 - 在它们所属的集合中是唯一的,在其他集合中是独一无二的。

我想制作一个最好在 Python 中实现的算法 - 但它可以是任何其他语言 - 从这些集合中的每一个中找到一个数字组合,总和为指定数字,如果这有帮助,我知道有可以多次使用同一个集合,并且可以重复使用集合中的一个元素。

实际示例:假设我有以下集合:

A = {1, 3, 6, 7, 15}
B = {2, 8, 10}
C = {4, 5, 9, 11, 12}

我想用find_subset_combination(expected_sum, subset_list)方法获取数字组合

>>> find_subset_combination(41, [A, B, B, C, B])
[1, 8, 10, 12, 10]

here 已经提出了解决这个问题的方法,但是这是一种蛮力的方法;由于在我的情况下集合的数量和它们的大小会大得多,我想要一种算法以尽可能少的迭代次数运行。

你会建议我什么方法?

【问题讨论】:

  • 每个集合的元素可以重复使用吗?
  • @wim 可以。我将更新问题以澄清
  • 该示例建议您只需要找到一个组合,而不是解决方案的总数或解决方案的枚举。对吗?
  • 这是 NP 难的。您不会找到有效的通用解决方案。
  • @wim 这是正确的。由于找到一种组合可能已经相当困难,因此我并不要求多种解决方案。我已经知道如何从一个组合中找到其他可能的组合。

标签: python subset-sum


【解决方案1】:

首先让我们只用两组来解决这个问题。这被称为“二和”问题。您有两组 ab 相加到 l。由于a + b = l,我们知道l - a = b。这很重要,因为我们可以在 O(1) 时间内确定 l - a 是否在 b 中。而不是循环通过b 在 O(b) 时间内找到它。这意味着我们可以在 O(a) 时间内解决 2 和问题。

注意:为简洁起见,提供的代码仅产生一种解决方案。但是将 two_sum 更改为生成器函数可以将它们全部返回。

def two_sum(l, a, b):
    for i in a:
        if l - i in b:
            return i, l - i
    raise ValueError('No solution found')

接下来我们可以解决“四和”问题。这次我们有四套cdef。通过将cd 组合成a,并将ef 组合成b,我们可以使用two_sum 来解决O(cd + ef) 空间和时间的问题。要组合这些集合,我们只需使用笛卡尔积,将结果相加即可。

注意:要获得所有结果,请对所有结果 a[i]b[j] 执行笛卡尔积。

import itertools


def combine(*sets):
    result = {}
    for keys in itertools.product(*sets):
        results.setdefault(sum(keys), []).append(keys)
    return results


def four_sum(l, c, d, e, f):
    a = combine(c, d)
    b = combine(e, f)
    i, j = two_sum(l, a, b)
    return (*a[i][0], *b[j][0])

很明显,“三和”问题只是“四和”问题的简化版本。不同之处在于我们在开始时得到a,而不是被要求计算它。这在 O(a + ef) 时间和 O(ef) 空间中运行。

def three_sum(l, a, e, f):
    b = combine(e, f)
    i, j = two_sum(l, a, b)
    return (i, *b[j][0])

现在我们有足够的信息来解决“六和”问题。问题归结为我们如何划分所有这些集合?

  • 如果我们决定将它们配对,那么我们可以使用“三和”解决方案来获得我们想要的结果。 但是这可能不是在最佳时间运行,因为它在 O(ab + bcde) 或 O(n^4) 时间内运行,如果它们都是相同的大小。
  • 如果我们决定将它们放在三重奏中,那么我们可以使用“两个和”来得到我们想要的。这在 O(abc + def) 或 O(n^3) 中运行(如果它们的大小相同)。

此时,我们应该拥有所有信息来制作一个在 O(n^⌈s/2⌉) 时间和空间内运行的通用版本。其中 s 是输入函数的集合数量。

def n_sum(l, *sets):
    midpoint = len(sets) // 2
    a = combine(*sets[:midpoint])
    b = combine(*sets[midpoint:])
    i, j = two_sum(l, a, b)
    return (*a[i][0], *b[j][0])

您可以进一步优化代码。两者之和的大小很重要。

  • 为了举例说明这一点,您可以想象一侧有 4 组 1 个数字,另一侧有 4 组 1000 个数字。这将在 O(1^4 + 1000^4) 时间内运行。这显然真的很糟糕。相反,您可以平衡两个总和的两侧以使其更小。通过在等式两边设置 2 组 1 个数字和 2 组 1000 个数字,性能会提高; O(1^2×1000^2 + 1^2×1000^2) 或简单的 O(1000^2)。远小于 O(1000^4)。

  • 如果您有 3 组 1000 数字和 3 组 10 数字扩展上一点,那么最好的解决方案是在一侧放置两个 1000,而在另一侧放置其他所有数字:

    • 1000^2 + 10^3×1000 = 2_000_000
    • 交错排序,两边大小相同 (10, 1000, 10), (1000, 10, 1000)
      10^2×1000 + 10×1000^2 = 10_100_000

此外,如果提供的每组数量相等,则只需调用一次combine 即可将运行时间缩短一半。例如,如果输入是n_sum(l, a, b, c, a, b, c)(没有上述优化),那么显然第二次调用combine 只是浪费时间和空间。

【讨论】:

  • 非常有趣的想法,即使我需要更多时间来完全理解和吸收所有这些解释。只是遗憾的是,算法解决方案在对大量集合(20 个,每个集合的容量从 5 到 80 不等)进行测试时显示,内存非常密集。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-11
  • 2023-02-21
  • 2012-12-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多