首先让我们只用两组来解决这个问题。这被称为“二和”问题。您有两组 a 和 b 相加到 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')
接下来我们可以解决“四和”问题。这次我们有四套c、d、e和f。通过将c 和d 组合成a,并将e 和f 组合成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 只是浪费时间和空间。